Skip to content

VPC Networking

Get any of these mechanics wrong and the failure is either immediate — nothing in a "private" subnet can reach the internet, or worse, something in a "public" one shouldn't be reachable at all — or slow-motion: a CIDR block sized too small forces a network redesign months later, once every subnet is full. Both trace back to the same handful of rules: how a CIDR block gets carved up, what a route table actually evaluates, and which of two firewalls is doing the blocking.

Scope: the AWS module page introduced public/private subnets by route-table association. This page goes one layer deeper: how CIDR blocks are actually sized and carved into subnets, what a route table evaluation really does, the IGW-vs-NAT and Security-Group-vs-NACL mechanics in full, and how you connect more than one VPC together.

TL;DR — in 30 seconds:

  • A subnet is public or private purely by its route table — a route to an IGW makes it public; only a NAT Gateway route makes it private (outbound-only).
  • Security Groups (stateful, instance-level, allow-only) and NACLs (stateless, subnet-level, allow and deny) are defense in depth — not two ways to do the same job.
  • VPC Peering is 1-to-1 and not transitive; once you're connecting more than a couple of VPCs, reach for a Transit Gateway hub instead.

1. CIDR blocks and subnets

A VPC starts life as a single CIDR block — a private IP range you choose (e.g. 10.0.0.0/16), sized by how many addresses you'll ever need across the whole account, not just today. A subnet is always a smaller slice of that block, and always lives entirely inside one Availability Zone — there is no such thing as a subnet spanning two AZs.

  • The prefix length (the /16, /24, …) sets how many addresses a range holds — a smaller number means a bigger range. A /16 VPC carved into /24 subnets gives 256 subnets of 256 addresses each.
  • AWS reserves 5 addresses per subnet (network address, VPC router, DNS, future use, broadcast) — a /24 subnet has 256 addresses on paper but 251 usable.
  • The standard shape spreads subnets across at least two, usually three, AZs — one public and one private subnet per AZ — so a single AZ failure doesn't take down every tier of the application.

2. Route tables — the mechanism, not just the label

"Public" and "private" aren't properties you set on a subnet directly — they're a consequence of which route table the subnet is associated with, and what that table contains.

  • Every VPC has one main route table, used automatically by any subnet not explicitly associated with another. Best practice is to leave the main table minimal and give each subnet tier its own custom route table, so a mistake in one tier's routing can't silently apply to another.
  • A route table is just a list of (destination CIDR → target) pairs, evaluated most-specific match wins — a /32 route beats a /24 route beats the default 0.0.0.0/0 route, regardless of the order the rows appear in.
  • Every table carries an implicit local route for the VPC's own CIDR, so resources in the same VPC can always reach each other — that route can't be removed.

3. IGW vs NAT Gateway

Both attach to the VPC and both sit on a route table's 0.0.0.0/0 row — the difference is direction and who they serve.

Internet Gateway (IGW) NAT Gateway
Direction inbound + outbound outbound only
Lives in attached to the VPC itself (one per VPC) a specific public subnet
Used by public subnets, via their route table private subnets, via their route table pointing at the NAT Gateway's ENI
Who it's for resources that need a public IP and must accept inbound connections private-subnet resources that need outbound-only internet (pull an image, call an API)

A NAT Gateway is a managed, AZ-scoped resource — one per AZ is the standard pattern, so a private subnet's outbound traffic never has to cross an AZ boundary to reach it (avoiding both the latency and the inter-AZ data-transfer cost). The older NAT instance (a self-managed EC2 box running as a NAT) is still possible but loses on availability and throughput — the managed NAT Gateway is the default reach-for. (Go deeper: the official NAT gateways guide — optional, not required to defend this page.)

flowchart LR
    Internet(("Internet"))
    IGW["Internet Gateway"]
    subgraph AZa["Availability Zone A"]
      direction TB
      PubA["Public subnet<br/>RT: 0.0.0.0/0 → IGW"]
      NatA["NAT Gateway"]
      PrivA["Private subnet<br/>RT: 0.0.0.0/0 → NAT Gateway"]
      PubA --- NatA
    end
    subgraph AZb["Availability Zone B"]
      direction TB
      PubB["Public subnet<br/>RT: 0.0.0.0/0 → IGW"]
      NatB["NAT Gateway"]
      PrivB["Private subnet<br/>RT: 0.0.0.0/0 → NAT Gateway"]
      PubB --- NatB
    end
    Internet <--> IGW
    IGW --- PubA
    IGW --- PubB
    NatA -->|"outbound only"| IGW
    NatB -->|"outbound only"| IGW
    PrivA -.->|"no inbound path exists"| Internet

