Cloud Tech
DevOpsIntermediate

Cloud Networking Fundamentals

How VPCs, subnets, and security groups model network isolation, and why public versus private subnet is a routing decision, not a label.

Reviewed Jul 22, 2026Victor Nwoke4 min read

Written and maintained by Victor Nwoke. Technical behavior is reviewed against the primary references listed on this page.

Overview

A virtual private cloud (VPC) is a logically isolated network within a cloud provider, and everything about how traffic flows in, out, and around it is governed by explicit configuration: subnets divide the VPC's address space, route tables decide where traffic destined outside a subnet goes, and security groups/network ACLs filter what's allowed at the resource and subnet level. The recurring point of confusion is that "public" and "private" aren't inherent properties of a subnet; they're just descriptions of what its route table currently does, which means auditing actual routes (not subnet names) is the only reliable way to know what's actually internet-reachable. The same non-transitive routing trap shows up in peering itself, not just public/private subnet labeling, see VNet Peering Is Not Transitive (written for Azure VNets, but VPC peering in AWS has the identical limitation), and The AWS Bill Spiked and Nobody Had Launched Anything New for what a NAT gateway route actually costs in practice.

Quick Reference

ConceptPurposeScope
VPCLogically isolated virtual networkAccount/region-wide container
SubnetAddress-space subdivision of a VPCAvailability zone
Route tableDetermines where traffic destined outside a subnet goesPer subnet
Security groupStateful, per-resource allow-list firewallAttached resources
Network ACLStateless, subnet-wide allow/deny rulesPer subnet

Syntax

hcl
resource "aws_subnet" "private" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.2.0/24"
}

resource "aws_route" "private_nat" {
  route_table_id         = aws_route_table.private.id
  destination_cidr_block = "0.0.0.0/0"
  nat_gateway_id          = aws_nat_gateway.main.id
}

Examples

hcl
# Security group - stateful, per-resource. Allowing inbound 443
# automatically permits the matching outbound response traffic.
resource "aws_security_group_rule" "https_in" {
  type              = "ingress"
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.web.id
}

Verifying a subnet's actual route beats trusting its name; this pulls the live route table rather than assuming from the console UI:

bash
aws ec2 describe-route-tables \
  --filters "Name=association.subnet-id,Values=subnet-0123456789abcdef0" \
  --query 'RouteTables[*].Routes[*].[DestinationCidrBlock,GatewayId,NatGatewayId]'

An igw-* GatewayId on the 0.0.0.0/0 route means public, regardless of what the subnet is named; a nat-* NatGatewayId means private-with-outbound; neither present means no internet route at all.

Trust route tables, not subnet names

A subnet named "private-subnet" with a route to an internet gateway is still public, regardless of its name. Always verify actual route table entries when auditing what's internet-reachable, naming conventions are not enforcement.

Visual Diagram

Common Mistakes

  • Trusting a subnet's name ("private-subnet-1") instead of checking its actual route table; the two can silently disagree after a configuration change.
  • Placing a resource that only needs outbound internet access directly in a public subnet with a public IP, exposing it to inbound traffic it never needed to accept.
  • Over-relying on security groups alone and never using network ACLs as a defense-in-depth layer, or conversely, fighting stateless NACL rules without understanding why return traffic needs its own explicit allow.
  • Forgetting that a security group allowing 0.0.0.0/0 inbound on a sensitive port is a standing exposure, not a temporary convenience, if left in place after testing.
  • Assuming VPC peering is transitive: if VPC A peers with VPC B, and B peers with VPC C, resources in A still cannot reach C through B. Every pair that needs to communicate needs its own direct peering connection (or a transit gateway/hub-spoke design instead of a peering mesh), the exact same limitation that trips people up in Azure VNet peering.

Performance

  • NAT gateways have their own throughput limits and per-GB data processing cost, very high-outbound-traffic workloads behind a single NAT gateway can hit real bottlenecks and cost, which is why multi-AZ NAT gateway placement matters at scale.
  • VPC peering and transit gateway routing add hops between networks, designing the topology (hub-and-spoke vs. full mesh) affects both latency and the blast radius of a routing misconfiguration.
  • Security group rule evaluation is effectively free at request time; it's a configuration correctness concern, not a runtime performance one.

Best Practices

  • Audit actual route tables, not subnet naming conventions, when determining what's genuinely internet-reachable.
  • Put anything that doesn't need to accept inbound internet traffic in a private subnet behind a NAT gateway for outbound access only.
  • I use security groups as the primary per-resource control and network ACLs as a coarser, subnet-wide defense-in-depth layer, not the other way around.
  • Design VPC topology (peering, transit gateway) deliberately for the traffic patterns you actually have, rather than defaulting to a full mesh that's harder to audit as it grows.
  • I verify routes with the provider CLI/API (describe-route-tables or equivalent) as part of any network audit, not just the console UI, so the check is scriptable and repeatable.
  • Route AWS-service traffic (S3, DynamoDB) through a VPC gateway endpoint instead of a NAT gateway; it costs nothing extra and keeps that traffic off the NAT gateway's throughput limit and per-GB bill entirely.

Interview questions

What makes a subnet "public" versus "private" in a cloud VPC?
It is entirely determined by routing, not by any label or flag on the subnet itself. A subnet is "public" if its route table sends traffic destined for the internet (0.0.0.0/0) to an internet gateway. A subnet is "private" if that route instead points to a NAT gateway (for outbound-only internet access) or has no internet route at all. Two subnets can be configured identically in every other respect and differ only in that one route table entry, which is why auditing actual route tables matters more than trusting subnet names like "public-subnet-1."
What is the difference between a security group and a network ACL?
A security group is stateful and attached to individual resources (like an instance or load balancer), if you allow inbound traffic on a port, the corresponding outbound response is automatically allowed, and rules are evaluated as an allow-list only. A network ACL is stateless and attached to a subnet, evaluating both inbound and outbound rules independently for every packet, including explicit deny rules. Security groups are the primary, more commonly used tool for per-resource access control; network ACLs add a coarser, subnet-wide layer, often left at their permissive default and used mainly for defense-in-depth or to explicitly block something.
Why would you use a NAT gateway instead of just putting a resource in a public subnet?
A NAT gateway lets resources in a private subnet initiate outbound connections to the internet (to pull a package, call an external API) while remaining unreachable from the internet for inbound connections; the NAT gateway only translates and forwards traffic the private resource itself initiated. Putting a resource directly in a public subnet with a public IP makes it directly reachable from the internet in both directions, which is unnecessary exposure for anything that only needs outbound access, like an application server that doesn't need to accept direct public traffic.

References

ShareXLinkedInReddit
Was this page helpful?
Suggest an improvement