VPCs, subnets, and the two firewalls
A cloud VPC is the whole first half of this course rendered as configuration. Address ranges, subnets, routing tables, gateways, NAT, packet filtering — every concept already covered, except that instead of cables and boxes you write CIDR blocks in a console and the provider's network fabric enforces them.
That's genuinely good news: there's very little new theory here. What there is, though, includes one distinction that catches nearly everyone exactly once, and it's the most reliably asked cloud networking interview question there is.
Examples below use AWS terminology, because it's the most common; Azure and Google Cloud have the same model under different names, noted where they differ meaningfully.
The address plan is the part you can't change later
A VPC is a private address range you choose:
VPC: 10.20.0.0/16 65,536 addresses
├── 10.20.0.0/20 public subnet, availability zone A (4,096)
├── 10.20.16.0/20 public subnet, availability zone B (4,096)
├── 10.20.32.0/20 private subnet, availability zone A (4,096)
└── 10.20.48.0/20 private subnet, availability zone B (4,096)
Two things about this shape are deliberate.
Subnets are per availability zone. A subnet doesn't span zones, so any workload that needs to survive a zone failure needs a subnet in each — which is why the pattern is always an even number of subnets, in pairs.
"Public" and "private" describe routing, not a setting. A public subnet is one whose route table sends 0.0.0.0/0 to an internet gateway. A private subnet's default route points at a NAT gateway instead, so instances can reach out but nothing can reach in. That's the same source NAT asymmetry as a home router, sold as a managed service.
And the constraint that makes address planning matter: you cannot renumber a VPC. Changing 10.20.0.0/16 later means building a new VPC and migrating everything. Overlapping ranges are the specific way this hurts — two VPCs that both use 10.0.0.0/16 cannot be peered, ever, because a router receiving a packet for 10.0.0.5 has no way to know which side is meant. Pick non-overlapping ranges across every environment and region from the start, and leave room; a /16 per VPC costs nothing and saves a migration.
The usable address count is lower than the arithmetic
A /24 has 256 addresses and, by the rule from the CIDR article, 254 usable. In a cloud subnet it's fewer: AWS reserves five addresses in every subnet — the network address, the VPC router, a DNS address, one reserved for future use, and the broadcast address — leaving 251. Azure also reserves five; Google Cloud reserves four.
Size subnets against the provider's real number, and never against the exact host count you need today. A subnet that's exactly full is one autoscaling event away from an outage that presents as "instances fail to launch" with no obvious network symptom.
Security groups and network ACLs are not two names for the same thing
Both filter traffic. They behave differently in a way that produces genuinely confusing outages.
| Security group | Network ACL | |
|---|---|---|
| Attached to | The instance's network interface | The whole subnet |
| Stateful? | Yes | No |
| Rule types | Allow only | Allow and deny |
| Evaluation | All rules considered; any match allows | In number order; first match wins |
| Default | Denies all inbound, allows all outbound | Allows everything, both directions |
The row that matters is stateful.
A security group tracks connections. Allow inbound TCP 443 and the replies flow back out automatically, regardless of what the outbound rules say — it's the same connection tracking as ct state established,related accept in nftables, applied for you.
A network ACL tracks nothing. Each packet is judged alone, so allowing inbound 443 permits requests to arrive and nothing else. The server's replies leave from port 443 to the client's ephemeral port — some number between 32768 and 60999 on Linux, or 49152–65535 by IANA's range — and unless an outbound rule permits that range, every reply is dropped.
The symptom is unmistakable once you've seen it: the TCP handshake half-completes, the client hangs, and eventually times out. On the server, tcpdump shows the SYN arriving and the SYN-ACK being sent. On the client, nothing comes back. Nothing is broken on either machine.
Security group: allow the port, replies handle themselves. Network ACL: allow the port and the ephemeral range back, or nothing works.
Which is why the practical advice is short: do your filtering in security groups, and leave network ACLs at their default unless you specifically need a subnet-wide deny — blocking a hostile address range, or enforcing a compliance boundary that must apply regardless of any instance's own configuration. Security groups can't express "deny" at all, so that's the one thing ACLs are genuinely for.
Security groups can reference each other
This is the feature that makes cloud filtering better than IP-based rules, and it's underused:
sg-database inbound: TCP 5432 from sg-application
sg-application inbound: TCP 8080 from sg-loadbalancer
sg-loadbalancer inbound: TCP 443 from 0.0.0.0/0
The database rule doesn't name an address range. It names a group, so any instance in sg-application can connect and nothing else can — automatically, as instances are created and destroyed, with no rule ever being updated. Autoscaling doesn't require touching the firewall.
Compare that to the CIDR-based equivalent, allow 5432 from 10.20.32.0/20, which permits every instance in that subnet, including ones that have nothing to do with the application. Group references are least privilege; subnet ranges are approximate.
Where the traffic actually goes
Reading a route table answers most "why can't this instance reach the internet" questions:
A private subnet: local traffic stays inside, everything else goes to the NAT gateway. Longest-prefix match applies exactly as it did in the routing article — local is more specific for in-VPC destinations, so it wins.
The public equivalent points 0.0.0.0/0 at an internet gateway. Two things must both be true for an instance there to be reachable from outside: that route, and a public IP address on the instance. An instance in a public subnet with no public IP is not reachable, and the route table looks perfectly correct.
Two more pieces complete the picture:
VPC endpoints keep traffic to provider services off the internet entirely. Without one, an instance in a private subnet reaching object storage goes out through the NAT gateway — which works, costs per gigabyte, and means the traffic transits the public internet. An endpoint adds a route so it stays inside the provider's network. This is a cost and security improvement that's frequently missed until someone reads a NAT gateway bill.
Peering is not transitive. VPC A peered with B, and B peered with C, does not let A reach C. Every pair needs its own peering connection and its own routes — which is why large environments move to a transit gateway (Azure: virtual WAN; GCP: network connectivity center) instead of a mesh of peerings that grows quadratically.
Investigation: the instance nobody can reach
A new instance in a public subnet won't accept SSH. Work down the list, cheapest check first:
- Does it have a public IP?
aws ec2 describe-instancesshowsPublicIpAddress. An empty value ends the investigation immediately — a public subnet doesn't assign one unless the subnet or launch configuration says to. - Does the route table send
0.0.0.0/0to an internet gateway? If it points at a NAT gateway, this isn't a public subnet no matter what it's named. Names are labels; routes are behaviour. - Does the security group allow inbound TCP 22 from your address? The default group allows nothing inbound.
- Does the network ACL allow inbound 22 and outbound ephemeral ports? If SSH connects and then hangs immediately, this is almost certainly the cause.
- Is the instance's own firewall allowing it, and is
sshdrunning? Cloud filtering is in addition to the host's, and the host's rules apply after.
Notice the shape of the failures. Steps 1–3 produce a connection that times out with nothing on the wire at the instance. Step 4 produces a connection that half-establishes and stalls. Step 5 produces a refusal or a timeout depending on the host's policy — which is exactly the refused-versus-dropped distinction from the troubleshooting module, now with five possible layers instead of one.
Both major providers offer a reachability analyser that simulates a path through the VPC and names the specific rule that blocks it. It's faster than reasoning through five layers by hand, and it's worth knowing exists before you need it at three in the morning.
Practice
- Draw an address plan for three environments — dev, staging, production — each needing two availability zones and public and private subnets, with no overlap anywhere and room to add a fourth environment later. State the usable host count per subnet using the provider's reserved-address rule, not plain arithmetic.
- In a free-tier account, launch an instance in a public subnet, confirm SSH works, then remove the outbound ephemeral port range from the subnet's network ACL. Predict what happens to a new SSH connection before testing, then test it.
- Build the three-tier security group chain above using group references rather than CIDR blocks. Then add a fourth instance to the application group and confirm it reaches the database without any rule being edited.
- Compare the route tables of a public and a private subnet, and explain in one sentence each what would happen to an instance moved between them.
- Explain, without looking back, why a network ACL needs two rules to permit one inbound connection while a security group needs one.
Exercise 5 is the question. If you can answer it in terms of connection tracking and ephemeral ports rather than by reciting "one is stateful," you understand both the cloud model and the packet-level mechanism underneath it — and that's the same understanding that made ct state established,related accept the first line of the nftables ruleset several articles ago. The vocabulary changed; the mechanism never did.
Sources
- AWS, Security groups
- AWS, Network ACLs
- AWS, Subnet CIDR blocks — the five reserved addresses per subnet.
- AWS, How VPC peering connections work — including the non-transitive property.