diff --git a/README.md b/README.md index d6821bd..df041dd 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,7 @@ This project creates and manages resources within an AWS account for infrastruct | [aws_iam_role_policy_attachment.infrastructure_rds_s3_backups_task_s3_list](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_role_policy_attachment.infrastructure_rds_s3_backups_task_s3_write](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_iam_user.infrastructure_ecs_cluster_service_build_pipeline_buildspec_store](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user) | resource | +| [aws_instance.infrastructure_bastion](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance) | resource | | [aws_internet_gateway.infrastructure_public](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/internet_gateway) | resource | | [aws_kms_alias.custom_s3_buckets](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource | | [aws_kms_alias.infrastructure](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource | @@ -371,12 +372,19 @@ This project creates and manages resources within an AWS account for infrastruct | [aws_s3_object.infrastructure_ecs_cluster_service_build_pipeline_buildspec_store_files](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_object) | resource | | [aws_secretsmanager_secret.infrastructure_rds_root_password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource | | [aws_secretsmanager_secret_version.infrastructure_rds_root_password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version) | resource | +| [aws_security_group.infrastructure_ec2_bastion_host](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group.infrastructure_ecs_cluster_container_instances](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group.infrastructure_ecs_cluster_efs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group.infrastructure_ecs_cluster_service_alb](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group.infrastructure_elasticache](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group.infrastructure_rds](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | | [aws_security_group.infrastructure_rds_s3_backups_scheduled_task](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource | +| [aws_security_group_rule.infrastructure_ec2_bastion_host_custom](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.infrastructure_ec2_bastion_host_egress_dns_tcp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.infrastructure_ec2_bastion_host_egress_dns_udp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.infrastructure_ec2_bastion_host_egress_https_tcp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.infrastructure_ec2_bastion_host_egress_https_udp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | +| [aws_security_group_rule.infrastructure_ec2_bastion_host_egress_rds](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | | [aws_security_group_rule.infrastructure_ecs_cluster_container_instances_custom](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | | [aws_security_group_rule.infrastructure_ecs_cluster_container_instances_egress_dns_tcp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | | [aws_security_group_rule.infrastructure_ecs_cluster_container_instances_egress_dns_udp](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource | @@ -421,6 +429,7 @@ This project creates and manages resources within an AWS account for infrastruct | [archive_file.ecs_cluster_infrastructure_draining_lambda](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | | [archive_file.ecs_cluster_infrastructure_ecs_asg_diff_metric_lambda](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | | [archive_file.ecs_cluster_infrastructure_pending_task_metric_lambda](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source | +| [aws_ami.bastion_ami](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | | [aws_ami.ecs_cluster_ami](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ami) | data source | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_cloudfront_cache_policy.managed_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/cloudfront_cache_policy) | data source | @@ -447,6 +456,7 @@ This project creates and manages resources within an AWS account for infrastruct | [ecs\_cluster\_efs\_performance\_mode](#input\_ecs\_cluster\_efs\_performance\_mode) | ECS cluser EFS performance mode | `string` | n/a | yes | | [ecs\_cluster\_efs\_throughput\_mode](#input\_ecs\_cluster\_efs\_throughput\_mode) | ECS cluser EFS throughput mode | `string` | n/a | yes | | [enable\_cloudformatian\_s3\_template\_store](#input\_enable\_cloudformatian\_s3\_template\_store) | Creates an S3 bucket to store custom CloudFormation templates, which can then be referenced in `custom_cloudformation_stacks`. A user with RW access to the bucket is also created. | `bool` | n/a | yes | +| [enable\_infrastructure\_bastion\_host](#input\_enable\_infrastructure\_bastion\_host) | Enable Infrastructure Bastion host. This launches a t3.micro AL2023 instance within the VPC that can be accessed via Session Manager | `bool` | n/a | yes | | [enable\_infrastructure\_ecs\_cluster](#input\_enable\_infrastructure\_ecs\_cluster) | Enable creation of infrastructure ECS cluster, to place ECS services | `bool` | n/a | yes | | [enable\_infrastructure\_ecs\_cluster\_asg\_cpu\_alert](#input\_enable\_infrastructure\_ecs\_cluster\_asg\_cpu\_alert) | Enable a CPU alert for the ECS cluster's Autoscaling Group | `bool` | n/a | yes | | [enable\_infrastructure\_ecs\_cluster\_ecs\_asg\_diff\_alert](#input\_enable\_infrastructure\_ecs\_cluster\_ecs\_asg\_diff\_alert) | Enable the ECS Cluster Container Instance / ASG instance diff alert | `bool` | n/a | yes | @@ -457,6 +467,7 @@ This project creates and manages resources within an AWS account for infrastruct | [enable\_infrastructure\_route53\_hosted\_zone](#input\_enable\_infrastructure\_route53\_hosted\_zone) | Creates a Route53 hosted zone, where DNS records will be created for resources launched within this module. | `bool` | n/a | yes | | [enable\_infrastructure\_vpc\_transfer\_s3\_bucket](#input\_enable\_infrastructure\_vpc\_transfer\_s3\_bucket) | Enable VPC transfer S3 bucket. This allows uploading/downloading files from resources within the infrastructure VPC | `bool` | n/a | yes | | [environment](#input\_environment) | The environment name to be used as part of the resource prefix | `string` | n/a | yes | +| [infrastructure\_bastion\_host\_custom\_security\_group\_rules](#input\_infrastructure\_bastion\_host\_custom\_security\_group\_rules) | Map of custom security group rules to add to the Infrastructure EC2 Bastion Host security group (eg. { rule-name = {type = "egress", ... } }) |
map(object({
description = string
type = string
from_port = number
to_port = number
protocol = string
source_security_group_id = optional(string, "")
cidr_blocks = optional(list(string), [])
}))
| n/a | yes | | [infrastructure\_dockerhub\_email](#input\_infrastructure\_dockerhub\_email) | Dockerhub email | `string` | n/a | yes | | [infrastructure\_dockerhub\_token](#input\_infrastructure\_dockerhub\_token) | Dockerhub token which has permissions to pull images | `string` | n/a | yes | | [infrastructure\_dockerhub\_username](#input\_infrastructure\_dockerhub\_username) | Dockerhub username | `string` | n/a | yes | diff --git a/data.tf b/data.tf index 6f33b6d..bace70c 100644 --- a/data.tf +++ b/data.tf @@ -31,6 +31,27 @@ data "aws_ami" "ecs_cluster_ami" { } } +data "aws_ami" "bastion_ami" { + count = local.enable_infrastructure_bastion_host ? 1 : 0 + + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = [ + "al2023-ami-2023*" + ] + } + + filter { + name = "architecture" + values = [ + "x86_64" + ] + } +} + data "aws_sns_topic" "infrastructure_slack_sns_topic" { count = local.infrastructure_slack_sns_topic_in_use ? 1 : 0 diff --git a/ec2-infrastructure-bastion-host-security-group.tf b/ec2-infrastructure-bastion-host-security-group.tf new file mode 100644 index 0000000..45d4417 --- /dev/null +++ b/ec2-infrastructure-bastion-host-security-group.tf @@ -0,0 +1,90 @@ +resource "aws_security_group" "infrastructure_ec2_bastion_host" { + count = local.enable_infrastructure_bastion_host ? 1 : 0 + + name = "${local.resource_prefix}-infrastructure-ec2-bastion-host" + description = "Infrastructure EC2 Bastion Host" + vpc_id = aws_vpc.infrastructure[0].id +} + +resource "aws_security_group_rule" "infrastructure_ec2_bastion_host_egress_https_tcp" { + count = local.enable_infrastructure_bastion_host ? 1 : 0 + + description = "Allow HTTPS tcp outbound" + type = "egress" + from_port = 443 + to_port = 443 + protocol = "tcp" + # tfsec:ignore:aws-ec2-no-public-egress-sgr + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.infrastructure_ec2_bastion_host[0].id +} + +resource "aws_security_group_rule" "infrastructure_ec2_bastion_host_egress_https_udp" { + count = local.enable_infrastructure_bastion_host ? 1 : 0 + + description = "Allow HTTPS udp outbound" + type = "egress" + from_port = 443 + to_port = 443 + protocol = "udp" + # tfsec:ignore:aws-ec2-no-public-egress-sgr + cidr_blocks = ["0.0.0.0/0"] + security_group_id = aws_security_group.infrastructure_ec2_bastion_host[0].id +} + +resource "aws_security_group_rule" "infrastructure_ec2_bastion_host_egress_dns_tcp" { + count = local.enable_infrastructure_bastion_host ? 1 : 0 + + description = "Allow DNS tcp outbound to AWS" + type = "egress" + from_port = 53 + to_port = 53 + protocol = "tcp" + cidr_blocks = local.infrastructure_ecs_cluster_publicly_avaialble ? [ + for subnet in aws_subnet.infrastructure_public : subnet.cidr_block + ] : [ + for subnet in aws_subnet.infrastructure_private : subnet.cidr_block + ] + security_group_id = aws_security_group.infrastructure_ec2_bastion_host[0].id +} + +resource "aws_security_group_rule" "infrastructure_ec2_bastion_host_egress_dns_udp" { + count = local.enable_infrastructure_bastion_host ? 1 : 0 + + description = "Allow DNS udp outbound to AWS" + type = "egress" + from_port = 53 + to_port = 53 + protocol = "udp" + cidr_blocks = local.infrastructure_ecs_cluster_publicly_avaialble ? [ + for subnet in aws_subnet.infrastructure_public : subnet.cidr_block + ] : [ + for subnet in aws_subnet.infrastructure_private : subnet.cidr_block + ] + security_group_id = aws_security_group.infrastructure_ec2_bastion_host[0].id +} + +resource "aws_security_group_rule" "infrastructure_ec2_bastion_host_egress_rds" { + for_each = local.enable_infrastructure_bastion_host ? local.infrastructure_rds : {} + + description = "Allow ${each.value["engine"]} tcp outbound to RDS security group" + type = "egress" + from_port = local.rds_ports[each.value["engine"]] + to_port = local.rds_ports[each.value["engine"]] + protocol = "tcp" + source_security_group_id = aws_security_group.infrastructure_rds[each.key].id + security_group_id = aws_security_group.infrastructure_ec2_bastion_host[0].id +} + +resource "aws_security_group_rule" "infrastructure_ec2_bastion_host_custom" { + for_each = local.enable_infrastructure_bastion_host ? local.infrastructure_bastion_host_custom_security_group_rules : {} + + description = each.value["description"] + type = each.value["type"] + from_port = each.value["from_port"] + to_port = each.value["to_port"] + protocol = each.value["protocol"] + source_security_group_id = each.value["source_security_group_id"] != "" ? each.value["source_security_group_id"] : null + cidr_blocks = length(each.value["cidr_blocks"]) > 0 ? each.value["cidr_blocks"] : null + security_group_id = aws_security_group.infrastructure_ec2_bastion_host[0].id +} diff --git a/ec2-infrastructure-bastion-host.tf b/ec2-infrastructure-bastion-host.tf new file mode 100644 index 0000000..03f586a --- /dev/null +++ b/ec2-infrastructure-bastion-host.tf @@ -0,0 +1,22 @@ +resource "aws_instance" "infrastructure_bastion" { + count = local.enable_infrastructure_bastion_host ? 1 : 0 + + ami = data.aws_ami.bastion_ami[0].id + instance_type = "t3.micro" + subnet_id = local.infrastructure_vpc_network_enable_private ? [for subnet in aws_subnet.infrastructure_private : subnet.id][0] : local.infrastructure_vpc_network_enable_public ? [for subnet in aws_subnet.infrastructure_public : subnet.id][0] : null + user_data_base64 = base64encode(templatefile("${path.root}/ec2-userdata/bastion.tpl", {})) + vpc_security_group_ids = [aws_security_group.infrastructure_ec2_bastion_host[0].id] + + root_block_device { + encrypted = true + } + + metadata_options { + http_endpoint = "enabled" + http_tokens = "required" + } + + tags = { + Name = "${local.resource_prefix}-infrastructure-bastion" + } +} diff --git a/ec2-userdata/bastion.tpl b/ec2-userdata/bastion.tpl new file mode 100644 index 0000000..37f1605 --- /dev/null +++ b/ec2-userdata/bastion.tpl @@ -0,0 +1,14 @@ +#!/bin/bash + +# Install useful packages +sudo yum update --security -y + +if ! command -v aws &> /dev/null +then + sudo yum install -y aws-cli +fi + +sudo yum install -y \ + jq \ + rsync \ + vim diff --git a/locals.tf b/locals.tf index 243291c..40f8490 100644 --- a/locals.tf +++ b/locals.tf @@ -96,6 +96,9 @@ locals { infrastructure_vpc_transfer_ssm_download_command = "aws s3 cp {{ Source }} {{ HostTarget }} {{ Recursive }}; if [ -n \\\"{{ TargetUID }}\\\" ] && [ -n \\\"{{ TargetGID }}\\\" ]; then chown {{ TargetUID }}:{{ TargetGID }} -R {{ HostTarget }}; fi" infrastructure_vpc_transfer_ssm_upload_command = "aws s3 cp {{ Source }} {{ S3Target }} {{ Recursive }}" + enable_infrastructure_bastion_host = var.enable_infrastructure_bastion_host + infrastructure_bastion_host_custom_security_group_rules = var.infrastructure_bastion_host_custom_security_group_rules + infrastructure_dockerhub_email = var.infrastructure_dockerhub_email infrastructure_dockerhub_username = var.infrastructure_dockerhub_username infrastructure_dockerhub_token = var.infrastructure_dockerhub_token diff --git a/variables.tf b/variables.tf index 039add4..568b16f 100644 --- a/variables.tf +++ b/variables.tf @@ -204,6 +204,24 @@ variable "infrastructure_vpc_transfer_s3_bucket_access_vpc_ids" { type = list(string) } +variable "enable_infrastructure_bastion_host" { + description = "Enable Infrastructure Bastion host. This launches a t3.micro AL2023 instance within the VPC that can be accessed via Session Manager" + type = bool +} + +variable "infrastructure_bastion_host_custom_security_group_rules" { + description = "Map of custom security group rules to add to the Infrastructure EC2 Bastion Host security group (eg. { rule-name = {type = \"egress\", ... } })" + type = map(object({ + description = string + type = string + from_port = number + to_port = number + protocol = string + source_security_group_id = optional(string, "") + cidr_blocks = optional(list(string), []) + })) +} + variable "route53_root_hosted_zone_domain_name" { description = "Route53 Hosted Zone in which to delegate Infrastructure Route53 Hosted Zones." type = string