AWS Bastion Host: How To Set Up Secure Connectivity to Your Private Instances.

Okey Ebere
7 min readNov 18, 2023

--

In my previous article, we delved into creating a secure Virtual Private Cloud (VPC), ensuring high availability through AWS Autoscaling Groups and Application Load Balancer (ALB) infrastructure on AWS. We extensively covered components such as subnets, security groups, route tables, and more, establishing a robust and secure environment. Expanding upon this groundwork, our focus today will shift to the concept of an EC2 bastion host and a guide through a step-by-step process to set up an EC2 bastion host.

Suppose you are responsible for managing Linux and Windows servers in the cloud. In that case, there is a 90 percent chance that they are hosted on a private subnet shielded by robust firewalls and security groups that protect them from potential threats from the internet. As a system administrator/cloud engineer, you understand the paramount importance of security for your organization. You recognize that establishing a secure and reliable method is crucial when managing servers. If your solution involves a Bastion Host, then this article is tailored for you.

A Bastion host, also referred to as a ‘jump box,’ is a dedicated server or instance designed to defend against attacks or threats. Functioning as a proxy server, Bastion host intermediates between client machines and remote servers. It acts as a gateway, facilitating secure connections from the internet to a private subnet.

Why should you use Bastion Host?

  1. Security and Access Control:
  • Controlled Access: The bastion host acts as a single access point to your private network, allowing you to control and monitor who can access your internal instances.
  • Reduced Attack Surface: Limiting direct external access to internal instances minimizes the attack surface and potential exposure to security threats.

2. Secure Remote Administration:

  • Remote SSH/RDP Access: System administrators can securely connect to the bastion host using SSH (for Linux instances) or RDP (for Windows instances), then use it as a launching point to connect to other internal instances.

3. Auditability and Logging:

  • Centralized Logging: Access to internal resources is consolidated through the bastion host, enabling centralized logging and auditing of user activities for compliance and security analysis.

4. Minimized Exposure of Internal Resources:

  • Private Subnet Access: Internal instances can be placed in private subnets without direct internet access, minimizing exposure to potential external threats. The bastion host serves as the gateway for legitimate access.
  • Security Industry Standard: Using a bastion host is considered a best practice in secure infrastructure design, providing an additional layer of security to your AWS environment.

How Bastion Host Works?

The Bastion host is a secure entry point into private networks when connecting from external networks, safeguarding against potential attacks. This Bastion host possesses both internal and external IP addresses. To connect to internal instances without relying on external IP addresses, users can first establish a connection to the Bastion host. Subsequently, users can connect to internal instances from the Bastion host. When utilizing the Bastion service, the login process involves initially accessing the Bastion host through SSH and then being directed to the private instances.

Provisioning EC2 Bastion Host Using Terraform.

To continue our journey to establish a secure and well-managed AWS infrastructure using Terraform, we will now focus on provisioning an EC2 Bastion host, which will be used to establish a secure connection to our private subnets.

As a prerequisite, we will utilize the VPC infrastructure we have provisioned in our previous article. If you still need to review it, we strongly recommend doing so as it lays the foundation for the bastion host setup.

All terraform configuration files used for this setup can be found on my GitHub Repository

STEP

  1. Generating SSH Key Pair

There are two ways in creating and managing SSH key pairs for bastion hosts which are using terraform tls_private_key resource and using ssh-keygen. But for this article, we will be making use of terraform tls_private_key.

Terraform provides the tls_private_key resource, which allows you to generate an SSH key pair within your Terraform configuration. We’ll use Terraform to generate an RSA key pair with 4096 bits and save the public key in an OpenSSH-formatted file and the private key in our local host ~/ssh/ working directory and granting all executable permission to the file.

## GENERATING SSH KEY FOR THE BASTION HOST
## Generate PEM (and OpenSSH) formatted private key.
resource "tls_private_key" "ec2-bastion-key-pair" {
algorithm = "RSA"
rsa_bits = 4096
}

## Create the file for Public Key
resource "local_file" "public-key" {
depends_on = [ tls_private_key.ec2-bastion-key-pair ]
content = tls_private_key.ec2-bastion-key-pair.public_key_openssh
filename = var.public-key-path
}

## Create the sensitive file for Private Key
resource "local_sensitive_file" "private-key" {
depends_on = [ tls_private_key.ec2-bastion-key-pair ]
content = tls_private_key.ec2-bastion-key-pair.private_key_pem
filename = var.private-key-path
file_permission = "0600"
}

## AWS SSH Key Pair
resource "aws_key_pair" "key-pair" {
depends_on = [ local_file.public-key ]
key_name = var.key-nam
public_key = tls_private_key.ec2-bastion-key-pair.public_key_openssh
}

