Introduction
Protecting your AWS infrastructure is essential in the constantly changing world of cloud security. A Bastion Host, a crucial gateway that serves as your fortress’s initial line of defense, is one crucial part of its defense. We’ll discuss the importance of a Bastion Host in this blog article and show you how to build one using Terraform on AWS. As we set out on a trip to reinforce your cloud infrastructure and improve your security posture, buckle up.
What is a Bastion Host?
A Bastion host, also known as a jump server, acts as a key intermediate between the broad and possibly hostile expanse of the internet and a network’s private, sensitive servers. This intermediate position gives it the ability to add an extra layer of protection to a network’s inner workings, increasing its resilience against external assaults.
The Bastion host guards the secret jewels hidden within the private architecture of a Virtual Private Cloud (VPC), which is positioned strategically within a public subnet. We can create a safe bridge that goes outside the boundaries of this VPC by using the Bastion host. SSH (Secure Shell) provides this bridge, enabling authorized users to connect to and communicate with the private EC2 instances housed inside the same VPC.
Understanding the intricate details of how the Bastion host, keeping watch in the public subnet, orchestrates the secure passage to the VPC’s inner sanctum and makes sure that only those with the necessary credentials are granted access, is essential to comprehending how this implementation works.
Practical Guide
We are going to create a Terraform Module to deploy a Bastion Host and handle the SSH key.
Prerequisites
- An AWS Account.
- A terminal
- Terraform CLI
Architecture diagram
We will have a VPC, a public subnet, a private subnet, a public instance, a private instance, and an internet gateway required for the public instance to connect to the internet, as shown in the above diagram.
Security group configuration
The configuration of the security group is crucial information that we need to understand for Bastion hosts. By inheriting the security group from the bastion host, the private security group enables SSH access from the bastion to the private EC2 instance. Take a look at it in Terraform code.
# Bastion Host Security Group
resource "aws_security_group" "bastion-host-sg" {
name_prefix = var.security_group_name
description = "Bastion Host instance security group"
vpc_id = aws_vpc.vpc.id
ingress {
description = "Allow SSH from my Public IP"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] #replace with your system ip
}
ingress {
description = "Allows HTTP Access to the Bastion Host"
from_port = 8080
to_port = 8080
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"]
}
tags = {
Name = "bastion-host-sg"
}
}
# Private EC2 Security Group
resource "aws_security_group" "private_sg" {
name_prefix = var.security_group_name
description = "Bastion Host instance security group"
vpc_id = aws_vpc.vpc.id
ingress {
description = "Allow SSH from my Public IP"
from_port = 22
to_port = 22
protocol = "tcp"
security_groups = [aws_security_group.bastion-host-sg.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "private-sg"
}
}
SSH key generation
To produce SSH keys for remote access to EC2 instances on the AWS cloud platform, use the Terraform code listed below. SSH keys are a safe way to log in to and access distant servers.
Let’s step-by-step through the code:A TLS private key is created in the first resource block. SSL (Secure Sockets Layer) was replaced by TLS (Transport Layer Security), which is frequently used for secure internet communication. A popular encryption strategy for safeguarding data transfer, the RSA algorithm is utilized in this instance to produce the private key. Other algorithms include ED25519, P334, and others. Refer to the Terraform registry for further information.
resource "tls_private_key" "rsa_key_generated" {
algorithm = "RSA"
}
The private key is stored in a local file by the second resource block. The “private_key_pem” attribute of the “tls_private_key.generated” resource is shown as the file’s content. “$var.ssh_key_name” is a placeholder for the name of the SSH key that is supplied in the input variables. The “filename” property specifies the name of the file as “$var.ssh_key_name.pem”. File permission is set to 0400 via the “file_permission” attribute, which denotes that only the owner has read access.
resource "local_file" "private_key_pem" {
content = tls_private_key.generated.private_key_pem
filename = "${var.ssh_key_name}.pem"
file_permission = "0400"
}
The “aws_key_pair” resource is used in the third resource block to construct an AWS key pair. Using the input variable once more, the “key_name” property defines the name of the key pair as “$var.ssh_key_name”. “tls_private_key.generated.public_key_openssh” is the value for the “public_key” property, and it fetches the public key in OpenSSH format from the “tls_private_key.generated” resource.
resource "aws_key_pair" "generated" {
key_name = var.ssh_key_name
public_key = tls_private_key.generated.public_key_openssh
}
You can generate an RSA private key using this sample Terraform code, save it to a local file with the required permissions, then construct an AWS key pair with the corresponding public key. AWS’s EC2 instances may then be accessed and authenticated using the generated key pair via SSH.
Bastion Host and EC2 configurations
The security group makes an important difference in this situation. The private ec2 instance receives the private ec2 security group, whereas the Bastion ec2 receives the public security group.
# Public EC2 Instance - BastionHost -Ubuntu, 22.04 LTS
resource "aws_instance" "BastionHost" {
ami = "ami-053b0d53c279acc90"
instance_type = "t2.micro"
key_name = var.common_ssh_key
security_groups = [aws_security_group.bastion-host-sg.id]
subnet_id = aws_subnet.subnet_public_bastion.id
root_block_device {
volume_type = "gp3"
volume_size = 10
throughput = 500
delete_on_termination = true
}
tags = {
Name = "Bastion-Host"
}
}
# Private EC2 Instance - Ubuntu, 22.04 LTS
resource "aws_instance" "ubuntu-instance" {
ami = "ami-053b0d53c279acc90"
instance_type = "t2.micro"
key_name = var.common_ssh_key
security_groups = [aws_security_group.private_sg.id]
subnet_id = aws_subnet.subnet_private.id
root_block_device {
volume_type = "gp3" #Faster then regular gp3 and cost less
volume_size = 10
throughput = 300
delete_on_termination = true
}
tags = {
Name = "ubuntu-private-instance"
}
}
In this code, the new code is the “root_block_device ” where I started to add volume type to “gp3” to increase efficiency and to reduce cost.
Networking configuration
As explained above, we need some configuration regarding to networking services on AWS. The code is below.
#Create VPC and its subnets
# Custom VPC
resource "aws_vpc" "vpc" {
cidr_block = var.vpc_cidr
tags = {
Name = var.vpc_custom
}
}
# Public Subnet for bastion host
resource "aws_subnet" "subnet_public_bastion" {
vpc_id = aws_vpc.vpc.id
cidr_block = var.subnet_cidr_bhec2
availability_zone = "us-east-1a"
map_public_ip_on_launch = true
tags = {
Name = "subnet_public_bastionhost"
}
}
# Private Subnet
resource "aws_subnet" "subnet_private" {
vpc_id = aws_vpc.vpc.id
cidr_block = var.subnet_cidr_pec2
availability_zone = "us-east-1b"
tags = {
Name = var.subnet_custom
}
}
# Internet Gateway
resource "aws_internet_gateway" "internet_gateway" {
vpc_id = aws_vpc.vpc.id
tags = {
Name = var.internet_gateway_tag
}
}
# Route Table
resource "aws_default_route_table" "default_route" {
default_route_table_id = aws_vpc.vpc.default_route_table_id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.internet_gateway.id
}
}
With this code, we can deploy a single VPC with two subnets, one private and one public, with a Internet Gateway attached to gain access to the infrastructure through internet. This code can be adapted in current architectures, only pointing to a existing VPC and subnets.
Outputs
Regarding to output items, we need this two basically:
output "remote_access_key" {
description = "EC2 Remote Access"
value = "ssh -i ${local_file.private_key_pem.filename} ubuntu@${aws_instance.BastionHost.public_ip}"
}
output "instance_public_ip" {
description = "Public IP address of the Jenkins EC2 instance"
value = "Bastion Host Public IP: ${aws_instance.BastionHost.public_ip}"
}
Deployment Guide
Before we run terraform Apply using the Terraform CLI, we must see the ssh file in the same directory as our terraform files
Then, we can look the new created instances:
Let’s try to connect to our Bastion Host
We can see that we connect successfully, we can get the same result as if we try to connect from the private instance and have access to internet in a secure way through the Internet Gateway.