Hapag-Lloyd / terraform-aws-bastion-host-ssm

Terraform module to create an enterprise grade bastion host: High availability, SSM access only, encrypted disk and flexible resource naming.
Apache License 2.0
9 stars 4 forks source link
aws bastion ssm terraform

terraform-aws-bastion-host-ssm

Terraform registry Actions

This Terraform module installs a bastion host accessible via SSM only. The underlying EC2 instance has no ports opened. All data is encrypted and a resource_prefix can be specified to integrate into your naming schema.

The implemented connection method allows port forwarding for one port only. Multiple port forwardings can be realized by the user by creating multiple connections to the bastion host.

Check the examples directory for the module usage.

Cost Estimation (for version 2.4.0)

Name                                                                    Monthly Qty  Unit                   Monthly Cost
module.bastion_host.aws_autoscaling_group.on_spot[0]
 └─ module.bastion_host.aws_launch_template.manual_start
    ├─ Instance usage (Linux/UNIX, on-demand, t3.nano)                          730  hours                         $4.38
      └─ root_block_device
         └─ Storage (general purpose SSD, gp3)                                   16  GB                            $1.52
    └─ Instance usage (Linux/UNIX, spot, t3.nano)                               730  hours                         $1.31
      └─ root_block_device
         └─ Storage (general purpose SSD, gp3)                                   16  GB                            $1.52

 module.bastion_host.aws_cloudwatch_log_group.panic_button_off
 ├─ Data ingested                                               Monthly cost depends on usage: $0.63 per GB
 ├─ Archival Storage                                            Monthly cost depends on usage: $0.0324 per GB
 └─ Insights queries data scanned                               Monthly cost depends on usage: $0.0063 per GB

 module.bastion_host.aws_cloudwatch_log_group.panic_button_on
 ├─ Data ingested                                               Monthly cost depends on usage: $0.63 per GB
 ├─ Archival Storage                                            Monthly cost depends on usage: $0.0324 per GB
 └─ Insights queries data scanned                               Monthly cost depends on usage: $0.0063 per GB

 module.bastion_host.aws_lambda_function.panic_button_off
 ├─ Requests                                                    Monthly cost depends on usage: $0.20 per 1M requests
 └─ Duration                                                    Monthly cost depends on usage: $0.0000166667 per GB-seconds

 module.bastion_host.aws_lambda_function.panic_button_on
 ├─ Requests                                                    Monthly cost depends on usage: $0.20 per 1M requests
 └─ Duration                                                    Monthly cost depends on usage: $0.0000166667 per GB-seconds

 OVERALL TOTAL                                                                                                     $8.73

Features

Panic Switch

Two lambda functions are provided. One to enable the bastion host, e.g. if you have to work at night and the bastion hosts are deactivated. The second lambda function disables the bastion host immediately no matter what.

As both functions are destructive (they modify the autoscaling group), you have to re-apply this module as soon as possible to restore the auto scaling setting (especially the schedules).

Keepass Support For IAM User Credentials

In case you are not using SSO or similar techniques you have to store the credentials for the user able to connect to the bastion host somewhere. We provide a little helper script to handle this scenario in a secure way.

Create a Keepass database and add the KPScript plugin. The scripts/export_aws_credentials_from_keypass.sh will read and export the credentials from the Keepass database.

Schedules

Schedules allow to start and shutdown the instance at certain times. If your work hours are from 9 till 5 in Berlin, add

module "bastion" {
  # ...
  schedule {
    start = "0 9 * * MON-FRI"
    stop = "0 17 * * MON-FRI"

    time_zone = "Europe/Berlin"
  }
}

The bastion host will automatically start at 9 and shuts down at 17 from monday to friday (Berlin time). Depending on the instance_type you will save more or less money. Do not forget to adjust the timezone.

In case you have to start a bastin host outside the working hours use the launch template provided by the module and launch the new instance from the AWS CLI or Console. Don't forget to shut it down if you are done.

