Let's pretend you have these beautiful secure AWS resources. Off some where in a Private subnet hidden away. Then one day something goes wrong with one of your EC2 instances, and you have to actually get on the server and run commands....So how do you get access...without just opening up these resources to the public internet.

Enter a Bastion host.
Straight from the wikipedia article:

A bastion host is a special purpose computer on a network specifically designed and configured to withstand attacks. The computer generally hosts a single application, for example a proxy server, and all other services are removed or limited to reduce the threat to the computer. It is hardened in this manner primarily due to its location and purpose, which is either on the outside of a firewall or in a demilitarized zone (DMZ) and usually involves access from untrusted networks or computers.


Steps for creating a simple Bastion Host


Create an EC2 instance:

provider "aws" {
  region = "us-west-1"
}

resource "aws_default_vpc" "default" {}

resource "aws_instance" "bastion" {
  ami                         = "ami-969ab1f6"
  instance_type               = "t2.micro"
  associate_public_ip_address = true
}

output "bastion_public_ip" {
  value = "${aws_instance.bastion.public_ip}"
}aws_default_vpc.default: Creating...     
  assign_generated_ipv6_cidr_block: "" => "<computed>"                             
  cidr_block:                       "" => "<computed>"                             
  default_network_acl_id:           "" => "<computed>"                             
  default_route_table_id:           "" => "<computed>"                             
  default_security_group_id:        "" => "<computed>"                             
  dhcp_options_id:                  "" => "<computed>"                             
  enable_classiclink:               "" => "<computed>"                             
  enable_classiclink_dns_support:   "" => "<computed>"                             
  enable_dns_hostnames:             "" => "<computed>"                             
  enable_dns_support:               "" => "true"                                   
  instance_tenancy:                 "" => "<computed>"                             
  ipv6_association_id:              "" => "<computed>"                             
  ipv6_cidr_block:                  "" => "<computed>"                             
  main_route_table_id:              "" => "<computed>"                             
aws_instance.bastion: Creating...        
  ami:                          "" => "ami-969ab1f6"                               
  associate_public_ip_address:  "" => "true"                                       
  availability_zone:            "" => "<computed>"                                 
  ebs_block_device.#:           "" => "<computed>"                                 
  ephemeral_block_device.#:     "" => "<computed>"                                 
  instance_state:               "" => "<computed>"                                 
  instance_type:                "" => "t2.micro"                                   
  ipv6_address_count:           "" => "<computed>"                                 
  ipv6_addresses.#:             "" => "<computed>"                                 
  key_name:                     "" => "<computed>"                                 
  network_interface.#:          "" => "<computed>"                                 
  network_interface_id:         "" => "<computed>"                                 
  placement_group:              "" => "<computed>"                                 
  primary_network_interface_id: "" => "<computed>"                                 
  private_dns:                  "" => "<computed>"                                 
  private_ip:                   "" => "<computed>"                                 
  public_dns:                   "" => "<computed>"                                 
  public_ip:                    "" => "<computed>"                                 
  root_block_device.#:          "" => "<computed>"                                 
  security_groups.#:            "" => "<computed>"                                 
  source_dest_check:            "" => "true"                                       
  subnet_id:                    "" => "<computed>"                                 
  tenancy:                      "" => "<computed>"                                 
  volume_tags.%:                "" => "<computed>"                                 
  vpc_security_group_ids.#:     "" => "<computed>"                                 
aws_default_vpc.default: Creation complete after 2s (ID: vpc-ed0b5e89)             
aws_instance.bastion: Still creating... (10s elapsed)                              
aws_instance.bastion: Still creating... (20s elapsed)                              
aws_instance.bastion: Still creating... (30s elapsed)                              
aws_instance.bastion: Creation complete after 32s (ID: i-08d7dd1535eb44c9a)        

Apply complete! Resources: 2 added, 0 changed, 0 destroyed.                        

Outputs:                                 

bastion_public_ip = 53.152.127.39   

Yeah!!! Except you can't SSH to this new bastion....which was kinda the whole point :(


Setting Up SSH


To SSH onto our Bastion we need to do a couple things:

  • Create a Key Pair
  • Add Key Pair to Terraform setup
  • Allow incoming traffic through SSH

Create Key Pair

ssh-keygen -t rsa -b 4096 -C "your_email@example.com" -f $HOME/.ssh/your_key_name

Add Key Pair to Terraform setup

