The problem
An instance in a private subnet has no route to the IGW and no public IP. That's great for security, but it still needs to run yum update, download packages... and you still need to SSH in to manage it.
NAT: out yes, in no
NAT translates private IPs → one public IP that many machines share. NAT only allows outbound connections started from the inside; the Internet can't open a connection to a private instance on its own.
- NAT Gateway — a managed service: the zonal type (the common one) sits in a public subnet, uses an Elastic IP, scales automatically, is highly available within one AZ, and needs no patching. The newer regional type (
--availability-mode regional) spans multiple AZs automatically and doesn't need a subnet. - NAT Instance — an EC2 instance you manage yourself: cheaper, but you must disable the Source/Destination Check, configure masquerading, and handle HA yourself.
After you create the NAT Gateway, add 0.0.0.0/0 → nat-xxxx to the private subnet's route table.
flowchart LR
Admin[You] -- SSH --> Bastion
subgraph VPC["VPC 10.0.0.0/16"]
subgraph Pub["Public subnet"]
Bastion[Bastion host]
NAT[NAT Gateway + EIP]
end
subgraph Priv["Private subnet"]
App[EC2 private 10.0.2.15]
end
end
Bastion -- SSH --> App
App -- outbound --> NAT --> IGW[Internet Gateway] --> Internet((Internet))
%% step Admin: You SSH to the bastion host, the only instance exposed for admin access.
%% step Bastion: From the bastion you jump to private instances, using ssh -J so the key stays on your machine.
%% step App: The private instance has no public IP and no route from the Internet.
%% step NAT: Its outbound traffic goes to the NAT Gateway in the public subnet, which uses an Elastic IP.
%% step IGW: The NAT Gateway sends traffic out through the Internet Gateway; replies come back the same way.
Bastion host
A bastion is a small EC2 instance in a public subnet that acts as your SSH "gateway". You SSH into the bastion, then SSH from there into the private instance.
Don't copy your .pem file onto the bastion — if the bastion is compromised, so is your key. Use ProxyJump (ssh -J) or agent forwarding (ssh -A) instead: the key stays only on your machine.
Handy commands
| Task | Command |
|---|---|
| Create a NAT GW in the public subnet | aws ec2 create-nat-gateway --subnet-id subnet-pub --allocation-id eipalloc-xxxx |
| Route private traffic to the NAT | aws ec2 create-route --route-table-id rtb-priv --destination-cidr-block 0.0.0.0/0 --nat-gateway-id nat-xxxx |
| Jump through the bastion | ssh -J ec2-user@<bastion-ip> ec2-user@<private-ip> |
| SSH with agent forwarding | ssh -A -i key.pem ec2-user@<bastion-ip> |
| Check Internet access | ping -c 3 8.8.8.8 |
Common mistakes
- Putting the NAT Gateway in a private subnet → the NAT itself has no path to the IGW.
- Pointing the private route
0.0.0.0/0at the IGW instead of the NAT — the instance has no public IP, so it still can't get out. - Copying the
.pemonto the bastion.