Each AZ gets its own NAT Gateway so a private subnet's traffic stays inside its own AZ — the diagram's two independent public/NAT/private stacks, not one shared NAT for the whole VPC.

4. Security Groups vs NACLs

Two firewalls guard the same traffic at different layers — this is defense in depth, not two ways of doing the same job.

Security Group NACL
Scope attached to an ENI (instance-level) attached to a subnet
State stateful — a reply to an allowed request is automatically allowed back stateless — return traffic needs its own explicit rule
Rule type allow only — everything not explicitly allowed is denied allow and deny — can explicitly block a specific source
Evaluation all matching rules combine (there's no ordering) rules are numbered and evaluated in order, first match wins
Applies to traffic to/from the instance all traffic crossing the subnet boundary, for every instance in it

Because a Security Group is stateful and allow-only, it's usually enough on its own. A NACL earns its keep for the case a Security Group can't cover: an explicit deny — blocking one bad IP range at the subnet boundary, in front of everything in it, without having to touch every instance's Security Group.

5. Connecting VPCs — peering vs Transit Gateway

  • VPC Peering is a direct, 1-to-1 connection between two VPCs, and it is not transitive — if A peers with B, and B peers with C, A still cannot reach C through B. Each pair needs its own peering connection, plus a route in each VPC's route table pointing at the peering connection.
  • A Transit Gateway is a regional routing hub: every VPC attaches to it once, and the Transit Gateway routes between all of them. Past a handful of VPCs, peering's pairwise connection count grows combinatorially while Transit Gateway stays at one attachment per VPC — the point where teams switch.
flowchart TB
    subgraph Peering["VPC Peering — pairwise, not transitive"]
      A1["VPC A"] <--> B1["VPC B"]
      B1 <--> C1["VPC C"]
      A1 -.->|"no path"| C1
    end
    subgraph TGW["Transit Gateway — hub and spoke"]
      A2["VPC A"] --> T["Transit Gateway"]
      B2["VPC B"] --> T
      C2["VPC C"] --> T
      T --> A2
      T --> B2
      T --> C2
    end

6. How this connects

  • This extends the AWS module page's networking section — same public/private mechanism, now with the CIDR sizing, route-table evaluation order, and NAT-per-AZ detail behind it.
  • The Security Group vs NACL split mirrors the same defense-in-depth idea the Kubernetes module applies at the Pod level with RBAC, SecurityContext, and NetworkPolicy — different layer, same principle: no single control is meant to be the only one that has to hold.

Common gotchas

  • Assuming a subnet is "public" because it looks public-facing. Fix: check the route table — only an explicit route to an IGW makes a subnet public; nothing else does, regardless of what's deployed in it.
  • Sizing a VPC CIDR block for today's needs. Fix: size for the whole account's lifetime — running out of address space later forces a network redesign, not a quick fix, since a VPC's CIDR can't be shrunk and only has limited room to grow.
  • Sharing one NAT Gateway across every AZ to save cost. Fix: give each AZ its own — otherwise a private subnet's outbound traffic crosses an AZ boundary (extra latency and inter-AZ data-transfer cost), and losing that single NAT Gateway takes down every AZ's outbound path at once.
  • Relying on a Security Group alone and forgetting a NACL is stateless. Fix: a NACL needs an explicit rule for return traffic too — an easy way to silently break connectivity that a Security Group alone would have allowed automatically.
  • Expecting VPC Peering to be transitive. Fix: A-peers-B and B-peers-C does not give A a path to C — each pair needs its own peering connection and route, or you need a Transit Gateway.
Check yourself — your team has grown to 8 VPCs that all need to talk to each other. Peering or Transit Gateway, and why?

Transit Gateway — pairwise peering for 8 VPCs would need up to 28 individual connections (and VPC Peering isn't transitive anyway), while Transit Gateway needs just one attachment per VPC and routes between all of them from a single hub.

Check yourself — a private-subnet instance can reach the internet to pull a package, but nothing on the internet can reach it directly. What's providing that asymmetry, and which route in its route table?

A NAT Gateway — the private subnet's route table has a 0.0.0.0/0 route pointing at the NAT Gateway (not an Internet Gateway), which is outbound-only by design. There's no route back in.

You can defend this when you can say why a subnet is public or private by pointing at its route table (not "it feels public"), explain why each AZ gets its own NAT Gateway instead of one shared NAT for the VPC, state the concrete difference between a Security Group and a NACL (stateful/allow-only vs stateless/allow-and-deny, instance vs subnet), and explain why VPC Peering doesn't scale past a few VPCs the way a Transit Gateway does.