├── config.tf ├── container-harden.png ├── outputs.tf ├── files └── assumption-policy.json ├── main.tf ├── CODE_OF_CONDUCT.md ├── infr-config.tf ├── image.tf ├── hardening-pipeline.tfvars ├── sec-groups.tf ├── LICENSE ├── dist-config.tf ├── components.tf ├── recipes.tf ├── variables.tf ├── kms-key.tf ├── trigger-build.tf ├── CONTRIBUTING.md ├── infra-network-config.tf ├── pipeline.tf ├── roles.tf └── README.md /config.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | region = var.aws_region 3 | } -------------------------------------------------------------------------------- /container-harden.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/terraform-ec2-image-builder-container-hardening-pipeline/HEAD/container-harden.png -------------------------------------------------------------------------------- /outputs.tf: -------------------------------------------------------------------------------- 1 | output "container_info" { 2 | description = "Various Container Image attributes." 3 | value = aws_imagebuilder_container_recipe.container_image 4 | sensitive = true 5 | } -------------------------------------------------------------------------------- /files/assumption-policy.json: -------------------------------------------------------------------------------- 1 | { 2 | "Version": "2012-10-17", 3 | "Statement": [ 4 | { 5 | "Effect": "Allow", 6 | "Principal": { 7 | "Service": "ec2.amazonaws.com" 8 | }, 9 | "Action": "sts:AssumeRole" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /main.tf: -------------------------------------------------------------------------------- 1 | locals { 2 | core_tags = { 3 | ManagedBy = "Terraform" 4 | } 5 | 6 | # These can be leveraged to customize your deployment. 7 | kms_admin_role_name = var.hardening_pipeline_role_name 8 | ecr_name = var.ecr_name 9 | } 10 | 11 | data "aws_caller_identity" "current" {} -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /infr-config.tf: -------------------------------------------------------------------------------- 1 | resource "aws_imagebuilder_infrastructure_configuration" "this" { 2 | description = "Container Image Infrastructure configuration" 3 | instance_profile_name = var.ec2_iam_role_name 4 | instance_types = ["t3.micro"] 5 | name = "${var.image_name}-infr" 6 | security_group_ids = [aws_security_group.image_builder_sg.id] 7 | subnet_id = aws_subnet.hardening_pipeline_private.id 8 | terminate_instance_on_failure = true 9 | 10 | logging { 11 | s3_logs { 12 | s3_bucket_name = var.aws_s3_ami_resources_bucket 13 | s3_key_prefix = "image-builder/" 14 | } 15 | } 16 | 17 | tags = local.core_tags 18 | } -------------------------------------------------------------------------------- /image.tf: -------------------------------------------------------------------------------- 1 | resource "aws_imagebuilder_image" "al2_container_latest" { 2 | distribution_configuration_arn = aws_imagebuilder_distribution_configuration.this.arn 3 | container_recipe_arn = aws_imagebuilder_container_recipe.container_image.arn 4 | infrastructure_configuration_arn = aws_imagebuilder_infrastructure_configuration.this.arn 5 | 6 | tags = { 7 | Name = var.image_name 8 | BuiltBy = "hardening-container-pipeline" 9 | } 10 | 11 | depends_on = [ 12 | aws_iam_role.ec2_iam_role, 13 | aws_iam_role.hardening_pipeline_role, 14 | aws_security_group.image_builder_sg, 15 | aws_s3_bucket_object.component_files, 16 | aws_imagebuilder_distribution_configuration.this, 17 | aws_kms_key.this 18 | ] 19 | } -------------------------------------------------------------------------------- /hardening-pipeline.tfvars: -------------------------------------------------------------------------------- 1 | # Enter values for all of the following if you wish to avoid being prompted on each run. 2 | account_id = "" 3 | aws_region = "us-east-1" 4 | vpc_name = "example-hardening-pipeline-vpc" 5 | kms_key_alias = "image-builder-container-key" 6 | ec2_iam_role_name = "example-hardening-instance-role" 7 | hardening_pipeline_role_name = "example-hardening-pipeline-role" 8 | aws_s3_ami_resources_bucket = "example-hardening-ami-resources-bucket-0123" 9 | image_name = "example-hardening-al2-container-image" 10 | ecr_name = "example-hardening-container-repo" 11 | recipe_version = "1.0.0" 12 | ebs_root_vol_size = 10 -------------------------------------------------------------------------------- /sec-groups.tf: -------------------------------------------------------------------------------- 1 | resource "aws_security_group" "image_builder_sg" { 2 | depends_on = [ 3 | aws_vpc.hardening_pipeline 4 | ] 5 | name = "${var.image_name}-sg" 6 | description = "Security group for EC2 Image Builder" 7 | vpc_id = aws_vpc.hardening_pipeline.id 8 | 9 | ingress { 10 | description = "TLS" 11 | from_port = 443 12 | to_port = 443 13 | protocol = "tcp" 14 | cidr_blocks = ["0.0.0.0/0"] 15 | } 16 | 17 | ingress { 18 | description = "Ephemeral" 19 | from_port = 1025 20 | to_port = 65535 21 | protocol = "tcp" 22 | cidr_blocks = ["0.0.0.0/0"] 23 | } 24 | 25 | egress { 26 | description = "Allow all eggress" 27 | from_port = 0 28 | to_port = 0 29 | protocol = "-1" 30 | cidr_blocks = ["0.0.0.0/0"] 31 | ipv6_cidr_blocks = ["::/0"] 32 | } 33 | 34 | tags = local.core_tags 35 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /dist-config.tf: -------------------------------------------------------------------------------- 1 | resource "aws_ecr_repository" "hardening_pipeline_repo" { 2 | name = var.ecr_name 3 | image_tag_mutability = "IMMUTABLE" 4 | 5 | encryption_configuration { 6 | encryption_type = "KMS" 7 | kms_key = aws_kms_key.this.arn 8 | } 9 | 10 | image_scanning_configuration { 11 | scan_on_push = true 12 | } 13 | 14 | force_delete = true 15 | } 16 | 17 | resource "aws_imagebuilder_distribution_configuration" "this" { 18 | # Modify this name if desired 19 | name = "local-distribution" 20 | 21 | distribution { 22 | ami_distribution_configuration { 23 | 24 | ami_tags = { 25 | Name = "${var.image_name}-{{ imagebuilder:buildDate }}" 26 | } 27 | 28 | name = "${var.image_name}-{{ imagebuilder:buildDate }}" 29 | 30 | launch_permission { 31 | user_ids = [var.account_id] 32 | } 33 | 34 | kms_key_id = aws_kms_key.this.arn 35 | } 36 | region = var.aws_region 37 | } 38 | } -------------------------------------------------------------------------------- /components.tf: -------------------------------------------------------------------------------- 1 | # Upload files to S3 2 | resource "aws_s3_bucket_object" "component_files" { 3 | depends_on = [ 4 | aws_s3_bucket.s3_pipeline_bucket, 5 | aws_kms_key.this 6 | ] 7 | 8 | for_each = fileset(path.module, "files/**/*.yml") 9 | 10 | bucket = var.aws_s3_ami_resources_bucket 11 | key = each.value 12 | source = "${path.module}/${each.value}" 13 | kms_key_id = aws_kms_key.this.id 14 | } 15 | 16 | /* Add custom component resources below 17 | The YAML file referenced in the URI attribute must exist in the files/ directory 18 | Below is an example component. */ 19 | /* resource "aws_imagebuilder_component" "example_custom_component" { 20 | name = "example-custom-component" 21 | platform = "Linux" 22 | uri = "s3://${var.aws_s3_ami_resources_bucket}/files/example-custom-component.yml" 23 | version = "1.0.0" 24 | kms_key_id = aws_kms_key.this.arn 25 | 26 | depends_on = [ 27 | aws_s3_bucket_object.component_files, 28 | aws_kms_key.this 29 | ] 30 | } */ -------------------------------------------------------------------------------- /recipes.tf: -------------------------------------------------------------------------------- 1 | resource "aws_imagebuilder_container_recipe" "container_image" { 2 | 3 | depends_on = [ 4 | aws_ecr_repository.hardening_pipeline_repo 5 | ] 6 | 7 | name = var.image_name 8 | version = "1.0.0" 9 | 10 | container_type = "DOCKER" 11 | parent_image = "amazonlinux:latest" 12 | working_directory = "/tmp" 13 | 14 | target_repository { 15 | repository_name = var.ecr_name 16 | service = "ECR" 17 | } 18 | 19 | instance_configuration { 20 | 21 | block_device_mapping { 22 | device_name = "/dev/xvda" 23 | 24 | ebs { 25 | delete_on_termination = true 26 | volume_size = var.ebs_root_vol_size 27 | volume_type = "gp2" 28 | encrypted = true 29 | kms_key_id = aws_kms_key.this.arn 30 | } 31 | } 32 | 33 | } 34 | 35 | component { 36 | component_arn = "arn:aws:imagebuilder:${var.aws_region}:aws:component/update-linux/x.x.x" 37 | } 38 | 39 | component { 40 | component_arn = "arn:aws:imagebuilder:${var.aws_region}:aws:component/stig-build-linux-medium/x.x.x" 41 | } 42 | 43 | # Add more component ARNs here to customize the recipe 44 | # You can also add custom components if you defined any in components.tf 45 | /* component { 46 | component_arn = aws_imagebuilder_component.example_custom_component.arn 47 | } */ 48 | 49 | dockerfile_template_data = < 0 56 | error_message = "Parameter `aws_s3_ami_resources_bucket` cannot start and end with \"/\", as well as cannot be empty." 57 | } 58 | } 59 | 60 | variable "ebs_root_vol_size" { 61 | type = number 62 | description = "Enter the size (in gigabytes) of the EBS Root Volume." 63 | } 64 | 65 | variable "kms_key_alias" { 66 | type = string 67 | description = "Enter the KMS Key name to be used by the image builder infrastructure configuration." 68 | } -------------------------------------------------------------------------------- /kms-key.tf: -------------------------------------------------------------------------------- 1 | /* As this is intended to enable a Key Administrator in a multi-account structure 2 | the action and resource definition is broad */ 3 | data "aws_iam_policy_document" "this" { 4 | statement { 5 | sid = "Enable IAM User Permissions" 6 | effect = "Allow" 7 | actions = ["kms:*"] 8 | resources = ["*"] 9 | 10 | principals { 11 | type = "AWS" 12 | identifiers = ["arn:aws:iam::${var.account_id}:root"] 13 | } 14 | } 15 | 16 | statement { 17 | sid = "Allow access for Key Administrators" 18 | effect = "Allow" 19 | actions = ["kms:*"] 20 | resources = ["*"] 21 | 22 | principals { 23 | type = "AWS" 24 | identifiers = [ 25 | "arn:aws:iam::${var.account_id}:role/${local.kms_admin_role_name}" 26 | ] 27 | } 28 | } 29 | 30 | statement { 31 | sid = "Allow use of the key" 32 | effect = "Allow" 33 | actions = [ 34 | "kms:Encrypt", 35 | "kms:Decrypt", 36 | "kms:ReEncrypt", 37 | "kms:GenerateDataKey", 38 | "kms:DescribeKey", 39 | "kms:CreateGrant" 40 | ] 41 | resources = ["*"] 42 | 43 | principals { 44 | type = "AWS" 45 | identifiers = [ 46 | "arn:aws:iam::${var.account_id}:role/${local.kms_admin_role_name}" 47 | ] 48 | } 49 | } 50 | 51 | statement { 52 | sid = "Allow attachment of persistent resources" 53 | effect = "Allow" 54 | actions = [ 55 | "kms:CreateGrant", 56 | "kms:ListGrants", 57 | "kms:RevokeGrant" 58 | ] 59 | resources = ["*"] 60 | 61 | principals { 62 | type = "AWS" 63 | identifiers = [ 64 | "arn:aws:iam::${var.account_id}:role/${local.kms_admin_role_name}" 65 | ] 66 | } 67 | 68 | condition { 69 | test = "Bool" 70 | variable = "kms:GrantIsForAWSResource" 71 | values = ["true"] 72 | } 73 | } 74 | } 75 | 76 | # Creates and manages KMS CMK 77 | resource "aws_kms_key" "this" { 78 | description = "EC2 Image Builder key" 79 | is_enabled = true 80 | enable_key_rotation = true 81 | tags = local.core_tags 82 | policy = data.aws_iam_policy_document.this.json 83 | deletion_window_in_days = 30 84 | } 85 | 86 | # Add an alias to the key 87 | resource "aws_kms_alias" "this" { 88 | name = "alias/${var.kms_key_alias}" 89 | target_key_id = aws_kms_key.this.key_id 90 | } -------------------------------------------------------------------------------- /trigger-build.tf: -------------------------------------------------------------------------------- 1 | resource "aws_sqs_queue" "container_build_queue" { 2 | name = "hardened-container-build-queue" 3 | sqs_managed_sse_enabled = true 4 | delay_seconds = 90 5 | max_message_size = 2048 6 | message_retention_seconds = 86400 7 | receive_wait_time_seconds = 10 8 | } 9 | 10 | resource "aws_cloudwatch_event_rule" "new_image_push" { 11 | name = "new-hardened-container-build-push" 12 | description = "New hardened container image successful push event rule." 13 | 14 | event_pattern = < 125 | AWS Secret Access Key [**************xxxx]: 126 | Default region name: [us-east-1]: 127 | Default output format [None]: 128 | ``` 129 | 3. Clone the repository with HTTPS or SSH 130 | 131 | _HTTPS_ 132 | ``` bash 133 | git clone https://github.com/aws-samples/terraform-ec2-image-builder-container-hardening-pipeline.git 134 | ``` 135 |          _SSH_ 136 | 137 | ``` bash 138 | git clone git@github.com:aws-samples/terraform-ec2-image-builder-container-hardening-pipeline.git 139 | ``` 140 | 4. Navigate to the directory containing this solution before running the commands below: 141 | ``` bash 142 | cd terraform-ec2-image-builder-container-hardening-pipeline 143 | ``` 144 | 145 | 5. Update the placeholder variable values in hardening-pipeline.tfvars. You must provide your own `account_id`, `kms_key_alias`, and `aws_s3_ami_resources_bucket`, however, you should also modify the rest of the placeholder variables to match your environment and your desired configuration. 146 | ``` properties 147 | account_id = "" 148 | aws_region = "us-east-1" 149 | vpc_name = "example-hardening-pipeline-vpc" 150 | kms_key_alias = "image-builder-container-key" 151 | ec2_iam_role_name = "example-hardening-instance-role" 152 | hardening_pipeline_role_name = "example-hardening-pipeline-role" 153 | aws_s3_ami_resources_bucket = "example-hardening-ami-resources-bucket-name" 154 | image_name = "example-hardening-al2-container-image" 155 | ecr_name = "example-hardening-container-repo" 156 | recipe_version = "1.0.0" 157 | ebs_root_vol_size = 10 158 | ``` 159 | 160 | 6. The following command initializes, validates and applies the terraform modules to the environment using the variables defined in your .tfvars file: 161 | ``` bash 162 | terraform init && terraform validate && terraform apply -var-file *.tfvars -auto-approve 163 | ``` 164 | 165 | 7. After successful completion of your first Terraform apply, if provisioning locally, you should see this snippet in your local machine’s terminal: 166 | ``` bash 167 | Apply complete! Resources: 43 added, 0 changed, 0 destroyed. 168 | ``` 169 | 170 | 8. *(Optional)* Teardown the infrastructure with the following command: 171 | ``` bash 172 | terraform init && terraform validate && terraform destroy -var-file *.tfvars -auto-approve 173 | ``` 174 | 175 | ## Troubleshooting 176 | 177 | When running Terraform apply or destroy commands from your local machine, you may encounter an error similar to the following: 178 | 179 | ``` properties 180 | Error: configuring Terraform AWS Provider: error validating provider credentials: error calling sts:GetCallerIdentity: operation error STS: GetCallerIdentity, https response error StatusCode: 403, RequestID: 123456a9-fbc1-40ed-b8d8-513d0133ba7f, api error InvalidClientTokenId: The security token included in the request is invalid. 181 | ``` 182 | 183 | This error is due to the expiration of the security token for the credentials used in your local machine’s configuration. 184 | 185 | See "[Set and View Configuration Settings](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-methods)" from the AWS Command Line Interface Documentation to resolve. 186 | 187 | ## Author 188 | 189 | * Mike Saintcross [@msntx](https://github.com/msntx) --------------------------------------------------------------------------------