AWS VPC Connectivity: Routes, NAT, Security Groups and the Rules That Are Stateless
Nearly every AWS connectivity problem reduces to one of four things: a missing route, a stateless NACL, a security group that does not reference what you think, or a subnet that is private when you believed it public. This guide separates them quickly.
Establish what kind of subnet you are actually in
There is no "public subnet" attribute in AWS. A subnet is public only because its route table has a default route to an internet gateway. Checking this takes seconds and resolves a surprising proportion of cases.
aws ec2 describe-route-tables \ --filters "Name=association.subnet-id,Values=subnet-0abc123" \ --query 'RouteTables[].Routes[]' --output table
| Destination | Target | Subnet is… |
|---|---|---|
| 0.0.0.0/0 | igw-… | Public |
| 0.0.0.0/0 | nat-… | Private with outbound internet |
| no 0.0.0.0/0 | — | Isolated; local VPC traffic only |
If a subnet has no explicit route-table association it inherits the VPC's main route table, which is a frequent surprise after infrastructure is created by hand:
aws ec2 describe-route-tables \ --filters "Name=vpc-id,Values=vpc-0abc" "Name=association.main,Values=true"
For an instance in a public subnet to be reachable it needs all three: the IGW route, a public IP or Elastic IP, and permissive security group rules. A public subnet with no public IP assigned is unreachable regardless of everything else.
Security groups are stateful; NACLs are not
This distinction causes more wasted hours than any other topic in VPC networking.
| Security group | Network ACL | |
|---|---|---|
| Applies to | ENI (instance) | Subnet |
| State | Stateful — return traffic automatically allowed | Stateless — return traffic needs its own rule |
| Rules | Allow only | Allow and deny |
| Evaluation | All rules, permissive union | In rule-number order, first match wins |
| Default | Deny inbound, allow outbound | Default NACL allows everything |
Because a security group is stateful, allowing inbound 443 is sufficient — the response is permitted automatically. A NACL requires both directions, and the return traffic uses an ephemeral port, not 443:
# inbound rule 100 TCP 443 0.0.0.0/0 ALLOW # outbound rule — the response leaves from an ephemeral port 100 TCP 1024-65535 0.0.0.0/0 ALLOW
Omitting the second rule produces the classic signature: the connection establishes and then hangs, or works in one direction only. Ephemeral ranges differ by client — Linux typically 32768–60999, Windows 49152–65535, and NAT gateways use 1024–65535, so allow the full range unless you have a specific reason not to.
Security group rules can reference another security group rather than a CIDR. This is the preferred pattern, but it is frequently misread: referencing a group permits traffic from instances in that group, not from the group's subnet.
aws ec2 describe-security-groups --group-ids sg-0abc \ --query 'SecurityGroups[].IpPermissions[]' --output json
Also check rule count limits. The default quota is 60 inbound and 60 outbound rules per group, and a group that has silently hit the limit will reject new rules during a deployment.
NAT gateway requirements
A NAT gateway lets private instances reach the internet outbound. Four conditions must all hold, and the second is the one most often missed:
- The NAT gateway sits in a public subnet — one with an IGW route.
- The private subnet's route table points
0.0.0.0/0at the NAT gateway. - The NAT gateway has an Elastic IP.
- The public subnet's own route table has the IGW route.
Placing the NAT gateway in the private subnet it serves is a configuration that creates a routing loop and fails with no obvious error.
aws ec2 describe-nat-gateways \
--filter "Name=vpc-id,Values=vpc-0abc" \
--query 'NatGateways[].{Id:NatGatewayId,Subnet:SubnetId,State:State}' --output table
Note that NAT gateways are zonal. A NAT gateway in eu-west-1a serving private subnets in 1b and 1c works, but the cross-AZ traffic is chargeable and becomes a single point of failure for both zones. Deploy one per availability zone for production.
# watch for port allocation errors under load aws cloudwatch get-metric-statistics --namespace AWS/NATGateway \ --metric-name ErrorPortAllocation --statistics Sum \ --start-time 2026-10-05T00:00:00Z --end-time 2026-10-06T00:00:00Z --period 3600
ErrorPortAllocation above zero means the NAT gateway exhausted ports to a single destination — typically many instances polling one endpoint. The fix is more destinations, connection reuse, or an additional NAT gateway.
Peering, transit and what is not transitive
VPC peering is explicitly non-transitive. If A peers with B and B peers with C, A cannot reach C. No route table entry will create that path; the packet is dropped at B.
aws ec2 describe-vpc-peering-connections \
--query 'VpcPeeringConnections[].{Id:VpcPeeringConnectionId,Status:Status.Code}' --output table
Both sides need routes — a peering connection in active state with routes on only one side gives you traffic out and nothing back. Overlapping CIDRs cannot be peered at all, which is the usual reason a peering request cannot be created in the first place.
For more than a handful of VPCs, Transit Gateway replaces the mesh and does support transitive routing, but each attachment needs an entry in the relevant transit gateway route table — attachment alone does not create reachability:
aws ec2 describe-transit-gateway-route-tables aws ec2 search-transit-gateway-routes \ --transit-gateway-route-table-id tgw-rtb-0abc \ --filters "Name=state,Values=active"
VPC endpoints and the policy that blocks you
Endpoints keep traffic to AWS services off the internet. The two types behave differently and fail differently:
- Gateway endpoints (S3, DynamoDB) add a prefix-list route to the route table. They cost nothing and are invisible to the instance. If the route is missing from the subnet's table, traffic silently falls back to the internet path — or fails if there is none.
- Interface endpoints (most other services) create an ENI with a private DNS name, and are governed by a security group. Forgetting to allow 443 inbound on that security group is the most common interface-endpoint failure.
aws ec2 describe-vpc-endpoints \
--query 'VpcEndpoints[].{Id:VpcEndpointId,Service:ServiceName,Type:VpcEndpointType,State:State}' \
--output table
Endpoint policies are a second, separate control. A restrictive endpoint policy will deny access even when the IAM policy permits it — both must allow the call:
aws ec2 describe-vpc-endpoints --vpc-endpoint-ids vpce-0abc \ --query 'VpcEndpoints[].PolicyDocument' --output text
For interface endpoints, private DNS must be enabled for the standard service hostname to resolve to the endpoint; without it the SDK continues to use the public endpoint.
Let AWS trace it for you
Reachability Analyzer evaluates the entire path — routes, security groups, NACLs, peering — and names the exact component that blocks it. It is faster and more reliable than manual tracing and should usually be the first step rather than the last.
aws ec2 create-network-insights-path \
--source i-0source --destination i-0dest \
--destination-port 443 --protocol tcp
aws ec2 start-network-insights-analysis \
--network-insights-path-id nip-0abc
aws ec2 describe-network-insights-analyses \
--network-insights-analysis-ids nia-0abc \
--query 'NetworkInsightsAnalyses[].{Reachable:NetworkPathFound,Explanation:Explanations}'
When the path is not reachable, the Explanations array names the component and the rule responsible.
Flow Logs show what actually happened on the wire. Filter for rejects to find the drop:
fields @timestamp, srcAddr, dstAddr, srcPort, dstPort, protocol, action, logStatus | filter action = "REJECT" | filter dstPort = 443 | sort @timestamp desc | limit 50
Two interpretation notes that matter. A REJECT recorded on the inbound ENI points at a security group or NACL on the destination. An absence of any record for the flow means the packet never arrived — look at routing, not filtering. And logStatus = SKIPDATA means entries were dropped during aggregation, so an empty result is not proof of absence.
Checklist
- Check the subnet's route table — including whether it is using the main table by default.
- Confirm public-subnet instances actually have a public or Elastic IP.
- Remember security groups are stateful; NACLs need an explicit ephemeral-port return rule.
- For NAT: gateway in a public subnet, private route to the NAT, EIP attached, IGW route present.
- Check
ErrorPortAllocationbefore blaming the application for intermittent timeouts. - Peering is not transitive, and both sides need routes.
- Interface endpoints need an open security group and private DNS; check the endpoint policy too.
- Run Reachability Analyzer early, and use Flow Log REJECT entries to confirm the layer.