VPC Networking, Explained for People Who Hate Networking
Subnets, gateways, route tables, security groups — the AWS networking basics I wish someone had drawn on a whiteboard for me the first time.
The first VPC I ever built, I put everything in one public subnet because "it works." A security reviewer later asked me, very politely, why my database had a public IP. That conversation is the reason this post exists.
VPC networking looks intimidating in the AWS console — a wall of CIDR blocks, gateways, and endpoint buttons — but the whole thing comes down to four questions. Where does traffic start? Where is it allowed to go? What stands in the middle? And who says yes or no?
Your private corner of AWS
A VPC is just a logically isolated network with an IP range you pick, like 10.0.0.0/16. That range gives you roughly 65,000 addresses, and you carve it into subnets. Each subnet lives in exactly one Availability Zone and gets a smaller slice, like 10.0.1.0/24.
The public versus private distinction is not about magic — it's about route tables. A subnet becomes public the moment its route table has a route to an Internet Gateway (0.0.0.0/0 -> igw-xxxx). No IGW route, no direct internet. That's the entire trick, and it took me an embarrassingly long time to internalize it.
The two gateways you'll actually use
Internet Gateway lets traffic flow between your VPC and the internet. Instances in public subnets use it directly.
NAT Gateway lets instances in private subnets reach the internet (for patches, package installs) without ever accepting inbound connections from it. It lives in a public subnet and forwards traffic for the private ones.
My rule of thumb: web servers in public subnets, databases and workers in private subnets, private subnets reaching out through NAT — never the other way around.
Internet
|
IGW
|
[public subnet] <- web servers, NAT gateway
|
[private subnet] <- databases, workers, caches
Route tables are the real map
Every subnet has exactly one route table. AWS creates a main route table by default and never attaches it to the internet — you create explicit associations. The routes decide everything:
| Destination | Target | Means |
|---|---|---|
| 10.0.0.0/16 | local | VPC-internal traffic, always there |
| 0.0.0.0/0 | igw-abc | this subnet is public |
| 0.0.0.0/0 | nat-xyz | private subnet can reach out |
If you remember nothing else: packets follow routes, not vibes.
Security groups versus NACLs
I still get asked this in interviews. Security groups are stateful, work at the instance level, and only allow — there is no deny rule. If you allow inbound port 443, the return traffic flows back automatically.
Network ACLs are stateless, work at the subnet level, and have both allow and deny rules. Because they're stateless, you must explicitly allow the ephemeral return ports, which is why most people (including me) keep NACLs at their defaults and lean on security groups.
A security group can reference another security group — so the database SG allows inbound 5432 from the web SG, not from 0.0.0.0/0. That single pattern solves 80% of "my RDS won't connect" tickets.
What I'd do differently next time
- Design the CIDR range once, with room to grow. Renumbering a live VPC is miserable.
- Put databases in private subnets from day one, even in a side project.
- Use VPC endpoints for S3 and DynamoDB so your private instances don't need NAT just to talk to AWS APIs — NAT data processing is where surprise bills hide.
- Write the architecture down. Future-you will not remember which route table belongs to which subnet.
Networking stops being scary the moment you realize it's just four boxes and arrows. Draw the arrows first, fill in the services second.