Everyone draws the same diagram the first time: VPN into the VPC, NAT gateway out to the internet, done. AWS does not support that path when the VPN terminates on a virtual private gateway, and the failure is quiet enough that teams rebuild it more than once. The design that works needs a transit gateway, which changes the gateway and the route tables on both sides. Compare the two shapes before you draw anything.

The topology that looks obvious and is not supported

AWS's NAT gateway documentation draws a line that surprises anyone who has only built single-VPC designs: traffic from a Site-to-Site VPN connection cannot be routed to a NAT gateway through a virtual private gateway. The gateway type terminating the VPN decides whether a translation path exists at all. The mistake survives because the tunnel status is UP and an EC2 instance in the same subnet reaches the internet without trouble. Neither observation exercises the direction that is broken.

In practice the packet arrives with an on-premises source address and finds no supported next hop that would translate it. Depending on the tables it is dropped at the egress boundary, and because the tunnel stays UP while an instance in the same subnet still reaches the internet, the failure gets reported as an on-premises firewall problem.

Virtual private gateway vs transit gateway

A virtual private gateway belongs to one VPC. It terminates the VPN, installs routes into that VPC's route tables, and stops there. It has no attachment model, so it cannot hand traffic to another VPC or to a shared egress segment. That single-VPC scope is the whole limitation, and no route you add inside the VPC changes it.

A transit gateway is a regional hub where the VPN, each spoke VPC and a shared services VPC all appear as attachments. It carries its own route tables, which choose the attachment that receives a given prefix.

That hub is what makes centralized egress possible. The VPN lands as one attachment, the egress VPC lands as another, and a transit gateway route table sends internet-bound prefixes from the first to the second. With a virtual private gateway there is no second attachment to send anything to, which is why the design cannot be patched into existence.

The supported egress path, hop by hop

The reference design has five hops, and every arrow is a routing decision you have to configure. Traffic leaves the on-premises network into the Site-to-Site VPN attachment. The transit gateway route table forwards it to the egress VPC attachment, where the subnet table sends it to a public NAT gateway. The NAT gateway translates the source and the internet gateway carries the packet out.

On-premises network
  -> Site-to-Site VPN attachment on the transit gateway
  -> transit gateway route table
  -> egress VPC attachment
  -> public NAT gateway in that VPC
  -> internet gateway

Two details decide whether the last hops work. The NAT gateway must be public, with an elastic IP and a route to an internet gateway, because a private NAT gateway translates between networks and never becomes an internet exit.

Plan one NAT gateway per Availability Zone you intend to serve, with each subnet table pointing at the local one. Keep the transit gateway table and the VPC tables as separate objects in your notes, since one chooses an attachment and the other chooses a resource inside the VPC. A default route in only one of them leaves half the path undefined and produces the partial failures that are hardest to attribute.

The return path is where these designs fail

A NAT gateway is stateful for flows it has already seen, so replies to an outbound connection return without a new inbound rule. That property does not publish an on-premises service to the internet, and treating it as though it did is how a team spends a day debugging a connection that was never going to establish. For client-originated traffic, four routes have to agree: the on-premises policy, the transit gateway table, the egress subnet table and the NAT subnet table. Miss the last one and the request leaves the building while the reply dies inside AWS.

Validate with a real client behind the VPN and check the public source address it presents. If that address is the elastic IP of the NAT gateway, translation is happening; if it is anything else, you are measuring a different egress.

The asymmetric version of this failure is worth recognising early. When one Availability Zone's NAT subnet table has the route back to the client prefix and another does not, new connections succeed or fail depending on which zone the flow lands in, and the pattern gets misread as flaky internet. Compare the subnet tables for every zone you serve rather than fixing the one where the ticket was raised, because a missing route is the whole fault and nothing on the far side needs to change.

When an explicit proxy is the better design

An HTTP proxy on an instance in the egress VPC is a legitimate alternative and often the cheaper one. Clients are configured to use it, the proxy opens its own connection outward, and the return path is the proxy's problem rather than a symmetric routing puzzle. For a small number of applications this removes most of the transit gateway work. It also gives you per-user logging that a NAT gateway cannot produce.

It is not transparent, though. Anything that ignores proxy settings, and every protocol that is not HTTP, will not follow that path.

The choice comes down to what your applications actually speak and how much identity you need in the logs. Where they speak HTTP, the proxy earns its place; where you need general egress for arbitrary protocols, the transit gateway design is the one AWS supports. Deciding that up front is cheaper than discovering it after the route tables are built.

Related articles