
AWS VPC(Virtual Private Cloud) Explained Like a Gated Society: Gates, Guards, Roads, and a 15-Minute Lab
A plain-English but technical breakdown of AWS VPC: subnets, route tables, gateways, security groups vs NACLs, and endpoints. Includes a hands-on CLI lab, an attacker vs defender view, and a checklist of the mistakes that get accounts breached.
TL;DR: A VPC is your private slice of AWS. "Public" or "private" is decided by one route, not a checkbox. Security groups guard instances, NACLs guard subnets, endpoints keep AWS traffic off the internet, and Flow Logs tell you what happened. Read the story, run the lab, steal the checklist.
Start here: a 10-second quiz
You launch an EC2 instance in a "private" subnet. What actually makes the subnet private?
A) The instance has no public IP B) The subnet is named "private" C) Its route table has no route to an Internet Gateway D) The security group blocks everything
If you picked C, you already understand more VPC than plenty of people who've used AWS for years. (A and D help, but they don't define it. B is just a label.)
Here's what you'll get from this article:
- β A mental model you won't forget (gated housing society)
- β The journey of one request, from the internet to your server and back
- β Security Groups vs NACLs, finally clear
- β A 15-minute CLI lab with proof and cleanup
- β Attacker vs defender view, common mistakes, and a checklist
ποΈ The mental model: a gated society
Forget diagrams for a minute. A VPC is a gated housing society that only you own.
| Real world | AWS | What it does |
|---|---|---|
| The whole society | VPC | Your private network with its own IP range |
| Blocks / wings | Subnets | Slices of the society, each in one Availability Zone |
| Roads and signboards | Route tables | Decide where traffic goes next |
| Main gate to the city | Internet Gateway (IGW) | Two-way door to the internet |
| One-way exit gate | NAT Gateway | Private things go out, nothing comes in |
| Guard at each flat door | Security Group | Firewall at the instance |
| Guard at each block entrance | Network ACL (NACL) | Firewall at the subnet |
| Private tunnel to a nearby shop | VPC Endpoint | Reach AWS services without the public internet |
| CCTV | VPC Flow Logs | Records who talked to whom |
πΊοΈ The big picture