The provided code snippet utilizes the tls_private_key resource to generate SSH private and public keys. Subsequently, it employs local_file and local_sensitive_file resources to handle and secure the generated private key.
The local_file resource facilitates saving the private key to a local file, allowing external access beyond Terraform. Given the sensitive nature of the private key containing authentication credentials, safeguarding it is paramount. Enter the local_sensitive_file resource, which encrypts the private key, ensuring secure storage.
Combining local_file and local_sensitive_file resources create a balance between convenience and security. This approach ensures the private key is securely stored and easily retrievable when necessary, addressing the challenges of effectively managing SSH key pairs.

  • variable file
variable "public-key-path" {
type = string
default = "ssh/ec2-bastion.pub"
}
variable "private-key-path" {
type = string
default = "ssh/ec2-bastion.pem"
}
variable "key-nam" {
default = "ec2-bastion"
}

2. Generating Security Group and Elastic IP

Create a security group that allows SSH traffic (port 22) from any source (0.0.0.0/0) and an egress rule that allows all outbound traffic (0.0.0.0/0) for our bastion host.

resource "aws_security_group" "ec2_sg" {
name = "ec2_security_group"
vpc_id = aws_vpc.vpc.id

ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}

Note: In this configuration, we allowed unrestricted access to the bastion host from the public internet through SSH, and this approach isn’t advisable; this is for demonstration purposes. For industrial purposes, it’s strongly recommended to restrict incoming traffic only from a trusted VPN or trusted CIDR range of your organization network. By limiting, you ensure a higher level of security for your Bastion Host and protect it from unauthorized access.

For the Elastic IP we will be creating an Elastic IP for our Bastion Host. Using an Elastic IP (EIP) for a bastion host is regarded as a best practice in designing secure infrastructure. By linking an EIP to the bastion host, you guarantee the persistence of its public IP address, unaffected by actions like stopping, restarting, or replacement of the instance. This approach establishes stability in accessing the bastion host, offering the advantage of depending on a static IP address rather than frequently modifying access rules.

# EIP
resource "aws_eip" "ec2_eip" {
vpc = true
}
## associating the EIP for our Bastion host
resource "aws_eip_association" "ec2-bastion-host-eip-association" {
instance_id = aws_instance.public_ec2.id
allocation_id = aws_eip.ec2_eip.id
}

3. Launch the Bastion Instance

Finally, now we will create EC2 instance that we will be using as bastion host.

# EC2 instance in Public Subnet
resource "aws_instance" "public_ec2" {
ami = var.os # Replace with the desired AMI ID
instance_type = var.instance # Replace with the desired instance type
vpc_security_group_ids = [aws_security_group.ec2_sg.id]
subnet_id = aws_subnet.public_subnet1.id
associate_public_ip_address = false # can only be accessible using EIP public IP
key_name = aws_key_pair.key-pair.key_name # Replace with your SSH key pair
tags = {
Name = "Bastion instances"
description = "EC2 instance "
}
}

This Terraform snippet defines an AWS EC2 instance resource. It Specifies the Amazon Machine Image (AMI) and the instance type for the EC2 instance and associates it with the security group created.
The SSH key pair, which was earlier created, was specified, which enables secure authentication, and a tag is assigned for ease of identification.

Now we are ready to apply the infrastructure,

# first
terraform init
# next
terraform plan
# then
terraform apply

# to destroy the infrasture
terraform destroy

once the infrastructure is provisioned.

  • Firstly, you transfer the private key to your bastion host using SCP.
scp -i "~/path-to-key/ec2-bastion.pem" /part-to-the-keyfile/ec2-bastion.pem  ec2-user@<public-ipv4-of-eip>:~/.
Transferring the private key to the bastion host
  • Then you use your private key and the public IP of your EIP to access the EC2 bastion host (SSH).
ssh -i "ec2-bastion.pem" ec2-user@ec2-user@<public-ipv4-of-eip>
SSH to the Bastion Host
  • Next is to SSH to your private ec2 instance from your bastion host.
ssh -i "~/path-to-the-privatekey/ec2-bastion.pem" ec2-user@<privateIP>
SSH to the private ec2 instance.
confirm the running containers on our private ec2 instance.

Conclusion

Implementing a bastion host in your AWS infrastructure is crucial in enhancing security and access control. The bastion host mitigates potential risks and unauthorized access to your network by providing a controlled gateway for accessing internal resources. Leveraging best practices, such as associating Elastic IPs for stability and employing secure key management, ensures a robust defense against security threats.

--

--