What is a Bastion Host?
A Bastion Host (also called a Jump Server or Jump Box) is a special-purpose server that acts as a secure gateway between an external network (like the internet) and a private network. Think of it as a heavily guarded front door to your castle—everyone who wants to enter must pass through this single, monitored entry point.
Why Use a Bastion Host?
Imagine you have critical application servers, databases, and backend services running in AWS. You don't want these directly exposed to the internet because:
- Reduced Attack Surface: Only one server (the bastion) is exposed to the internet, not your entire infrastructure.
- Centralized Access Control: All SSH access flows through a single point, making it easier to monitor and audit.
- Enhanced Security: Private instances have no public IP addresses, making them invisible to the outside world.
- Compliance: Many security standards (PCI-DSS, HIPAA, SOC2) require this level of network segregation.
The Architecture We'll Build
┌─────────────────────────────────────────────────────────────────┐
│ AWS CLOUD │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ VPC (10.0.0.0/16) │ │
│ │ │ │
│ │ ┌─────────────────┐ ┌─────────────────────────┐ │ │
│ │ │ Public Subnet │ │ Private Subnet │ │ │
│ │ │ (10.0.1.0/24) │ │ (10.0.3.0/24) │ │ │
│ │ │ │ │ │ │ │
│ │ │ ┌───────────┐ │ │ ┌───────────────┐ │ │ │
│ │ │ │ Bastion │ │ SSH │ │ Private │ │ │ │
│ │ │ │ Host │──────────────▶│ Instance │ │ │ │
│ │ │ │ (Public IP)│ │ │ │ (No Public IP)│ │ │ │
│ │ │ └───────────┘ │ │ └───────────────┘ │ │ │
│ │ │ ▲ │ │ │ │ │
│ │ └────────│────────┘ └─────────────────────────┘ │ │
│ │ │ │ │
│ │ ┌────────┴────────┐ │ │
│ │ │ Internet Gateway│ │ │
│ │ └────────┬────────┘ │ │
│ └────────────│──────────────────────────────────────────────┘ │
│ │ │
└───────────────│─────────────────────────────────────────────────┘
│
┌──────┴──────┐
│ INTERNET │
│ (You) │
└─────────────┘
Prerequisites
Before we begin, make sure you have:
- An AWS account with appropriate permissions.
- Basic understanding of networking concepts (IP addresses, subnets, routing).
- SSH client installed on your local machine (If you have MacBook then not needed since it Comes with inbuilt SSH Client).
- AWS Console access.
Step 1: Create a Virtual Private Cloud (VPC)
A VPC (Virtual Private Cloud) is your own isolated section of the AWS cloud where you can launch resources in a virtual network that you define.
Navigate to VPC Dashboard
- Log into the AWS Management Console
- Search for "VPC" in the search bar
- Click on VPC Dashboard
Create the VPC
- Click Create VPC
- Select VPC only (we'll create subnets manually for learning purposes)
- Configure the following:
| Setting | Value |
|---|---|
| Name tag | my-bastion-vpc |
| IPv4 CIDR block | 10.0.0.0/16 |
| IPv6 CIDR block | No IPv6 CIDR block |
| Tenancy | Default |
- Click Create VPC
Understanding the CIDR Block
The CIDR block 10.0.0.0/16 means:
- 10.0.0.0 is the base IP address
- /16 means the first 16 bits are fixed (total 32 bits), giving us total of (32-16) = 16 bits or 2 octets (1 octet = 8 bits). So, total 2^16 = 65,536 available IP addresses (10.0.0.0 to 10.0.255.255)
This large range allows us to create multiple subnets within our VPC.
Step 2: Create and Attach an Internet Gateway
An Internet Gateway (IGW) is a horizontally scaled, redundant, and highly available VPC component that allows communication between your VPC and the internet.
Create the Internet Gateway
- In the VPC Dashboard, click Internet Gateways in the left sidebar
- Click Create internet gateway
- Configure:
| Setting | Value |
|---|---|
| Name tag | my-bastion-igw |
- Click Create internet gateway
Attach to VPC
After creation, the IGW is in a "Detached" state. We need to attach it to our VPC:
- Select the newly created internet gateway
- Click Actions → Attach to VPC
- Select
my-bastion-vpc - Click Attach internet gateway
The status should now show "Attached".
💡 Key Concept: Without an Internet Gateway, nothing in your VPC can communicate with the internet, regardless of other configurations.
Step 3: Create Public and Private Subnets
Subnets are segments of your VPC's IP address range where you can place groups of isolated resources. We'll create two subnets:
- Public Subnet: For resources that need direct internet access (our bastion host)
- Private Subnet: For resources that should NOT be directly accessible from the internet
Create the Public Subnet
- In the VPC Dashboard, click Subnets in the left sidebar
- Click Create subnet
- Configure:
| Setting | Value |
|---|---|
| VPC ID | my-bastion-vpc |
| Subnet name | public-subnet |
| Availability Zone | Choose any (e.g., us-east-1a) |
| IPv4 CIDR block | 10.0.1.0/24 |
- Click Create subnet
Create the Private Subnet
- Click Create subnet again
- Configure:
| Setting | Value |
|---|---|
| VPC ID | my-bastion-vpc |
| Subnet name | private-subnet |
| Availability Zone | Same as public subnet (e.g., us-east-1a) |
| IPv4 CIDR block | 10.0.3.0/24 |
- Click Create subnet
Understanding the Subnet CIDR Blocks
- 10.0.1.0/24: Provides 256 IP addresses (10.0.1.0 to 10.0.1.255)
- 10.0.3.0/24: Provides 256 IP addresses (10.0.3.0 to 10.0.3.255)
Both are subsets of our VPC's 10.0.0.0/16 range.
💡 Pro Tip: Keep public and private subnets in the same Availability Zone to minimize latency and data transfer costs between your bastion host and private instances.
Step 4: Configure Route Tables
A Route Table contains a set of rules (routes) that determine where network traffic is directed. This is where we define what makes a subnet "public" or "private".
Create the Public Route Table
- In the VPC Dashboard, click Route Tables in the left sidebar
- Click Create route table
- Configure:
| Setting | Value |
|---|---|
| Name | public-route-table |
| VPC | my-bastion-vpc |
- Click Create route table
Add Internet Route to Public Route Table
- Select
public-route-table - Click the Routes tab
- Click Edit routes
- Click Add route
- Configure:
| Destination | Target |
|---|---|
0.0.0.0/0 |
Select "Internet Gateway" → my-bastion-igw
|
- Click Save changes
The route 0.0.0.0/0 means "all traffic not destined for the local VPC should go to the Internet Gateway".
Associate Public Subnet with Public Route Table
- With
public-route-tablestill selected, click the Subnet associations tab - Click Edit subnet associations
- Check the box next to
public-subnet(Select the Subnet that you created which is supposed to be accessible to the internet) - Click Save associations
Create the Private Route Table
- Click Create route table
- Configure:
| Setting | Value |
|---|---|
| Name | private-route-table |
| VPC | my-bastion-vpc |
- Click Create route table
Associate Private Subnet with Private Route Table
- Select
private-route-table - Click the Subnet associations tab
- Click Edit subnet associations
- Check the box next to
private-subnet - Click Save associations
🔒 Security Note: Notice we did NOT add an internet gateway route to the private route table. This is intentional—the private subnet has no direct path to the internet.
Step 5: Create Security Groups
Security Groups act as virtual firewalls for your EC2 instances, controlling inbound and outbound traffic at the instance level.
Create Security Group for Bastion Host
- In the VPC Dashboard, click Security Groups in the left sidebar
- Click Create security group
- Configure:
| Setting | Value |
|---|---|
| Security group name | bastion-sg |
| Description | Security group for bastion host |
| VPC | my-bastion-vpc |
- Add Inbound Rule:
| Type | Protocol | Port Range | Source | Description |
|---|---|---|---|---|
| SSH | TCP | 22 |
0.0.0.0/0 (or your IP for better security) |
SSH access |
- Keep the default outbound rule (Allow all traffic)
- Click Create security group
Create Security Group for Private Instance
- Click Create security group
- Configure:
| Setting | Value |
|---|---|
| Security group name | private-instance-sg |
| Description | Security group for private instances |
| VPC | my-bastion-vpc |
- Add Inbound Rule:
| Type | Protocol | Port Range | Source | Description |
|---|---|---|---|---|
| SSH | TCP | 22 |
bastion-sg (select the security group) |
SSH from bastion only |
- Click Create security group
🔒 Security Best Practice: By specifying the bastion's security group as the source (instead of an IP range), we ensure only the bastion host can SSH into private instances. If the bastion's IP changes, the rule still works.
Step 6: Launch the Bastion Host (Public EC2 Instance)
Now we'll launch our bastion host in the public subnet.
Create a Key Pair (if you don't have one)
- Navigate to EC2 Dashboard → Key Pairs
- Click Create key pair
- Configure:
| Setting | Value |
|---|---|
| Name | bastion-key |
| Key pair type | RSA |
| Private key file format |
.pem (for Linux/Mac) or .ppk (for Windows/PuTTY) |
- Click Create key pair
- The private key file will automatically download—keep this safe!
Launch the Bastion EC2 Instance
- Navigate to EC2 Dashboard → Instances
- Click Launch instances
- Configure:
Name and Tags:
| Setting | Value |
|---------|-------|
| Name | bastion-host |
Application and OS Images:
| Setting | Value |
|---------|-------|
| AMI | Ubuntu Server 22.04 LTS (or Amazon Linux 2023) |
Instance Type:
| Setting | Value |
|---------|-------|
| Instance type | t2.micro (Free tier eligible) |
Key Pair:
| Setting | Value |
|---------|-------|
| Key pair name | bastion-key |
Network Settings: Click Edit
| Setting | Value |
|---|---|
| VPC | my-bastion-vpc |
| Subnet | public-subnet |
| Auto-assign public IP | Enable |
| Security group | Select existing → bastion-sg
|
- Click Launch instance
💡 Important: We enabled "Auto-assign public IP" because this instance needs to be accessible from the internet.
Step 7: Launch the Private EC2 Instance
Create Another Key Pair for Private Instance
For better security, use a separate key pair for the private instance:
- Create a new key pair named
private-instance-key - Download and save the
.pemfile
Launch the Private EC2 Instance
- Click Launch instances
- Configure:
Name and Tags:
| Setting | Value |
|---------|-------|
| Name | private-instance |
Application and OS Images:
| Setting | Value |
|---------|-------|
| AMI | Ubuntu Server 22.04 LTS (same as bastion) |
Instance Type:
| Setting | Value |
|---------|-------|
| Instance type | t2.micro |
Key Pair:
| Setting | Value |
|---------|-------|
| Key pair name | private-instance-key |
Network Settings: Click Edit
| Setting | Value |
|---|---|
| VPC | my-bastion-vpc |
| Subnet | private-subnet |
| Auto-assign public IP | Disable |
| Security group | Select existing → private-instance-sg
|
- Click Launch instance
🔒 Security Note: We disabled "Auto-assign public IP" because this instance should NOT be directly accessible from the internet.
Step 8: Connect to Your Infrastructure
Now comes the exciting part—actually connecting to your private instance through the bastion host!
Step 8.1: SSH into the Bastion Host
First, let's connect to our bastion host from your local machine.
- Get the public IP of your bastion host from the EC2 console
- Open your terminal
- Set correct permissions on your key file:
chmod 400 bastion-key.pem
- SSH into the bastion:
ssh -i "bastion-key.pem" ubuntu@<BASTION_PUBLIC_IP>
Replace <BASTION_PUBLIC_IP> with your bastion's actual public IP address.
You should see the Ubuntu welcome message—you're now on the bastion host!
Step 8.2: Transfer the Private Key to Bastion Host
To SSH from the bastion to the private instance, we need the private instance's key on the bastion. There are several ways to do this:
Method 1: SCP (Secure Copy)
From your local machine (not the bastion), run:
scp -i "bastion-key.pem" private-instance-key.pem ubuntu@<BASTION_PUBLIC_IP>:~/
Method 2: Copy-Paste (for quick testing)
- Open the private key file locally and copy its contents
- On the bastion, create a new file:
nano ~/private-instance-key.pem
- Paste the contents and save (Ctrl+X, Y, Enter)
Step 8.3: Set Correct Permissions on the Private Key
On the bastion host, set the correct permissions:
chmod 400 ~/private-instance-key.pem
⚠️ Common Error: If you see
Load key "private-instance-key.pem": Permission denied, the key permissions are too open. SSH requires private keys to be readable only by the owner (permissions 400 or 600).⚠️ Another Common Error: If the key is owned by root but you're running as ubuntu, you'll need to change ownership:
sudo chown ubuntu:ubuntu ~/private-instance-key.pem
Step 8.4: SSH into the Private Instance
- Get the private IP of your private instance from the EC2 console (it will be something like
10.0.3.xxx) - From the bastion host, SSH into the private instance:
ssh -i "private-instance-key.pem" ubuntu@<PRIVATE_INSTANCE_PRIVATE_IP>
Success! You're now connected to your private instance that has no public IP address. The only way to reach it is through the bastion host.
Verifying the Setup
Let's verify everything is working as expected.
On the Private Instance, Try to Reach the Internet
ping google.com
This should fail (hang or show "Network is unreachable") because the private instance has no route to the internet.
Check the Private Instance Has No Public IP
curl ifconfig.me
This should timeout or fail, confirming no public IP is assigned.
Check You Cannot SSH Directly to Private Instance
From your local machine, try:
ssh -i "private-instance-key.pem" ubuntu@<PRIVATE_INSTANCE_PRIVATE_IP>
This should fail because your local machine cannot route to private IP addresses in AWS.
Best Practices and Security Recommendations
1. Restrict Bastion SSH Access
Instead of allowing SSH from 0.0.0.0/0, restrict to your specific IP:
Source: YOUR_PUBLIC_IP/32
2. Use SSH Agent Forwarding
Instead of copying private keys to the bastion, use SSH agent forwarding:
# On your local machine
ssh-add private-instance-key.pem
ssh -A -i "bastion-key.pem" ubuntu@<BASTION_PUBLIC_IP>
# Now on the bastion, you can SSH without specifying a key
ssh ubuntu@<PRIVATE_INSTANCE_PRIVATE_IP>
3. Enable VPC Flow Logs
Monitor all network traffic for security analysis:
- Go to VPC → Your VPC → Flow Logs
- Create a flow log to CloudWatch Logs
4. Use Session Manager Instead
AWS Systems Manager Session Manager provides a more secure alternative to SSH bastion hosts:
- No need to open port 22
- No need to manage SSH keys
- Built-in logging and auditing
5. Implement Multi-Factor Authentication
Consider using AWS IAM Identity Center with MFA for accessing your AWS resources.
Troubleshooting Common Issues
"Permission denied (publickey)"
Cause: SSH key issues
Solutions:
- Check key file permissions:
chmod 400 key.pem - Check key ownership:
ls -la key.pem - Ensure you're using the correct key for the correct instance
- Verify the username (ubuntu for Ubuntu AMI, ec2-user for Amazon Linux)
"Connection timed out"
Cause: Network/routing issues
Solutions:
- Verify the instance is running
- Check security group rules allow SSH (port 22)
- Verify route table associations
- Ensure Internet Gateway is attached (for bastion)
"error in libcrypto"
Cause: Corrupted key file
Solutions:
- Re-download the key from AWS
- Use SCP instead of copy-paste to transfer keys
- Check for hidden characters in the key file
"Network is unreachable" from private instance
Cause: This is expected! Private instances have no internet access.
Solution: If you need internet access for updates, set up a NAT Gateway (additional topic for another article).
Cost Considerations
Here's what this setup will cost:
| Resource | Cost |
|---|---|
| VPC | Free |
| Internet Gateway | Free |
| Subnets | Free |
| Route Tables | Free |
| Security Groups | Free |
| t2.micro EC2 (Free Tier) | Free for 750 hours/month |
| t2.micro EC2 (after Free Tier) | ~$8.50/month |
Total: Free if using Free Tier eligible instances, otherwise minimal cost.
Cleanup
If you are doing this for learning purpose, then, clean up resources to avoid unexpected charges when done:
- Terminate EC2 Instances: EC2 → Instances → Select → Instance State → Terminate
- Delete Security Groups: VPC → Security Groups → Delete (delete custom ones)
- Delete Subnets: VPC → Subnets → Delete
- Detach and Delete Internet Gateway: VPC → Internet Gateways → Detach → Delete
- Delete Route Tables: VPC → Route Tables → Delete (delete custom ones)
- Delete VPC: VPC → Your VPCs → Delete
Summary
Congratulations! 🎉 You've successfully built a secure bastion host architecture in AWS. Here's what we accomplished:
✅ Created a VPC with custom CIDR block
✅ Set up an Internet Gateway for public internet access
✅ Created public and private subnets
✅ Configured route tables for proper traffic flow
✅ Implemented security groups for access control
✅ Launched a bastion host in the public subnet
✅ Launched a private instance accessible only through the bastion
✅ Successfully SSH'd from local → bastion → private instance
This architecture forms the foundation for many production AWS environments and is a crucial pattern to understand for anyone working with cloud infrastructure.
What's Next?
Now that you have this foundation, consider exploring:
- NAT Gateway: Allow private instances to access the internet for updates
- AWS Systems Manager Session Manager: Bastion-less access to private instances
- VPC Peering: Connect multiple VPCs together
- AWS PrivateLink: Access AWS services without going through the internet
- Infrastructure as Code: Automate this setup using Terraform or CloudFormation
Connect With Me
If you found this article helpful, give it a ❤️ and follow me for more cloud and DevOps content!
Have questions? Drop them in the comments below—I'd love to help!
This article is part of my AWS Fundamentals series. I am documenting my own cloud journey to serve as a personal knowledge base and to provide a clear, relatable guide for others navigating the same path. For more comprehensive deep dives, explore my full collection of AWS tutorials.
Tags: #aws #cloud #devops #security #networking #tutorial
Top comments (0)