AWS EC2 Bastion Hosts
Introduction
In cloud infrastructure, security is essential, and a bastion host is key to keeping your private network safe. Acting as a secure gateway, it protects access to private resources. In this post, we’ll explore AWS EC2 bastion hosts, their role in network security, and the best ways to set them up for maximum security.
What a Bastion Host Is and How It Works
Simply put, a bastion host (also called a “jump box”) is a server specifically designed to provide secure, limited access to your private network. Instead of allowing direct access to your servers, which increases vulnerability, a bastion host acts as a go-between.
Think of a bastion host as a secure gateway or checkpoint into your private network. It's an EC2 instance that sits in a public subnet and acts as the only entry point to your private EC2 instances. When you need to access a private server, you first SSH into the bastion host, and from there, you can SSH into your private servers.
Imagine a bastion host as a secure entry point to your private network. It’s an EC2 instance in a public subnet that acts as the only door to reach your private servers. If you need to access a server in the private network, you start by connecting to the bastion host, and then from there, you can connect to the private servers.
The bastion host is set up with strict security rules, usually allowing only SSH access through port 22 and accepting connections only from approved IP addresses, this way your private servers stay protected from the public internet, but authorized admins can still access them securely.
From Concept to Execution
Throughout this post, I will guide you through the architecture outlined below in order to build a Bastion Host.
The idea is to provision two EC2 servers in a private subnet and another EC2 server in a public subnet to serve as a jump box. The jump box will allow the admin to securely access the private instances for configuration and other administrative tasks.
Provision Resources
I’ll use a CloudFormation template to set up the VPC, subnets, the internet gateway and route tables. For the EC2 instances, I’ll set them up through the AWS Console to make things easier to follow.
First, create a file named BastionHosts.yaml and start by adding the following code:
This is the Parameters section of a CloudFormation template. It defines three parameters to configure the IP ranges for a VPC and its subnets:
- VpcCidr: Sets the IP range (CIDR block) for the entire VPC. Default Value:
10.0.0.0/16
- PublicSubnetCidr: Sets the IP range for the public subnet. Default Value:
10.0.1.0/24
- PrivateSubnetCidr: Sets the IP range for the private subnet. Default Value:
10.0.2.0/24
These parameters make the VPC’s IP ranges easy to customize without changing the template code directly.
Next, we need to add the Resources section, which will define the logical resources that will become physical resources when the template is deployed.
This section defines the resources that will be created when the CloudFormation template is deployed:
- MzVPC: Creates a VPC with the CIDR block provided by the
VpcCidr
parameter. It also enables DNS hostnames and support for DNS. - InternetGateway: Creates an Internet Gateway (IGW) to provide internet access to the VPC.
- AttachGateway: Attaches the Internet Gateway to the VPC so that resources in the VPC can access the internet.
These resources define the basic network infrastructure: a VPC, internet access, and the necessary connection between them.
The code above defines the public subnet and private subnet that will be created in the VPC:
- PublicSubnet: Creates a public subnet within the VPC using the CIDR block provided by the
PublicSubnetCidr
parameter. It is placed in the first available availability zone and enables public IP addresses for instances launched in this subnet. - PrivateSubnet: Creates a private subnet within the VPC using the CIDR block provided by the
PrivateSubnetCidr
parameter. It is also placed in the first available availability zone, but it does not automatically assign public IPs to instances launched in this subnet.
These subnets define the network layout: one for public-facing resources and one for private, internal resources.
The code above defines the route tables for both the public and private subnets, as well as the routes that connect them to the internet:
- PublicRouteTable: Creates a route table for the public subnet and associates it with the VPC.
- PublicRoute: Defines the route for the public subnet, allowing traffic to the internet via the Internet Gateway. The route is set to
0.0.0.0/0
, meaning it allows all traffic to the internet. - PublicSubnetRouteTableAssociation: Associates the public subnet with the public route table so that instances in the public subnet can route traffic to the internet.
- PrivateRouteTable: Creates a route table for the private subnet, again associating it with the VPC but without a direct route to the internet.
- PrivateSubnetRouteTableAssociation: Associates the private subnet with the private route table, ensuring instances in the private subnet route traffic only internally.
These route tables and associations ensure that the public subnet can access the internet while the private subnet remains isolated or has controlled access to the internet.
Now that our template is ready, let's deploy it using the CloudFormation service in the AWS Console UI. If you click the View in Infrastructure Composer button, you will see the infrastructure as code that we created, as represented in the image below
Now we're ready to proceed with deploying our template. Simply wait until the CloudFormation stack status updates to CREATE_COMPLETE.
Now that our VPC architecture is ready, we can begin creating the EC2 instances to complete the Bastion Host setup.
Provision The Bastian Host
As mentioned in the previous section, I will use the Console UI to provision the EC2 instances. Go to the EC2 service, and click on Launch Instance to begin.
After entering the name and selecting the type for the EC2 instance, we need to specify a key pair to enable SSH access from our computer to the Bastion Host.
If you don't already have a key pair, simply click on the Create new key pair button
Under the network settings, we need to select the VPC we created and the public subnet, as the jump box needs to be located in the public subnet
The final step is to set the security group, we will create a new one and allow SSH connections from anywhere
Note: We can specify a specific CIDR IP block in the source of the security group for better security, but for the purposes of this post, we will allow access from anywhere.
After that, click on the Launch Instance button and wait for the EC2 instance to be provisioned. Once the instance is running, go to the Instance Details section to verify the Public IP other configurations.
SSH the Bastian Host
In my case, the public IP address for the Bastion Host is "35.91.217.143," and the key name is "MZ-Bastina-Host-Key.pem", so to establish an SSH connection, follow these steps:
- Open the terminal: Navigate to the folder where you downloaded the key pair.
- Change the permissions for the key pair: Type the following command in the terminal:
chmod 400 "MZ-Bastina-Host-Key.pem"
- Copy the public IP address: Copy the public IP address of the Bastion Host.
- Run the SSH command: Run the following SSH command to connect:
ssh -i .\MZ-Bastina-Host-Key.pem ec2-user@35.91.217.143
Provision The Private Instances
The steps to provision the private instances, Server 1 and Server 2, are the same as those for the Bastion Host instance. The only difference is in the Network settings, as these instances are private and need to be located in the private subnet without enabling the Auto-assign public IP option.
About the security group, it will be different because we need to allow SSH access only from the Jump Box. We can do this by changing the source type to custom, then selecting the security group of the Jump Box as the source.
Now that our three instances are running, it's time to test the connectivity from the Jump Box to the private instances, Server 1 and Server 2.
In order to do an SSH connection from our Bastion Host to the private servers, we first need to upload the key pairs of Server 1 and Server 2 to the Bastion Host and we can use the scp command.
Now, let's try to make the SSH connection from the jump box. For that, we need to use the private IPs of Server 1 (ip: 10.0.2.252) and Server 2 (ip: 10.0.2.164).
Conclusion
In summary, setting up a Bastion Host is a key step in building secure access points within your AWS environment. By carefully configuring networking, security groups, and SSH keys, we’ve established a secure gateway that allows controlled access to private instances. This setup enhances security by isolating access to sensitive resources, while also simplifying management and reducing potential vulnerabilities.