Chapter 15: Amazon VPC — Subnets, Route Tables, NACL, NAT Gateway and Bastion Host
15.9 Bastion host (jump server) and NAT gateway
In short: A bastion host (also called a jump server) is a small EC2 instance in a public subnet that is the only server accepting SSH from your IP.
Servers in a private subnet have no public IP – so how do we SSH into them? And how will they install packages? Two answers: a bastion host and a NAT gateway. The bastion is like the society's reception – to go inside, you pass through reception first. And the NAT gateway is the society's courier counter: people inside can send parcels out, but nobody from outside can walk straight into a flat.
Private subnet मधल्या servers ला public IP नाही – मग आपण त्यांच्यात SSH कसं करणार? आणि ते packages install कसे करणार? दोन उत्तरं: bastion host आणि NAT gateway. Society च्या reception सारखा bastion – आत जायचं तर आधी reception वरून. आणि NAT gateway म्हणजे society चा courier counter: आतले लोक बाहेर parcel पाठवू शकतात, पण बाहेरून कोणी सरळ flat मध्ये येत नाही.
Private subnet के servers के पास public IP नहीं – तो हम उनमें SSH कैसे करेंगे? और वे packages install कैसे करेंगे? दो जवाब: bastion host और NAT gateway. Society के reception जैसा bastion – अंदर जाना है तो पहले reception से होकर. और NAT gateway यानी society का courier counter: अंदर वाले लोग बाहर parcel भेज सकते हैं, पर बाहर से कोई सीधे flat में नहीं आता.
Bastion host (jump server)
A bastion host (also called a jump server) is a small EC2 instance in a public subnet that is the only server accepting SSH from your IP. From it, you SSH to the private instances using their private IPs.
bastion-sg: inbound SSH 22 from My IP only.- Private servers: inbound SSH 22 only from
bastion-sg(a security group as the source), never from0.0.0.0/0. - Keep it tiny (
t3.micro), patched, with nothing else installed, and stop it when you are not using it. - Modern alternatives with no port 22 at all: AWS Systems Manager Session Manager and EC2 Instance Connect Endpoint.
ssh -J first connects to the bastion in the public subnet on port 22, then jumps to the private server 10.0.2.10, which allows SSH only from bastion-sg. The private key stays on your laptop.
ssh -J आधी public subnet मधल्या bastion ला port 22 वर connect होतो, मग private server 10.0.2.10 वर jump करतो, जो SSH फक्त bastion-sg कडून allow करतो. Private key तुमच्या laptop वरच राहते.
ssh -J पहले public subnet के bastion से port 22 पर connect होता है, फिर private server 10.0.2.10 पर jump करता है, जो SSH सिर्फ bastion-sg से allow करता है. Private key आपके laptop पर ही रहती है.
Never copy your .pem key onto the bastion. Anyone who gets into the bastion would then hold the key to every private server. Keep the key on your laptop and use one of these:
Option 1 — ProxyJump ssh -J (recommended). Your laptop tunnels through the bastion; the key never leaves your laptop.
chmod 400 society-key.pem
ssh-add society-key.pem # load the key into your ssh-agent
ssh -J ec2-user@<BASTION_PUBLIC_IP> ec2-user@10.0.2.10
Options such as -i on the command line apply to the final host, not to the jump host. That is why the key is loaded into the agent first. If ssh-add says Could not open a connection to your authentication agent, start the agent with eval "$(ssh-agent -s)" (Linux, macOS or Git Bash). On Windows, in PowerShell as administrator, run Get-Service ssh-agent | Set-Service -StartupType Manual and then Start-Service ssh-agent.
Option 2 — agent forwarding ssh -A. You log in to the bastion first, and the bastion borrows your laptop's agent for the next hop:
ssh -A ec2-user@<BASTION_PUBLIC_IP>
# now on the bastion:
ssh ec2-user@10.0.2.10
While you are connected, an administrator (root) of the bastion could use your forwarded agent, so use -A only with a bastion you control. -J is safer.
Option 3 — ~/.ssh/config on your laptop, so that ssh app is enough:
Host bastion
HostName <BASTION_PUBLIC_IP>
User ec2-user
IdentityFile ~/keys/society-key.pem
Host app
HostName 10.0.2.10
User ec2-user
IdentityFile ~/keys/society-key.pem
ProxyJump bastion
NAT gateway
Private servers still need to go out to the internet: sudo yum install, security updates, calls to outside APIs. A NAT gateway lets instances in private subnets start connections to the internet, while the internet can never start a connection to them.
- A public NAT gateway is created in a public subnet (its subnet's route table points to the internet gateway) and needs an Elastic IP.
- The private route table gets
0.0.0.0/0→nat-.... - It is managed by AWS: it scales by itself and you never patch it (unlike a do-it-yourself NAT instance).
- Outgoing traffic from the private servers appears on the internet with the NAT gateway's Elastic IP.
- A classic (zonal) NAT gateway lives in one AZ; for high availability you create one per AZ. Since November 2025 AWS also offers a regional availability mode that does not need a public subnet. This chapter uses the zonal mode, because it shows the routing clearly.
- Cost: a NAT gateway is charged for every hour it exists (in the Available state) and for every GB of data it processes, plus the hourly charge for its public IPv4 address. It is not part of the free tier. Delete it as soon as the lab is over, and release its Elastic IP.
Create the NAT gateway
- VPC → NAT gateways → Create NAT gateway.
- Name
society-nat. If the console asks for an Availability mode, choose Zonal. - Subnet:
web-public-1a(a public subnet). Connectivity type: Public. - Elastic IP allocation ID: click Allocate Elastic IP.
- Create NAT gateway and wait until the status changes from Pending to Available (a few minutes).
- Route tables →
private-rt→ Routes → Edit routes → Add route →0.0.0.0/0, Target NAT Gateway →society-nat→ Save changes.
aws ec2 allocate-address --domain vpc
aws ec2 create-nat-gateway --subnet-id subnet-0aaa1111bbbb2222c --allocation-id eipalloc-0123456789abcdef0
aws ec2 create-route --route-table-id rtb-0ddd3333eeee4444f --destination-cidr-block 0.0.0.0/0 --nat-gateway-id nat-0123456789abcdef0
Test from a private server (through the bastion):
curl -s https://checkip.amazonaws.com # prints the NAT gateway's Elastic IP
sudo yum check-update # the package repositories answer
Ravindra Bagale's Tip
The most common mistake: creating the NAT gateway in a private subnet. Then yum times out on the private servers, because the NAT gateway itself has no path to the internet. The NAT gateway always goes in a public subnet, and the route 0.0.0.0/0 → NAT goes in the private route table. Two things, two different route tables – remember that.
Ravindra Bagale's Tip – मराठी
सगळ्यात common चूक: NAT gateway private subnet मध्ये बनवतात. मग private servers वर yum timeout होतं, कारण NAT gateway ला स्वतःला internet चा रस्ता नाही. NAT gateway नेहमी public subnet मध्ये, आणि route 0.0.0.0/0 → NAT private route table मध्ये. दोन गोष्टी, दोन वेगळे route tables – लक्षात ठेवा.
Ravindra Bagale's Tip – हिंदी
सबसे common गलती: NAT gateway private subnet में बना देते हैं. फिर private servers पर yum timeout होता है, क्योंकि NAT gateway के पास खुद internet का रास्ता नहीं है. NAT gateway हमेशा public subnet में, और route 0.0.0.0/0 → NAT private route table में. दो बातें, दो अलग route tables – याद रखो.
Ravindra Bagale's Tip
The NAT gateway is not free – you pay by the hour for as long as it runs, plus a separate charge for data. Many students forget it after the lab and the bill arrives at the end of the month. When the lab is over, delete the NAT gateway, then go to Elastic IPs and release its IP – deleting does not release the IP automatically. Don't forget at all.
Ravindra Bagale's Tip – मराठी
NAT gateway free नाही – तो जितका वेळ चालू आहे तितका तासाला खर्च, आणि data वर वेगळा खर्च. बरेच students lab नंतर विसरतात आणि महिन्याच्या शेवटी bill येतं. Lab संपला की NAT gateway delete करा, आणि Elastic IPs मध्ये जाऊन त्याचा IP release करा – delete केल्याने IP आपोआप release होत नाही. बिल्कुल विसरू नका.
Ravindra Bagale's Tip – हिंदी
NAT gateway free नहीं है – जितनी देर चालू है उतना घंटे के हिसाब से खर्च, और data पर अलग खर्च. बहुत से students lab के बाद भूल जाते हैं और महीने के आखिर में bill आता है. Lab खत्म होते ही NAT gateway delete करो, और Elastic IPs में जाकर उसका IP release करो – delete करने से IP अपने आप release नहीं होता. बिल्कुल मत भूलना.
Lab
Chala, launch a bastion in web-public-1a and a test instance without a public IP in app-private-1a. Connect with ssh -J. On the private instance, run curl -s https://checkip.amazonaws.com before and after creating the NAT gateway and adding the route: first it hangs, then it prints the NAT gateway's Elastic IP. Delete the NAT gateway and release the Elastic IP when you finish.