Three paths to remember:
- Inbound: Internet β IGW β Load Balancer β App β Database
- Outbound (updates, APIs): App β NAT Gateway β IGW β Internet
- AWS services: App β S3 Gateway Endpoint β S3 (never touches the internet)
π¦ Follow one request
You open a website hosted in this VPC. Here's what the packet goes through:
- DNS resolves the domain to the load balancer's public IP
- The packet hits the Internet Gateway
- The subnet's route table sends it to the load balancer inside the VPC
- The subnet's NACL inbound rules are checked
- The load balancer's Security Group is checked
- The load balancer forwards to your app. The app's Security Group must allow traffic from the load balancer's Security Group
- The response goes back. Security Groups allow it automatically (stateful), but the NACL outbound rules must allow it too, or it silently dies
Step 7 is the classic "why does it work one way only?" bug. We'll come back to it.
π§© Piece by piece
1. The VPC and its CIDR
Your VPC gets an IP range like
10.0.0.0/16 (about 65,000 addresses). Pick it before you build, because changing it later hurts.- Don't overlap with your office network, home lab, or any VPC you might peer with later. Overlapping ranges can't be connected
- A comfortable default:
/16for the VPC,/24for subnets - A
/24gives you 251 usable IPs, because AWS reserves 5 per subnet
2. Subnets: "public" is not a setting
There's no "make this public" checkbox. A subnet is public only because its route table sends
0.0.0.0/0 to an Internet Gateway. Delete that route and it's private.A subnet lives in one Availability Zone. For resilience, create one per AZ.
3. Route tables: the real boss
Every subnet follows exactly one route table. Two rules:
- The most specific route wins (
10.0.0.0/16beats0.0.0.0/0) - Every VPC has a built-in
localroute, so anything in the VPC can reach anything else in the VPC. Security Groups are what stop that
4. IGW vs NAT Gateway
| Internet Gateway | NAT Gateway | |
|---|---|---|
| Direction | Two-way | Outbound only |
| Used by | Resources with public IPs | Private instances that need updates or APIs |
| Lives in | Attached to the VPC | A public subnet |
| Cost | No hourly charge | Hourly charge plus data processing |
NAT Gateways are the classic "why is my bill high?" surprise. Never leave one running after a lab.
5. Security Groups vs NACLs
The interview question that never dies:
| Security Group | Network ACL | |
|---|---|---|
| Level | Instance / network interface | Subnet |
| State | Stateful: return traffic is automatic | Stateless: allow both directions yourself |
| Rules | Allow only | Allow and deny |
| Evaluation | All rules together | In number order, first match wins |
| Default | New SG: deny inbound, allow outbound | Default NACL allows all. A new custom NACL denies all |
Practical rule: do your real work in Security Groups. Use NACLs as a blunt backup, for example to deny a known-bad IP range for a whole subnet.
6. VPC Endpoints: keep AWS traffic off the internet
Without an endpoint, a private instance reaches S3 by going out through NAT to the public S3 endpoint. That costs money and widens your exposure.
- Gateway endpoints (S3 and DynamoDB): added as a route, no hourly charge
- Interface endpoints (PrivateLink, most other services): a network interface in your subnet, billed hourly plus data
Attach an endpoint policy to limit what can be reached through it.
ποΈ Real-world layout: the 3-tier pattern
This is how most production apps are laid out. The trick is Security Groups that point at other Security Groups, not at IP ranges.
| Tier | Subnet | Inbound allowed from |
|---|---|---|
| Load balancer | Public | 443 from 0.0.0.0/0 |
| App servers | Private | 8080 from the load balancer's SG |
| Database | Private | 5432 from the app's SG |
1
2
3
# Database SG: only the app tier can talk to Postgres
aws ec2 authorize-security-group-ingress --group-id $DB_SG \
--protocol tcp --port 5432 --source-group $APP_SG
Servers come and go and their IPs change. The rule "app tier can reach the DB" stays true.
π§ͺ Lab: build it yourself in about 15 minutes
Use a test account. Everything below is free to create: VPCs, subnets, IGWs, route tables, security groups and gateway endpoints have no hourly charge. We skip the NAT Gateway on purpose.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
export AWS_DEFAULT_REGION=ap-south-1
AZ=${AWS_DEFAULT_REGION}a
# 1. VPC
VPC=$(aws ec2 create-vpc --cidr-block 10.0.0.0/16 \
--query Vpc.VpcId --output text)
aws ec2 create-tags --resources $VPC --tags Key=Name,Value=lab-vpc
aws ec2 modify-vpc-attribute --vpc-id $VPC --enable-dns-hostnames '{"Value":true}'
# 2. Two subnets in the same AZ
PUB=$(aws ec2 create-subnet --vpc-id $VPC --cidr-block 10.0.1.0/24 \
--availability-zone $AZ --query Subnet.SubnetId --output text)
PRIV=$(aws ec2 create-subnet --vpc-id $VPC --cidr-block 10.0.2.0/24 \
--availability-zone $AZ --query Subnet.SubnetId --output text)
# 3. Internet Gateway
IGW=$(aws ec2 create-internet-gateway \
--query InternetGateway.InternetGatewayId --output text)
aws ec2 attach-internet-gateway --internet-gateway-id $IGW --vpc-id $VPC
# 4. Route table with an IGW route, attached to the PUBLIC subnet only
RT=$(aws ec2 create-route-table --vpc-id $VPC \
--query RouteTable.RouteTableId --output text)
aws ec2 create-route --route-table-id $RT \
--destination-cidr-block 0.0.0.0/0 --gateway-id $IGW
aws ec2 associate-route-table --route-table-id $RT --subnet-id $PUB
π Prove the "public vs private" claim
1
2
3
4
5
6
7
8
9
10
11
# Public subnet: expect a 0.0.0.0/0 route pointing at igw-...
aws ec2 describe-route-tables \
--filters Name=association.subnet-id,Values=$PUB \
--query 'RouteTables[].Routes[].[DestinationCidrBlock,GatewayId]' --output table
# Private subnet: no explicit route table, so it uses the MAIN one. No IGW route
MAIN=$(aws ec2 describe-route-tables \
--filters Name=vpc-id,Values=$VPC Name=association.main,Values=true \
--query 'RouteTables[0].RouteTableId' --output text)
aws ec2 describe-route-tables --route-table-ids $MAIN \
--query 'RouteTables[].Routes[].[DestinationCidrBlock,GatewayId]' --output table
Same VPC, same account. The only difference is one route. That's the whole "public subnet" secret.
π Add a tight Security Group and a private path to S3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
SG=$(aws ec2 create-security-group --group-name lab-web \
--description "lab web tier" --vpc-id $VPC \
--query GroupId --output text)
aws ec2 authorize-security-group-ingress --group-id $SG \
--protocol tcp --port 443 --cidr 0.0.0.0/0
# Gateway endpoint for S3, attached to the private side (main route table)
VPCE=$(aws ec2 create-vpc-endpoint --vpc-id $VPC \
--service-name com.amazonaws.${AWS_DEFAULT_REGION}.s3 \
--route-table-ids $MAIN \
--query VpcEndpoint.VpcEndpointId --output text)
# See the new S3 route. Endpoint routes use a prefix list, not a CIDR
aws ec2 describe-route-tables --route-table-ids $MAIN \
--query 'RouteTables[].Routes[].[DestinationCidrBlock,DestinationPrefixListId,GatewayId]' \
--output table
You should now see a
pl-... entry pointing at a vpce-... target. That's your private S3 path.π§Ή Cleanup
1
2
3
4
5
6
7
8
aws ec2 delete-vpc-endpoints --vpc-endpoint-ids $VPCE
aws ec2 delete-subnet --subnet-id $PUB
aws ec2 delete-subnet --subnet-id $PRIV
aws ec2 delete-route-table --route-table-id $RT
aws ec2 detach-internet-gateway --internet-gateway-id $IGW --vpc-id $VPC
aws ec2 delete-internet-gateway --internet-gateway-id $IGW
aws ec2 delete-security-group --group-id $SG
aws ec2 delete-vpc --vpc-id $VPC
If a delete fails, something still depends on that resource. Read the error and work backwards.
π΄ Attacker view vs π΅ Defender view
| What the attacker does | What the defender does |
|---|---|
| Scans for Security Groups open to the world on SSH (22) or RDP (3389) | Never expose admin ports to 0.0.0.0/0. Use SSM Session Manager |
| Finds an SSRF bug and queries the instance metadata service for role credentials | Enforce IMDSv2 and keep instance roles least-privilege |
| Lands on one instance and moves sideways through loose Security Groups | Tier your SGs and reference SGs instead of CIDR ranges |
| Exfiltrates data out through NAT to their own server | Restrict egress, use endpoint policies, watch Flow Logs |
| Hunts the default VPC, where subnets are public by default | Keep real workloads out of the default VPC |
For detection, turn on VPC Flow Logs and pair them with GuardDuty. Flow Logs show who talked to whom, GuardDuty flags the weird stuff.
β Mistakes that actually cause incidents
- Database in a public subnet "just for now"
0.0.0.0/0on port 22 or 3389- Running production in the default VPC
- Overlapping CIDR ranges, discovered only when you try to peer
- A NAT Gateway left running after a lab
- Using NACLs as the main firewall, then losing a day to stateless return-traffic rules
- No Flow Logs, so nobody can answer "what happened?"
β Your 8-point VPC checklist
- [ ] CIDR planned so it won't overlap with anything you'll connect
- [ ] Subnets spread across at least two AZs
- [ ] Databases and app servers in private subnets
- [ ] Only the load balancer lives in the public tier
- [ ] Security Groups reference other Security Groups
- [ ] Gateway endpoints for S3 and DynamoDB
- [ ] Flow Logs enabled
- [ ] Nothing important in the default VPC
π Cheat sheet: "I need X, so I use Y"
| I need to... | Use |
|---|---|
| Put a web app on the internet | Public subnet + IGW + load balancer |
| Let a private server download updates | NAT Gateway |
| Let a private server reach S3 or DynamoDB | Gateway endpoint |
| Let a private server reach other AWS APIs privately | Interface endpoint (PrivateLink) |
| Allow tier-to-tier traffic | Security Group referencing a Security Group |
| Block one bad IP range from a whole subnet | NACL deny rule |
| Log in to private instances | SSM Session Manager |
| See who talked to whom | VPC Flow Logs |
π¬ Your turn
- Did the route-table check make "public vs private" click? Tell me what you saw in your output
- What confused you most: route tables, SG vs NACL, or NAT vs IGW? Vote, and the winner gets a deep-dive
- Drop your own VPC gotcha in the comments. We all learn more from the thing that broke
Next in this series: VPC Peering vs Transit Gateway, and how to read VPC Flow Logs like an investigator.
Found it useful? Save it for your next AWS interview. π
Enjoyed reading this content? Let the author know!
Your likes, comments, shares, and saves help creators reach more builders.
Loading recommendations
Loading article