Encryption

In case you are using spot instances don't forget to allow AWSServiceRoleForAutoScaling to access your keys.

data "aws_iam_policy_document" "key_policy" {
    # ...

    statement {
    sid    = "AdminKMSManagement"
    effect = "Allow"

    principals {
      identifiers = [
        "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
      ]
      type = "AWS"
    }

    actions = [
      "kms:*"
    ]
    resources = ["*"]
    }

    statement {
    sid    = "Allow spot instances use of the customer managed key"
    effect = "Allow"

    principals {
      identifiers = ["arn:aws:iam::${var.aws_account_id}:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling"]
      type        = "AWS"
    }

    actions = ["kms:Encrypt",
      "kms:Decrypt",
      "kms:ReEncrypt*",
      "kms:GenerateDataKey*",
      "kms:DescribeKey"
    ]
    resources = ["*"]
  }

  statement {
    sid    = "Allow attachment of persistent resources"
    effect = "Allow"

    principals {
      identifiers = ["arn:aws:iam::${var.aws_account_id}:role/aws-service-role/autoscaling.amazonaws.com/AWSServiceRoleForAutoScaling"]
      type        = "AWS"
    }

    actions   = ["kms:CreateGrant"]
    resources = ["*"]
    condition {
      test     = "Bool"
      variable = "kms:GrantIsForAWSResource"
      values   = ["true"]
    }
  }
}

Connect To The Bastion Host

The Session Manager Plugin is needed to connect via SSM to the bastion host.

AWS-Gate

AWS-Gate.

AWS CLI

instance_id="my-bastion-instance-id"
az="az-of-the-bastion"

export AWS_ACCESS_KEY_ID="xxxxx"
export AWS_SECRET_ACCESS_KEY="yyyyy"
export AWS_SESSION_TOKEN=""

aws sts assume-role --role-arn the-bastion-role-arn --role-session-profile bastion
echo "export the credentials from above!"

echo -e 'y\n' | ssh-keygen -t rsa -f bastion_key -N '' >/dev/null 2>&1
ssh_public_key=$(cat bastion_key.pub)

aws ec2-instance-connect send-ssh-public-key --instance-id "${instance_id}" --availability-zone "${az}" \
      --instance-os-user ec2-user --ssh-public-key "${ssh_public_key}"

ssh "ec2-user@${instance_id}" -i bastion_key -N -L "12345:my.cloud.http:80" -o "UserKnownHostsFile=/dev/null" \
    -o "StrictHostKeyChecking=no" -o ProxyCommand="aws ssm start-session --target %h --document AWS-StartSSHSession \
      --parameters portNumber=%p"

curl http://localhost:12345/

AWS CLI With Menu

  1. Export the AWS credentials for the user able to connect to the bastion host.
  2. Execute scripts/connect_bastion.sh. Make sure to add the port forwarding and change the role ARN and bastion instance name.
  3. Access the forwarded service through the local port.

Direct access to the bastion host is not granted but the specified port is forwarded. This way you can access the database, Redis cluster, ... directly from your localhost.

A Bastion Host

Wikipedia

Module Documentation

Requirements

Name Version
terraform >= 1.0.0
archive >= 2.0.0
aws >= 4.0.0

Providers

Name Version
archive >= 2.0.0
aws >= 4.0.0

Modules

Name Source Version
instance_profile_role terraform-aws-modules/iam/aws//modules/iam-assumable-role 5.39.0

Resources

