A private network, in the cloud.
This guide covers CIDR allocation, route tables, gateways, stateful security groups vs stateless ACLs, peering, Transit Gateway and PrivateLink, using AWS terms.
What is a VPC?
A VPC (Virtual Private Cloud) is a logically isolated network in a public cloud. AWS VPC launched in 2009; GCP VPC and Azure VNet followed. A VPC has five layers (VPC CIDR, subnets, route tables, gateways, security groups/ACLs), tied together by routing. CIDR choices are hard to change later, so size them carefully.
A VPC is a software-defined network you carve out of a cloud provider's substrate. It behaves like a private datacenter network, but is built from overlay constructs (VPC, subnets, route tables, gateways, firewalls). Pick a layer to inspect.
Subnets carve a VPC by AZ.
A VPC is a private IPv4 (and optionally IPv6) range: say 10.0.0.0/16, sixty-five thousand addresses. You divide it into subnets, each pinned to one Availability Zone, each with a smaller CIDR (10.0.1.0/24 = 256 addresses). Public subnets have a route to the Internet Gateway; private subnets do not.
Plan your CIDR carefully, because you cannot resize it
A very common VPC mistake is sizing the primary CIDR too small or overlapping with on-prem. AWS allows 5 secondary CIDRs but they cannot overlap. Pick a /16 you have not used elsewhere; carve /20 per AZ; carve /24s inside that for app, db, and shared subnets.
Reserve five addresses in every subnet: AWS uses the network address, the VPC router (.1), DNS (.2), a future-use slot (.3), and the broadcast address. So a /24 has 251 usable IPs, not 256.
# A typical 3-AZ VPC layout VPC 10.0.0.0/16 65,536 addresses AZ-a public 10.0.0.0/24 251 + IGW route AZ-a private 10.0.16.0/20 4,091 + NAT route AZ-b public 10.0.1.0/24 251 + IGW route AZ-b private 10.0.32.0/20 4,091 + NAT route AZ-c public 10.0.2.0/24 251 + IGW route AZ-c private 10.0.48.0/20 4,091 + NAT route DB AZ-a/b/c 10.0.64.0/22 ... no internet route at all
Route tables: the most specific match wins
Every packet leaving an ENI consults the route table attached to its subnet. Routes are evaluated longest-prefix-first: a /32 host route beats a /24 subnet route beats a 0.0.0.0/0 default. The local route covering the entire VPC CIDR is implicit and cannot be removed.
"Public" vs "private" is purely a route table distinction. A subnet whose default route points at an Internet Gateway is public; one pointing at a NAT GW or no default at all is private. Subnets themselves have no firewall; that is what NACLs and security groups are for.
Two routes
10.0.0.0/16 → local handles intra-VPC. 0.0.0.0/0 → igw-xyz handles everything else. Instances need a public IP (or Elastic IP) for return traffic.
Two routes, different target
10.0.0.0/16 → local same. 0.0.0.0/0 → nat-xyz sends egress to the NAT in a public subnet, which SNATs to its own EIP.
Internet Gateway vs NAT Gateway
An Internet Gateway is a logical component and does not do many-to-one NAT. It performs 1:1 translation between an instance's public IP (or EIP) and its private IP. If the instance has no public IP, the IGW does nothing and the packet is dropped.
A NAT Gateway is a managed many-to-one source NAT. It SNATs all egress from many private instances to its own EIP, tracks the connection, and reverses the translation on return. Inbound-initiated connections cannot reach behind it (much like a one-way reverse proxy). AWS bills NAT GW per hour and per GB, which makes it an easy source of unexpected cost in a VPC.
Internet Gateway
No many-to-one NAT. Bidirectional. No hourly charge. Requires public IPs on instances. One per VPC.
NAT Gateway
SNAT only. Outbound-initiated. Per-AZ for HA. Charged per-hour and per-GB.
VPC endpoint
PrivateLink. Reach AWS services without IGW or NAT. Cheaper at scale; private DNS optional.
Security groups are stateful, allow-only firewalls on each ENI
A security group is a stateful firewall pinned to one or more elastic network interfaces. You can specify only allow rules, there is no deny, and the source can be a CIDR or another security group. The latter matters: "allow port 5432 from sg-app" lets you wire database access without managing IP lists. The same model shows up in Kubernetes NetworkPolicy.
Stateful means that if you allow inbound, the return path is automatic, and if you allow outbound, the inbound reply is automatic. With security groups you rarely need to think about return traffic. Default outbound is "all allowed", which most teams leave alone, although tightening it adds defense in depth.
# Web -> App -> DB three-tier, by SG reference, not CIDR sg-web in: 80, 443 from 0.0.0.0/0 sg-web out: 8080 to sg-app sg-app in: 8080 from sg-web sg-app out: 5432 to sg-db sg-db in: 5432 from sg-app sg-db out: (empty - no egress allowed)
Network ACLs are stateless, ordered filters at the subnet edge
A Network ACL is a stateless filter applied at the subnet boundary. Rules are numbered; lowest-number-first wins; both allow and deny are valid; default is deny. Stateless means: you must explicitly allow return traffic in the opposite direction, including ephemeral source ports (1024–65535).
Most teams leave NACLs at the default-allow-all and rely entirely on security groups. Use NACLs when you need to block something more broadly than per instance, for example blackholing a CIDR across a whole subnet without touching every security group.
VPC peering vs Transit Gateway: many links or one hub
VPC peering is a direct, non-transitive link between two VPCs. CIDRs must not overlap. Routing each side requires updating both route tables. With four VPCs you need six peerings; with ten you need 45. It does not scale, so it suits occasional pairings.
Transit Gateway replaces N×N peering with a single hub. Each VPC attaches once. Routing tables on the TGW decide which spokes can talk to which. It also terminates Direct Connect (which rides BGP), Site-to-Site VPN, and SD-WAN appliances. For multi-account or multi-region topologies it is usually needed.
PrivateLink is different in purpose: it exposes a single service from a producer VPC to many consumer VPCs without ever sharing CIDRs or routing. The consumers see an interface endpoint with a private IP in their own subnet; the producer sees a network load balancer. The two VPCs have no other connectivity.
VPC observability: flow logs and DNS
Enable VPC Flow Logs from day one. They record every accepted or rejected packet flow at the ENI, subnet, or VPC level, with five-tuple, action, and byte counts. Send to S3 (cheap, archival) or CloudWatch (queryable). When one service can't reach another, flow logs help show whether the cause is a route, a security group or something else.
VPC DNS is enabled by default at VPC_CIDR.0.2 (Route 53 Resolver). It resolves public AWS records and any private hosted zones associated with the VPC. Outbound resolver endpoints let you forward queries to on-prem; inbound endpoints let on-prem resolve VPC zones. This is how hybrid DNS is usually set up.
Works from the bastion, times out from Lambda
Here is a typical incident. You SSH to the bastion and curl a third-party API: 200 OK, instant. The same call from a Lambda attached to the VPC hangs for thirty seconds and times out. It is not refused and not a 4xx, just a hang. A hang means packets are leaving and nothing is coming back, or they are never leaving at all, and the investigation is about telling those apart.
First suspect: the Lambda's security group. Its egress rule is the default allow-all, so outbound 443 is fine. Security groups are stateful, so if the request gets out the reply is allowed back in, and there is no return rule to forget. That clears the security groups.
Second: the subnet's NACL. This is where stateless matters. Even with outbound 443 allowed, the API's reply arrives addressed to whatever ephemeral source port the client picked, so the NACL needs an explicit inbound allow on ports 1024–65535. Teams that hand-tighten NACLs and forget that return rule produce exactly this symptom. Here, though, the subnet uses the default NACL, which allows everything in both directions, so that is cleared too.
Third is the route table of the Lambda's subnet, and the problem is there. The Lambda's ENIs landed in a subnet someone created for internal tooling, and its route table has only the local route. There is no 0.0.0.0/0 route at all. The bastion sits in the public subnet, which has a default route to the IGW, so "works from the bastion" said nothing about the Lambda's subnet.
Pointing the subnet at the IGW would not fix it. Lambda ENIs never get public IPs, and an IGW only translates for instances that have one, so for a private ENI it drops the packet. The subnet needs 0.0.0.0/0 → nat-gw, and this VPC had no NAT gateway. The outage came from a missing piece of architecture rather than a typo.
# The order to check 1 Route table which subnet holds the ENI; where does 0.0.0.0/0 point? 2 Security grp outbound rule for the port (stateful: return is implicit) 3 NACL outbound port AND inbound ephemeral 1024-65535 (stateless) 4 Flow logs REJECT row = SG/NACL; no row at all = routing
What AWS, Azure, and GCP each call their private network
- AWS VPC
- One VPC per region. Subnets are AZ-scoped (one subnet, one AZ). Default tenancy. Launched in 2009 and the base of most AWS architectures.
- GCP VPC
- VPCs are global by default: one VPC can span all regions, with subnets scoped per region. Subnets can be expanded after creation (AWS cannot resize). This suits globally distributed architectures.
- Azure VNet
- VNet is regional. Different terminology: NSG (Network Security Group) plays the role of a security group; UDR (User Defined Route) is the route table. Subnets are not AZ-scoped (zone-redundant by default).
- Cloudflare Magic WAN
- A software-defined WAN that overlays Cloudflare's network on cloud VPCs and on-prem sites, which avoids managing cross-cloud peering directly.
- Tailscale
- A mesh VPN that creates a flat overlay across VPCs, on-prem sites and laptops, which sidesteps the peering vs Transit Gateway question for internal connectivity.
Cost. AWS bills per GB by distance: cross-region traffic costs roughly double cross-AZ, and internet egress several times more. At terabytes per day, network charges dominate many bills. Gateway VPC endpoints to S3 and DynamoDB avoid the NAT and egress charges for those services, which many teams miss.
A VPC is mostly route tables. "Public" is a routing decision, "stateful" is a security group property, and "egress only" comes from a NAT GW. Most of the rest follows from those. Common VPC problems are overlapping CIDRs nobody planned for and NAT GW bills nobody noticed.
Further reading on VPC networking
- AWS docsAmazon VPC user guideThe reference for routing, gateways, endpoints and peering.
- AWSAWS Architecture CenterReference architectures with VPC topologies you can copy.
- Semicolony guideNAT explainedHow NAT GW works: port translation, connection tracking and the connection lifecycle.
- Semicolony guideBGP: how the internet routesDirect Connect and TGW peerings use BGP.