cat $HOME/.ssh/your_key_name.pub | pbcopy
resource "aws_key_pair" "bastion_key" {
  key_name   = "your_key_name"
  public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDWbz6ur89BKQ+am87EovJsv6g9QpbOiw13lTF7Kw1StbQAmkcGGrNTK2LIWsP3cQf+P+gptRAJbeeB1jQKZ283TwwREIv+l5AMKrbEkanOF4zsc8a9zitejlOLvVUxtVoMi5ROVYD2dLKjqAbDtqIC9LmMD+hcpqcXLhS6t+HVSVI862dTNVFY1EGukLGQ3IEJfw5v7FDzLn72NsuUiXEeCZu8DtlXLCTYRnqv+XkJQWVocPdFDUWISSIQ0CTFu+GJvJjdqDyAhYo3it7Eybj6XuSgLDwkQcNU45Eee4Nn7LwV+f4Av8D25m4FZOfpWaj5+q9Fc9nRdIsB7P0oFgj5YoaTngQKy27MJ5UppMO7OOhriurJ/PBOrGpeqPcftWKLpcHLIGrm3ndoDKQx12R1s0gyYpA4JuNUWHYcxNrFa2rs/6AoFuS7wNUmM+DYB8iTjOl6dT8dS5AgMxGoZ3NepMPYilw1gf+gw9Ft3pHs2IMfDfqwZpXga8KdYwxBmRakpHdA7Nzje8ufvP/TBawsqVcW7z5gG9uPhYtfnYYezSIxv56PMSWEfqchkz+raPsElzIGtPcC1snncQlau95utV25r88BzXhCMJwNy9aDNEfSrm5SORlA97xicroCOuRjw2PnQyIXKvWDZtyqX5799x37K/HDYpJnvcgwpTlDZQ== your_email@example.com"
}

Allow incoming Traffic through SSH

resource "aws_default_vpc" "default" { }

resource "aws_security_group" "bastion-sg" {
  name   = "bastion-security-group"
  vpc_id = "${aws_default_vpc.default.id}"

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

We use the default VPC ID, which we get through the aws_default_vpc resource,
which is an advanced resource, so tread lightly.

Finally we can SSH onto the Bastion:

ssh -i ~/.ssh/your_key_name ubuntu@54.219.138.125

Note: your IP will be different

And Hurray! we are inside the bastion....but wait a minute:

curl -4 http://wttr.in/los_angeles

....we are cutting off from the outside world. We allow incoming traffic on port 22, but we did not specify anything about outgoing traffic. We need to add an egress rule to allow us to call out to where ever our hearts desire:

resource "aws_security_group" "bastion-sg" {
  name   = "bastion-security-group"
  vpc_id = "${aws_default_vpc.default.id}"

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

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

After we apply ours changes with terraform apply and SSH onto our new EC2 instance, we now curl to the outside world:

curl -4 http://wttr.in/los_angeles

Huzzah!

cropped_weather

That is a simple place to start with a Bastion in Terraform.


The whole script looks like this:

provider "aws" {
  region = "us-west-1"
}

resource "aws_default_vpc" "default" {}

resource "aws_instance" "bastion" {
  ami                         = "ami-969ab1f6"
  key_name                    = "${aws_key_pair.bastion_key.key_name}"
  instance_type               = "t2.micro"
  security_groups             = ["${aws_security_group.bastion-sg.name}"]
  associate_public_ip_address = true
}

resource "aws_security_group" "bastion-sg" {
  name   = "bastion-security-group"
  vpc_id = "${aws_default_vpc.default.id}"

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

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

resource "aws_key_pair" "bastion_key" {
  key_name   = "your_key_name"
  public_key = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDWbz6ur89BKQ+am87EovJsv6g9QpbOiw13lTF7Kw1StbQAmkcGGrNTK2LIWsP3cQf+P+gptRAJbuqB1jQKZ283TwwREIv+l5AMKrbEkanOF4zsc8a9zitejlOLvVUxtVoMi5ROVYD2dLKjqAbDtqIC9LmMD+hcpqcXLhS6t+HVSVI862dTNVFY1EGukLGQ3IEJfw5v7FDzLn72NsuUiXEeCZu8DtlXLCTYRnqv+XkJQWVocPdFDUWISSIQ0CTFu+GJvJjdqDyAhYo3it7Eybj6XuSgLDwkQcNU45Ewz4Nn7LwV+f4Av8D25m4FZOfpWaj5+q9Fc9nRdIsB7P0oFgj5YoaTngQKy27MJ5UppMO7OOhriurJ/PBOrGpeqPcftWKLpcHLIGrm3ndoDKQx12R1s0gyYpA4JuNUWHYcxNrFa2rs/6AoFuS7wNUmM+DYB8iTjOl6dT8dS5AgMxGoZ3NepMPYilw1gf+gw9Ft3pHs2IMfDfqwZpXga8KdYwxBmRakpHdA7Nzje8ufvP/TBawsqVcW7z5gG9uPhYtfnYYezSIxv56PMSWEfqchkz+raPsElzIGtPcC1snncQlau95utV25r88BzXhCMJwNy9aDNEfSrm5SORlA97xicroCOuRjw2PnQyIXKvWDZtyqX5799x37K/HDYpJnvcgwpTlDZQ== your_email@example.com"
}

output "bastion_public_ip" {
  value = "${aws_instance.bastion.public_ip}"
}