DEV Community

Sagnik Ghosh
Sagnik Ghosh

Posted on

Building a Secure Bastion Host Architecture in AWS: A Complete Step-by-Step Guide

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:

  1. Reduced Attack Surface: Only one server (the bastion) is exposed to the internet, not your entire infrastructure.
  2. Centralized Access Control: All SSH access flows through a single point, making it easier to monitor and audit.
  3. Enhanced Security: Private instances have no public IP addresses, making them invisible to the outside world.
  4. 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)     │
         └─────────────┘
Enter fullscreen mode Exit fullscreen mode

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

  1. Log into the AWS Management Console
  2. Search for "VPC" in the search bar
  3. Click on VPC Dashboard

Create the VPC

  1. Click Create VPC
  2. Select VPC only (we'll create subnets manually for learning purposes)
  3. 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
  1. 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

  1. In the VPC Dashboard, click Internet Gateways in the left sidebar
  2. Click Create internet gateway
  3. Configure:
Setting Value
Name tag my-bastion-igw
  1. Click Create internet gateway

Attach to VPC

After creation, the IGW is in a "Detached" state. We need to attach it to our VPC:

  1. Select the newly created internet gateway
  2. Click ActionsAttach to VPC
  3. Select my-bastion-vpc
  4. 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

  1. In the VPC Dashboard, click Subnets in the left sidebar
  2. Click Create subnet
  3. 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
  1. Click Create subnet

Create the Private Subnet

  1. Click Create subnet again
  2. 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
  1. 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

  1. In the VPC Dashboard, click Route Tables in the left sidebar
  2. Click Create route table
  3. Configure:
Setting Value
Name public-route-table
VPC my-bastion-vpc
  1. Click Create route table

Add Internet Route to Public Route Table

  1. Select public-route-table
  2. Click the Routes tab
  3. Click Edit routes
  4. Click Add route
  5. Configure:
Destination Target
0.0.0.0/0 Select "Internet Gateway" → my-bastion-igw
  1. 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

  1. With public-route-table still selected, click the Subnet associations tab
  2. Click Edit subnet associations
  3. Check the box next to public-subnet (Select the Subnet that you created which is supposed to be accessible to the internet)
  4. Click Save associations

Create the Private Route Table

  1. Click Create route table
  2. Configure:
Setting Value
Name private-route-table
VPC my-bastion-vpc
  1. Click Create route table

Associate Private Subnet with Private Route Table

  1. Select private-route-table
  2. Click the Subnet associations tab
  3. Click Edit subnet associations
  4. Check the box next to private-subnet
  5. 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

  1. In the VPC Dashboard, click Security Groups in the left sidebar
  2. Click Create security group
  3. Configure:
Setting Value
Security group name bastion-sg
Description Security group for bastion host
VPC my-bastion-vpc
  1. 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
  1. Keep the default outbound rule (Allow all traffic)
  2. Click Create security group

Create Security Group for Private Instance

  1. Click Create security group
  2. Configure:
Setting Value
Security group name private-instance-sg
Description Security group for private instances
VPC my-bastion-vpc
  1. Add Inbound Rule:
Type Protocol Port Range Source Description
SSH TCP 22 bastion-sg (select the security group) SSH from bastion only
  1. 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)

  1. Navigate to EC2 DashboardKey Pairs
  2. Click Create key pair
  3. Configure:
Setting Value
Name bastion-key
Key pair type RSA
Private key file format .pem (for Linux/Mac) or .ppk (for Windows/PuTTY)
  1. Click Create key pair
  2. The private key file will automatically download—keep this safe!

Launch the Bastion EC2 Instance

  1. Navigate to EC2 DashboardInstances
  2. Click Launch instances
  3. 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
  1. 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:

  1. Create a new key pair named private-instance-key
  2. Download and save the .pem file

Launch the Private EC2 Instance

  1. Click Launch instances
  2. 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
  1. 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.

  1. Get the public IP of your bastion host from the EC2 console
  2. Open your terminal
  3. Set correct permissions on your key file:
chmod 400 bastion-key.pem
Enter fullscreen mode Exit fullscreen mode
  1. SSH into the bastion:
ssh -i "bastion-key.pem" ubuntu@<BASTION_PUBLIC_IP>
Enter fullscreen mode Exit fullscreen mode

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>:~/
Enter fullscreen mode Exit fullscreen mode

Method 2: Copy-Paste (for quick testing)

  1. Open the private key file locally and copy its contents
  2. On the bastion, create a new file:
nano ~/private-instance-key.pem
Enter fullscreen mode Exit fullscreen mode
  1. 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
Enter fullscreen mode Exit fullscreen mode

⚠️ 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

  1. Get the private IP of your private instance from the EC2 console (it will be something like 10.0.3.xxx)
  2. From the bastion host, SSH into the private instance:
ssh -i "private-instance-key.pem" ubuntu@<PRIVATE_INSTANCE_PRIVATE_IP>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

3. Enable VPC Flow Logs

Monitor all network traffic for security analysis:

  1. Go to VPC → Your VPC → Flow Logs
  2. 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:

  1. Terminate EC2 Instances: EC2 → Instances → Select → Instance State → Terminate
  2. Delete Security Groups: VPC → Security Groups → Delete (delete custom ones)
  3. Delete Subnets: VPC → Subnets → Delete
  4. Detach and Delete Internet Gateway: VPC → Internet Gateways → Detach → Delete
  5. Delete Route Tables: VPC → Route Tables → Delete (delete custom ones)
  6. 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)