Name Type
aws_ami_copy.latest_amazon_linux resource
aws_autoscaling_group.on_demand resource
aws_autoscaling_group.on_spot resource
aws_autoscaling_schedule.down resource
aws_autoscaling_schedule.up resource
aws_cloudwatch_log_group.panic_button_off resource
aws_cloudwatch_log_group.panic_button_on resource
aws_iam_policy.access_bastion resource
aws_iam_policy.panic_button_off resource
aws_iam_policy.panic_button_on resource
aws_iam_role.access_bastion resource
aws_iam_role.panic_button_off_execution resource
aws_iam_role.panic_button_on_execution resource
aws_iam_role_policy_attachment.access_bastion resource
aws_iam_role_policy_attachment.panic_button_off resource
aws_iam_role_policy_attachment.panic_button_off_basic_execution resource
aws_iam_role_policy_attachment.panic_button_off_x_ray resource
aws_iam_role_policy_attachment.panic_button_on resource
aws_iam_role_policy_attachment.panic_button_on_basic_execution resource
aws_iam_role_policy_attachment.panic_button_on_x_ray resource
aws_lambda_function.panic_button_off resource
aws_lambda_function.panic_button_on resource
aws_launch_configuration.this resource
aws_launch_template.manual_start resource
aws_security_group.this resource
aws_security_group_rule.egress_open_ports resource
aws_security_group_rule.egress_ssm resource
archive_file.panic_button_off_package data source
archive_file.panic_button_on_package data source
aws_ami.deprecated_latest_amazon_linux data source
aws_caller_identity.this data source
aws_iam_policy_document.access_bastion data source
aws_iam_policy_document.panic_button_off data source
aws_iam_policy_document.panic_button_off_assume_role data source
aws_iam_policy_document.panic_button_on data source
aws_iam_policy_document.panic_button_on_assume_role data source
aws_region.this data source

Inputs

Name Description Type Default Required
ami_id The AMI ID to use for the bastion host. If not set, the latest AMI matching the ami_name_filter will be used. string null no
ami_name_filter (Deprecated; set var.ami_id instead; will be removed in v3.0.0) The search filter string for the bastion AMI. string "amzn2-ami-hvm-*-x86_64-ebs" no
bastion_access_tag_value Value added as tag 'bastion-access' of the launched EC2 instance to be used to restrict access to the machine vie IAM. string "developer" no
egress_open_tcp_ports The list of TCP ports to open for outgoing traffic. list(number) n/a yes
iam_role_path Role path for the created bastion instance profile. Must end with '/'. Not used if instance["profile_name"] is set. string "/" no
iam_user_arns ARNs of the user who are allowed to assume the role giving access to the bastion host. Not used if instance["profile_name"] is set. list(string) n/a yes
instance Defines the basic parameters for the EC2 instance used as Bastion host
object({
type = string # EC2 instance type
desired_capacity = number # number of EC2 instances to run
root_volume_size = number # in GB
enable_monitoring = bool
enable_spot = bool
profile_name = string
})
{
"desired_capacity": 1,
"enable_monitoring": false,
"enable_spot": false,
"profile_name": "",
"root_volume_size": 8,
"type": "t3.nano"
}
no
instances_distribution Defines the parameters for mixed instances policy auto scaling
object({
on_demand_base_capacity = number # absolute minimum amount of on_demand instances
on_demand_percentage_above_base_capacity = number # percentage split between on-demand and Spot instances
spot_allocation_strategy = string
})
{
"on_demand_base_capacity": 0,
"on_demand_percentage_above_base_capacity": 0,
"spot_allocation_strategy": "lowest-price"
}
no
kms_key_arn The ARN of the KMS key used to encrypt the resources. string null no
resource_names Settings for generating resource names. Set the prefix and the separator according to your company style guide.
object({
prefix = string
separator = string
})
{
"prefix": "bastion",
"separator": "-"
}
no
schedule Defines when to start and stop the instances. Use 'start' and 'stop' with a cron expression and add the 'time_zone'.
object({
start = string
stop = string
time_zone = string
})
null no
subnet_ids The subnets to place the bastion in. list(string) n/a yes
tags A list of tags to add to all resources. map(string) {} no
vpc_id The bastion host resides in this VPC. string n/a yes

Outputs

Name Description
security_group_id ID of the security group assigned to the bastion host.