The situation
The organisation has grown organically: twelve AWS accounts, forty VPCs, each built with the same reference architecture (three-AZ, public + private subnets, a NAT gateway in each AZ). The NAT bill is the single largest networking line item. Worse, every time compliance adds a new outbound destination to the SaaS allow-list, somebody has to update forty VPC route tables or forty NACLs or forty security-group egress rules. Observability is fragmented across forty Flow Log destinations.
The platform team has a proposal: one network hub account, with a single Transit Gateway, a single egress VPC carrying NAT gateways and a Network Firewall, and all spoke VPCs’ default routes pointing to the TGW. The spokes stop having their own NATs; they route through the hub.
Before we sign off, we need to walk the options, because “centralise everything” hides a few traps.
What actually matters
The core trade in network centralisation is uniformity in exchange for blast radius. Forty independent egress paths are forty things to configure, forty things to monitor, and forty separate failure domains. One central egress path is one thing to configure, one place to monitor, and one place where a misconfiguration takes everything offline. The question is always: which trade pays for itself?
The first thing to ask is: what are we actually trying to centralise? Three distinct jobs usually get tangled:
- IP exhaustion: NAT gateways let many private addresses share a small pool of public ones. Centralising NAT saves money but doesn’t change the address story.
- Egress inspection: somebody needs to see, allow, or deny outbound traffic. This is the firewalling job, stateful rules, TLS SNI inspection, domain allow-lists.
- Egress logging: an auditable record of what talked to what. Flow Logs, DNS query logs, firewall logs.
Each can live in the spoke, in the hub, or both. The centralised-egress pattern puts all three in the hub; other patterns split them.
The second thing is: how much extra latency do we accept? Centralising egress means every outbound packet takes an extra hop through the TGW and the firewall. That’s typically a few hundred microseconds of latency and a TGW data-processing charge per GB. Fine for most workloads; painful for a latency-sensitive service that talks to an external API on the hot path.
The third is: how do we route? Once the spokes’ default routes point to TGW, everything not matching a more specific route goes to the hub. That includes traffic to AWS service endpoints, which we usually want to stay on the AWS backbone via Gateway endpoints, not round-trip through the hub.
The fourth is: what happens when the hub fails? A central egress VPC is a single-point-of-failure unless we’re careful. Two egress VPCs in different AZs? Two TGWs? Cross-Region failover? Each adds cost; none is free.
What we’ll filter on
- NAT cost profile, fixed per-hour plus per-GB processed, and how many NATs we’re paying for.
- Inspection granularity, can we allow/deny by domain, TLS SNI, stateful flow?
- Logging completeness, do we see source, destination, decision, bytes?
- Operational overhead, how many places to change a rule?
- Blast radius of a misconfiguration, how many workloads break?
The egress landscape
-
Per-VPC NAT gateways (status quo). Each VPC has two or three NAT gateways (one per AZ), routes
0.0.0.0/0to them from private subnets. Simple, no inter-VPC dependency, AZ-resilient. Expensive at scale: every NAT costs ~$0.045/hour plus data processing, so forty VPCs with three AZs is ~120 NATs = ~$130/day just in fixed cost, before a byte flows. Rules live in spoke security groups; logging is forty Flow Log destinations. -
Centralised egress via Transit Gateway + egress VPC. One hub VPC hosts the NAT gateways. All spoke VPCs attach to a Transit Gateway and route
0.0.0.0/0to the TGW. The TGW forwards to the egress VPC’s attachment. The egress VPC’s route table sends the traffic out through the NATs. One TGW, one set of NATs (three, for AZ resilience), one bill. Inspection and logging still need somewhere to live; typically the egress VPC also runs Network Firewall for stateful inspection and domain filtering. -
Centralised egress via VPC peering. The pre-TGW version: every spoke peered to a shared egress VPC. Doesn’t scale, peering connections are 1:1, and forty spokes means hub-VPC peering config that’s hard to maintain. Superseded by TGW for anything past a few VPCs.
-
AWS PrivateLink for SaaS-only egress. If the reason we have egress is “talk to a specific SaaS provider that has a PrivateLink endpoint in AWS,” we don’t need NAT at all for that traffic. A VPC endpoint service connects to the provider privately; traffic never traverses the public internet. Doesn’t replace general egress, but reduces what needs centralising.
-
AWS Gateway Load Balancer in front of a third-party appliance. Traffic steering via GWLB to a fleet of firewall appliances (Palo Alto, Fortinet, etc.) inside the hub. Same centralisation story, different inspection engine. Worth knowing when the organisation already has contracts with a firewall vendor and wants to keep the same policy language across on-prem and cloud.
-
Cloud WAN. AWS’s managed multi-Region wide-area network. For single-Region egress centralisation it’s overkill; for a multi-Region hub-and-spoke with egress in multiple Regions it’s the one-click version of building it yourself on TGW. Priced higher per-attachment.
Side by side
| Option | NAT cost profile | Inspection | Logging | Ops overhead | Blast radius |
|---|---|---|---|---|---|
| Per-VPC NAT | ~120 NATs, local | Spoke SGs only | Per-VPC Flow Logs | 40 places | Per-VPC |
| TGW + egress VPC + ANFW | 3 NATs, central | Domain, SNI, stateful | Centralised firewall + flow logs | 1 place | Whole org |
| VPC peering hub | 3 NATs, central | Spoke SGs | Per-peer | 40 peering configs | Whole hub |
| PrivateLink (SaaS) | N/A for that traffic | ✓ (endpoint policy) | VPC endpoint logs | Per endpoint | Per endpoint |
| GWLB + appliance fleet | 3 NATs, central | Vendor-defined | Vendor + flow logs | Vendor policy + AWS | Whole org |
| Cloud WAN | 3 NATs per Region | Via inspection VPC | Centralised | 1 place per Region | Whole Region |
Reading by workload:
- General outbound HTTP/HTTPS, centralised egress with Network Firewall. Domain allow-listing in one place, NAT bill collapses.
- SaaS providers with PrivateLink, endpoint services per SaaS, bypass central egress.
- AWS service API calls. Gateway endpoints for S3 and DynamoDB, Interface endpoints for the rest. Never leaves the VPC, never touches NAT or TGW.
- Latency-critical calls to a specific external API, accept the per-VPC NAT cost for that workload, route to TGW for the rest. Mixed topology is fine.
The hub topology
The picks in depth
The Transit Gateway. One TGW in the hub account, shared to the organisation via Resource Access Manager (RAM). Each spoke VPC has a TGW attachment in its own account (attachment lives in the spoke, association with TGW route tables happens in the hub). Two TGW route tables: one for spokes (every spoke associated, propagates to the egress attachment), one for egress (egress attached, propagates to all spokes). Spokes cannot route to each other directly, the spoke route table doesn’t propagate to spoke attachments. If two spokes need to talk, they go through a separate pair of route tables, not through the egress VPC.
The egress VPC. A slim VPC in the hub account, CIDR like 10.0.0.0/22, big enough for three AZs of firewall and NAT subnets, no room for workloads. Public subnets host the NAT gateways (one per AZ) and the Internet Gateway attaches at the VPC level. Firewall subnets host the Network Firewall endpoints. TGW attachment subnets host the TGW elastic network interfaces.
Routing through the egress VPC. The route is chained: TGW attachment subnet -> firewall endpoint -> NAT subnet -> NAT gateway -> IGW. Each subnet’s route table has exactly the next hop. The firewall inserts itself via a Gateway Load Balancer endpoint; traffic goes from the TGW attachment into the firewall endpoint in the same AZ, out into the NAT subnet, through the NAT, through the IGW. Return traffic takes the reverse path. Getting the AZ alignment wrong is the most common source of “packets leave but never come back”, the symmetric path is essential.
AWS Network Firewall. Stateful firewall with Suricata-compatible rules. The critical capabilities for centralised egress:
- Domain allow-list via Stateful Rule Groups with
domain-listrules. Allow*.stripe.com,api.github.com, and the package mirrors; deny everything else. - TLS SNI inspection without decryption, the firewall reads the SNI from the TLS ClientHello and matches it against the allow-list. No MITM, no cert distribution, but also no inspection of the encrypted payload.
- Logging to S3, CloudWatch Logs, or Kinesis Data Firehose. Alert logs (rule hits) and flow logs (5-tuple plus bytes) separately configurable.
The firewall isn’t free, per-endpoint hourly fee plus per-GB processed, but it’s cheaper than maintaining Suricata on your own EC2 fleet and the logs land in one Region’s single log group instead of forty.
Gateway endpoints for S3 and DynamoDB. Every spoke VPC gets its own Gateway endpoint for S3 and DynamoDB. Route tables in the spoke have a prefix-list route to the endpoint. This traffic never touches the TGW, never sees the firewall, doesn’t hit NAT. S3 is usually the largest outbound workload by volume; routing it through a central NAT would double the NAT data-processing bill for no benefit.
Interface endpoints for AWS services. For services that don’t have Gateway endpoints (EC2 API, ECR, SSM, etc.), Interface endpoints can live in the spokes or be centralised in a shared-services VPC behind the TGW. Centralising them saves on per-endpoint hourly cost when many spokes share a small set of services; leaving them per-spoke saves TGW transit bytes. The break-even is roughly: if a service sees more than ~50GB/month per spoke, per-spoke endpoints win; below that, central endpoints win.
SCPs and RAM for governance. The TGW is owned by the hub account and shared via RAM to the organisation. An SCP on member accounts denies ec2:CreateTransitGateway, workload accounts can’t spin up their own hub. Another denies ec2:CreateRoute for 0.0.0.0/0 to anything that isn’t the TGW-ID pattern, workloads can’t route around the central egress. The hub owns the path; spokes can’t escape it.
A worked egress trace
A Lambda in the payments VPC in account A needs to call api.stripe.com.
- Lambda makes the HTTPS request. Source IP is Lambda’s ENI in the VPC (10.10.4.37).
- Spoke route table matches default route to the TGW attachment. Packet goes to the TGW.
- TGW spoke RT forwards to the egress VPC attachment. Packet lands in the TGW attachment subnet of the egress VPC in the same AZ.
- That subnet’s route table sends
0.0.0.0/0to the firewall endpoint. Packet enters Network Firewall. - Firewall inspects: TLS ClientHello SNI =
api.stripe.com, matches allow-list, stateful rule passes. Flow recorded. - Packet exits firewall endpoint into the NAT subnet. Route table there sends
0.0.0.0/0to the NAT gateway. - NAT translates source to the NAT’s elastic IP (stripe sees this as the org’s “one IP out”). Stripe can allow-list it once, for the whole org.
- Packet leaves via the IGW, to
api.stripe.com. - Return packet arrives at the IGW, matches the NAT’s elastic IP, NAT translates back, routes to the firewall endpoint (stateful match, allowed), back through TGW, back to the Lambda ENI.
Total extra latency: ~1-2ms (TGW hop, firewall inspection, NAT). Total audit artefacts: firewall alert log with the SNI and source IP, firewall flow log with byte counts, VPC Flow Log in the egress VPC. Total places to update the allow-list: one.
A second worked trace: the hub fails
A misconfigured firewall rule is pushed to the Network Firewall at 10:14. Outbound HTTPS starts failing in every spoke simultaneously. Monitoring catches it at 10:15. Rollback at 10:16. Three minutes of full-org outbound outage.
This is the blast-radius story. Mitigations, in rough order of cost:
- Staged firewall rule deploys. Deploy to a “test” rule group, exercise against a synthetic workload, then promote. This is a pipeline, not a click-ops workflow.
- Two firewall endpoints per AZ, with independent rule groups so we can blue-green. Doubles the per-hour firewall cost; turns a bad deploy into a deployment-time incident rather than a runtime incident.
- A second egress VPC as a warm spare. Expensive; reserved for organisations where three minutes of outbound outage costs more than a second egress path.
- Break-glass routes. A documented procedure (and rehearsed Terraform change) to bypass the firewall and go direct through NAT when the firewall itself is broken. The bypass is a larger blast radius in a different direction, no inspection, but it restores connectivity.
Centralising means the hub is now on the business-critical path. Treat it like one.
What’s worth remembering
- Centralised egress is three jobs, not one. NAT consolidation saves money; Network Firewall adds inspection; Flow Logs + firewall logs centralise audit. You can do any of them without the others.
- Transit Gateway is the hub-and-spoke building block, not the firewall. TGW routes packets between attachments; Network Firewall inspects them. Don’t conflate the two; don’t put firewall rules on TGW route tables.
- AZ-symmetric routing through the firewall is mandatory. Traffic must enter and leave the firewall endpoint in the same AZ. Asymmetric paths drop packets; the route tables are the place this gets wrong.
- Gateway endpoints for S3 and DynamoDB bypass the hub. Always configure them per spoke; the bandwidth savings pay for themselves in days.
- SNI inspection is not payload inspection. Network Firewall reads the unencrypted SNI; that’s enough for domain allow-listing. If you need to inspect what’s inside the TLS stream, you need a forward proxy with certificate distribution (a much bigger project).
- The hub is a single point of failure you chose on purpose. Accept that a bad deploy in the hub breaks everything, and build deployment discipline that matches the blast radius: staged rollouts, pipelines, rehearsed break-glass.
- RAM shares TGW across accounts; SCPs stop spokes routing around it. Without the SCPs, a workload account can add its own IGW and a default route, and the central egress is bypassed silently.
- Watch the TGW data-processing charge. Every GB through TGW bills. Gateway endpoints for S3 and DynamoDB, and interface endpoints where per-spoke volume justifies them, keep the TGW bill in check.
One hub, one firewall, one NAT bill; forty spokes with default routes that do what the hub tells them. The centralisation pays for itself, as long as we keep in mind that the hub now holds the whole thing up.