From 26199725ccf50b376e72086c5dfab33ed275ae33 Mon Sep 17 00:00:00 2001 From: Andy Price Date: Fri, 9 Feb 2024 14:12:21 +0000 Subject: [PATCH] VEGA-2289 - S3 Storage & Replication #minor (#103) * VEGA-2289 - Add local localstack config and local lpa-store-static bucket #minor * VEGA-2289 - Add initial cross region S3 setup #minor * VEGA-2289 - Remove redundant keys and sort permissions for cross region replication #minor * VEGA-2289 - Add Accces Logging to ccount Buckets #minor * VEGA-2289 - Add Cross Account S3 Backup #minor * VEGA-2289 - Grant Lambda Roles S3 & KMS Permissions for Object Put #minor * VEGA-2289 - Move lambda policy rather than destroy/create #patch * VEGA-2289 - Remove Life Cycle configuration from S3 Modules as we want to keep everything #minor --- docker-compose.yml | 23 ++- localstack/init/localstack_init.sh | 33 ++++ localstack/wait/healthcheck.sh | 6 + terraform/environment/.envrc | 1 + terraform/environment/data_sources.tf | 3 + terraform/environment/region/iam.tf | 70 ++++++++ .../environment/region/{main.tf => lambda.tf} | 24 +-- terraform/environment/region/outputs.tf | 6 + terraform/environment/region/variables.tf | 28 +-- terraform/environment/regions.tf | 36 ++-- terraform/environment/s3.tf | 89 ++++++++++ terraform/environment/terraform.tf | 14 ++ terraform/environment/variables.tf | 9 +- terraform/modules/lambda/outputs.tf | 6 +- .../s3_cross_account_backup/data_sources.tf | 11 ++ .../modules/s3_cross_account_backup/iam.tf | 74 ++++++++ .../modules/s3_cross_account_backup/kms.tf | 39 +++++ .../s3_cross_account_backup/outputs.tf | 7 + .../modules/s3_cross_account_backup/s3.tf | 128 ++++++++++++++ .../s3_cross_account_backup/terraform.tf | 13 ++ .../s3_cross_account_backup/variables.tf | 26 +++ .../data_sources.tf | 3 + .../iam_replication.tf | 79 +++++++++ .../s3_multi_region_replica_bucket/kms.tf | 71 ++++++++ .../s3_multi_region_replica_bucket/outputs.tf | 7 + .../s3_multi_region_replica_bucket/s3.tf | 162 ++++++++++++++++++ .../terraform.tf | 9 + .../variables.tf | 49 ++++++ 28 files changed, 968 insertions(+), 58 deletions(-) create mode 100755 localstack/init/localstack_init.sh create mode 100644 localstack/wait/healthcheck.sh create mode 100644 terraform/environment/data_sources.tf create mode 100644 terraform/environment/region/iam.tf rename terraform/environment/region/{main.tf => lambda.tf} (54%) create mode 100644 terraform/environment/s3.tf create mode 100644 terraform/modules/s3_cross_account_backup/data_sources.tf create mode 100644 terraform/modules/s3_cross_account_backup/iam.tf create mode 100644 terraform/modules/s3_cross_account_backup/kms.tf create mode 100644 terraform/modules/s3_cross_account_backup/outputs.tf create mode 100644 terraform/modules/s3_cross_account_backup/s3.tf create mode 100644 terraform/modules/s3_cross_account_backup/terraform.tf create mode 100644 terraform/modules/s3_cross_account_backup/variables.tf create mode 100644 terraform/modules/s3_multi_region_replica_bucket/data_sources.tf create mode 100644 terraform/modules/s3_multi_region_replica_bucket/iam_replication.tf create mode 100644 terraform/modules/s3_multi_region_replica_bucket/kms.tf create mode 100644 terraform/modules/s3_multi_region_replica_bucket/outputs.tf create mode 100644 terraform/modules/s3_multi_region_replica_bucket/s3.tf create mode 100644 terraform/modules/s3_multi_region_replica_bucket/terraform.tf create mode 100644 terraform/modules/s3_multi_region_replica_bucket/variables.tf diff --git a/docker-compose.yml b/docker-compose.yml index c55def4d..1d8ca540 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -8,7 +8,7 @@ services: - "8000:8000" lambda-create: - depends_on: [ddb] + depends_on: [ ddb ] build: context: . dockerfile: ./lambda/Dockerfile @@ -27,7 +27,7 @@ services: entrypoint: /aws-lambda/aws-lambda-rie /var/task/main lambda-update: - depends_on: [ddb] + depends_on: [ ddb ] build: context: . dockerfile: ./lambda/Dockerfile @@ -46,7 +46,7 @@ services: entrypoint: /aws-lambda/aws-lambda-rie /var/task/main lambda-get: - depends_on: [ddb] + depends_on: [ ddb ] build: context: . dockerfile: ./lambda/Dockerfile @@ -65,7 +65,7 @@ services: entrypoint: /aws-lambda/aws-lambda-rie /var/task/main apigw: - depends_on: [lambda-create, lambda-update, lambda-get] + depends_on: [ lambda-create, lambda-update, lambda-get ] build: context: . dockerfile: ./mock-apigw/Dockerfile @@ -73,7 +73,7 @@ services: - 9000:8080 aws: - depends_on: [ddb] + depends_on: [ ddb ] image: amazon/aws-cli:latest environment: AWS_ENDPOINT_URL: http://ddb:8000/ @@ -82,6 +82,19 @@ services: AWS_SECRET_ACCESS_KEY: X AWS_PAGER: "" + localstack: + image: localstack/localstack:3.0 + volumes: + - "./localstack/init:/etc/localstack/init/ready.d" + - "./localstack/wait:/scripts/wait" + environment: + AWS_DEFAULT_REGION: eu-west-1 + healthcheck: + test: bash /scripts/wait/healthcheck.sh + interval: 10s + timeout: 10s + retries: 50 + go-lint: image: golangci/golangci-lint:v1.55.2 working_dir: /go/src/app diff --git a/localstack/init/localstack_init.sh b/localstack/init/localstack_init.sh new file mode 100755 index 00000000..81f196f3 --- /dev/null +++ b/localstack/init/localstack_init.sh @@ -0,0 +1,33 @@ +#! /usr/bin/env bash +create_bucket() { + BUCKET=$1 + # Create Private Bucket + awslocal s3api create-bucket \ + --acl private \ + --region eu-west-1 \ + --create-bucket-configuration LocationConstraint=eu-west-1 \ + --bucket "$BUCKET" + + # Add Public Access Block + awslocal s3api put-public-access-block \ + --public-access-block-configuration "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true" \ + --bucket "$BUCKET" + + # Add Default Encryption + awslocal s3api put-bucket-encryption \ + --bucket "$BUCKET" \ + --server-side-encryption-configuration '{ "Rules": [ { "ApplyServerSideEncryptionByDefault": { "SSEAlgorithm": "AES256" } } ] }' + + # Add Encryption Policy + awslocal s3api put-bucket-policy \ + --policy '{ "Statement": [ { "Sid": "DenyUnEncryptedObjectUploads", "Effect": "Deny", "Principal": { "AWS": "*" }, "Action": "s3:PutObject", "Resource": "arn:aws:s3:::'${BUCKET}'/*", "Condition": { "StringNotEquals": { "s3:x-amz-server-side-encryption": "AES256" } } }, { "Sid": "DenyUnEncryptedObjectUploads", "Effect": "Deny", "Principal": { "AWS": "*" }, "Action": "s3:PutObject", "Resource": "arn:aws:s3:::'${BUCKET}'/*", "Condition": { "Bool": { "aws:SecureTransport": false } } } ] }' \ + --bucket "$BUCKET" + + # Add Bucket Versioning + awslocal s3api put-bucket-versioning \ + --versioning-configuration '{ "MFADelete": "Disabled", "Status": "Enabled" }' \ + --bucket "$BUCKET" +} + +# S3 +create_bucket "opg-lpa-store-static-eu-west-1" diff --git a/localstack/wait/healthcheck.sh b/localstack/wait/healthcheck.sh new file mode 100644 index 00000000..e054f08c --- /dev/null +++ b/localstack/wait/healthcheck.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# S3 +buckets=$(awslocal s3 ls) + +echo $buckets | grep "opg-lpa-store-static-eu-west-1" || exit 1 diff --git a/terraform/environment/.envrc b/terraform/environment/.envrc index 1ce1470c..031b2b1d 100644 --- a/terraform/environment/.envrc +++ b/terraform/environment/.envrc @@ -1,5 +1,6 @@ # Terraform export TF_WORKSPACE=development +export TF_VAR_app_version=latest export TF_VAR_default_role=operator export TF_VAR_management_role=operator diff --git a/terraform/environment/data_sources.tf b/terraform/environment/data_sources.tf new file mode 100644 index 00000000..2b54043b --- /dev/null +++ b/terraform/environment/data_sources.tf @@ -0,0 +1,3 @@ +data "aws_caller_identity" "current" { + provider = aws.eu_west_1 +} diff --git a/terraform/environment/region/iam.tf b/terraform/environment/region/iam.tf new file mode 100644 index 00000000..ee139027 --- /dev/null +++ b/terraform/environment/region/iam.tf @@ -0,0 +1,70 @@ +moved { + from = aws_iam_role_policy.lambda + to = aws_iam_role_policy.lambda_dynamodb +} + +resource "aws_iam_role_policy" "lambda_dynamodb" { + for_each = local.functions + name = "LambdaAllowDynamoDB" + role = module.lambda[each.key].iam_role.id + policy = data.aws_iam_policy_document.lambda_dynamodb_policy.json + provider = aws.region +} + +data "aws_iam_policy_document" "lambda_dynamodb_policy" { + statement { + sid = "allowDynamoDB" + effect = "Allow" + resources = [var.dynamodb_arn] + actions = [ + "dynamodb:PutItem", + "dynamodb:GetItem", + ] + } +} + +resource "aws_iam_role_policy" "lambda_s3" { + for_each = local.functions + name = "LambdaAllowS3" + role = module.lambda[each.key].iam_role.id + policy = data.aws_iam_policy_document.lambda_s3_policy.json + provider = aws.region +} + +data "aws_iam_policy_document" "lambda_s3_policy" { + statement { + sid = "allowS3Access" + effect = "Allow" + resources = [ + var.lpa_store_static_bucket.arn, + "${var.lpa_store_static_bucket.arn}/*", + ] + actions = [ + "s3:PutObject", + ] + } + statement { + sid = "allowS3KMS" + effect = "Allow" + resources = [var.lpa_store_static_bucket_kms_key.arn] + actions = [ + "kms:GenerateDataKey", + "kms:Encrypt" + ] + + condition { + test = "StringLike" + variable = "kms:ViaService" + values = ["s3.${data.aws_region.current.name}.amazonaws.com"] + } + + condition { + test = "StringLike" + variable = "kms:EncryptionContext:aws:s3:arn" + values = [ + "${var.lpa_store_static_bucket.arn}/*", + ] + } + } +} + diff --git a/terraform/environment/region/main.tf b/terraform/environment/region/lambda.tf similarity index 54% rename from terraform/environment/region/main.tf rename to terraform/environment/region/lambda.tf index 5cf739e6..614c9ef3 100644 --- a/terraform/environment/region/main.tf +++ b/terraform/environment/region/lambda.tf @@ -16,9 +16,9 @@ module "lambda" { cloudwatch_kms_key_id = aws_kms_key.cloudwatch.arn environment_variables = { - DDB_TABLE_NAME_DEEDS = var.dynamodb_name + DDB_TABLE_NAME_DEEDS = var.dynamodb_name DDB_TABLE_NAME_CHANGES = var.dynamodb_name_changes - JWT_SECRET_KEY = "secret" + JWT_SECRET_KEY = "secret" } providers = { @@ -31,23 +31,3 @@ data "aws_ecr_repository" "lambda" { name = "lpa-store/lambda/api-${each.key}" provider = aws.management } - -resource "aws_iam_role_policy" "lambda" { - for_each = local.functions - name = "LambdaAllowDynamoDB" - role = module.lambda[each.key].iam_role_id - policy = data.aws_iam_policy_document.lambda_access_ddb.json - provider = aws.region -} - -data "aws_iam_policy_document" "lambda_access_ddb" { - statement { - sid = "allowDynamoDB" - effect = "Allow" - resources = [var.dynamodb_arn] - actions = [ - "dynamodb:PutItem", - "dynamodb:GetItem", - ] - } -} diff --git a/terraform/environment/region/outputs.tf b/terraform/environment/region/outputs.tf index 83d052f6..f595a2cd 100644 --- a/terraform/environment/region/outputs.tf +++ b/terraform/environment/region/outputs.tf @@ -1,3 +1,9 @@ output "base_url" { value = "https://${local.domain_name}" } + +output "lambda_iam_roles" { + value = [ + for lambda in module.lambda : lambda.iam_role + ] +} diff --git a/terraform/environment/region/variables.tf b/terraform/environment/region/variables.tf index c2956ec8..4fad8668 100644 --- a/terraform/environment/region/variables.tf +++ b/terraform/environment/region/variables.tf @@ -1,6 +1,6 @@ -variable "environment_name" { - description = "The name of the environment the region is deployed to" - type = string +variable "allowed_arns" { + description = "List of external ARNs allowed to access the API Gateway" + type = list(string) } variable "app_version" { @@ -8,6 +8,12 @@ variable "app_version" { type = string } +variable "dns_weighting" { + description = "What percentage of DNS traffic to send to this region" + type = number + default = 50 +} + variable "dynamodb_arn" { description = "ARN of DynamoDB table" type = string @@ -28,13 +34,15 @@ variable "dynamodb_name_changes" { type = string } -variable "allowed_arns" { - description = "List of external ARNs allowed to access the API Gateway" - type = list(string) +variable "environment_name" { + description = "The name of the environment the region is deployed to" + type = string } -variable "dns_weighting" { - description = "What percentage of DNS traffic to send to this region" - type = number - default = 50 +variable "lpa_store_static_bucket" { + description = "LPA Store Static bucket object for the region" +} + +variable "lpa_store_static_bucket_kms_key" { + description = "LPA Store Static bucket KMS Key object for the region" } diff --git a/terraform/environment/regions.tf b/terraform/environment/regions.tf index c549f8f9..ba74e03c 100644 --- a/terraform/environment/regions.tf +++ b/terraform/environment/regions.tf @@ -1,14 +1,16 @@ module "eu_west_1" { source = "./region" - app_version = var.app_version - dynamodb_arn = aws_dynamodb_table.deeds_table.arn - dynamodb_name = aws_dynamodb_table.deeds_table.name - dynamodb_arn_changes = aws_dynamodb_table.changes_table.arn - dynamodb_name_changes = aws_dynamodb_table.changes_table.name - environment_name = local.environment_name - allowed_arns = local.environment.allowed_arns - dns_weighting = 100 + allowed_arns = local.environment.allowed_arns + app_version = var.app_version + dns_weighting = 100 + dynamodb_arn = aws_dynamodb_table.deeds_table.arn + dynamodb_arn_changes = aws_dynamodb_table.changes_table.arn + dynamodb_name = aws_dynamodb_table.deeds_table.name + dynamodb_name_changes = aws_dynamodb_table.changes_table.name + environment_name = local.environment_name + lpa_store_static_bucket = module.s3_lpa_store_static_eu_west_1.bucket + lpa_store_static_bucket_kms_key = module.s3_lpa_store_static_eu_west_1.encryption_kms_key providers = { aws.region = aws.eu_west_1 @@ -19,14 +21,16 @@ module "eu_west_1" { module "eu_west_2" { source = "./region" - app_version = var.app_version - dynamodb_arn = aws_dynamodb_table_replica.deeds_table.arn - dynamodb_name = aws_dynamodb_table.deeds_table.name - dynamodb_arn_changes = aws_dynamodb_table_replica.changes_table.arn - dynamodb_name_changes = aws_dynamodb_table.changes_table.name - environment_name = local.environment_name - allowed_arns = local.environment.allowed_arns - dns_weighting = 0 + allowed_arns = local.environment.allowed_arns + app_version = var.app_version + dns_weighting = 0 + dynamodb_arn = aws_dynamodb_table_replica.deeds_table.arn + dynamodb_arn_changes = aws_dynamodb_table_replica.changes_table.arn + dynamodb_name = aws_dynamodb_table.deeds_table.name + dynamodb_name_changes = aws_dynamodb_table.changes_table.name + environment_name = local.environment_name + lpa_store_static_bucket = module.s3_lpa_store_static_eu_west_2.bucket + lpa_store_static_bucket_kms_key = module.s3_lpa_store_static_eu_west_2.encryption_kms_key providers = { aws.region = aws.eu_west_2 diff --git a/terraform/environment/s3.tf b/terraform/environment/s3.tf new file mode 100644 index 00000000..b7f085a2 --- /dev/null +++ b/terraform/environment/s3.tf @@ -0,0 +1,89 @@ +resource "aws_iam_role" "s3_replication_role" { + name = "s3-replication-role-${local.environment_name}" + description = "IAM Role for S3 replication in ${local.environment_name}" + assume_role_policy = data.aws_iam_policy_document.s3_replication_role_assume_role.json + provider = aws.global +} + +data "aws_iam_policy_document" "s3_replication_role_assume_role" { + provider = aws.global + statement { + effect = "Allow" + + principals { + type = "Service" + identifiers = ["s3.amazonaws.com"] + } + actions = ["sts:AssumeRole"] + } +} + +module "s3_lpa_store_static_eu_west_1" { + source = "../modules/s3_multi_region_replica_bucket" + accounts_allowed_to_read = [local.backup_account_id] + bucket_name = "opg-lpa-store-static-${local.environment_name}-eu-west-1" + force_destroy = local.is_ephemeral + kms_allowed_iam_roles = module.eu_west_1.lambda_iam_roles[*].arn + replication_configuration = concat( + [{ + account_id = data.aws_caller_identity.current.account_id, + bucket = module.s3_lpa_store_static_eu_west_2.bucket + kms_key_arn = module.s3_lpa_store_static_eu_west_2.encryption_kms_key.arn + }], + local.cross_account_s3_replica_config) + replication_kms_key_arns = [ + module.s3_lpa_store_static_eu_west_2.encryption_kms_key.arn + ] + s3_access_logging_bucket = "s3-access-logs-opg-lpa-store-${local.environment.account_name}-eu-west-1" + s3_replication_role = aws_iam_role.s3_replication_role + providers = { + aws = aws.eu_west_1 + } +} + +module "s3_lpa_store_static_eu_west_2" { + source = "../modules/s3_multi_region_replica_bucket" + accounts_allowed_to_read = [local.backup_account_id] + bucket_name = "opg-lpa-store-static-${local.environment_name}-eu-west-2" + force_destroy = local.is_ephemeral + kms_allowed_iam_roles = module.eu_west_2.lambda_iam_roles[*].arn + replication_configuration = concat( + [{ + account_id = data.aws_caller_identity.current.account_id, + bucket = module.s3_lpa_store_static_eu_west_1.bucket + kms_key_arn = module.s3_lpa_store_static_eu_west_1.encryption_kms_key.arn + }], + local.cross_account_s3_replica_config) + replication_kms_key_arns = [ + module.s3_lpa_store_static_eu_west_1.encryption_kms_key.arn + ] + s3_access_logging_bucket = "s3-access-logs-opg-lpa-store-${local.environment.account_name}-eu-west-2" + s3_replication_role = aws_iam_role.s3_replication_role + providers = { + aws = aws.eu_west_2 + } +} + +module "s3_data_store_backup_account" { + count = local.cross_account_backup_enabled ? 1 : 0 + source = "../modules/s3_cross_account_backup" + bucket_name = "opg-lpa-store-static-${local.environment_name}-backup-eu-west-2" + environment_name = local.environment_name + force_destroy = local.is_ephemeral + s3_access_logging_bucket_prefix = "s3-access-logs-opg-opg-backups-opg-backups" + s3_replication_role = aws_iam_role.s3_replication_role + providers = { + aws.backup-account = aws.opg_backup + aws.source-account = aws.global + } +} + +locals { + cross_account_s3_replica_config = local.cross_account_backup_enabled ? [ + { + account_id = local.backup_account_id, + bucket = module.s3_data_store_backup_account[0].bucket + kms_key_arn = module.s3_data_store_backup_account[0].kms_key.arn + } + ] : [] +} diff --git a/terraform/environment/terraform.tf b/terraform/environment/terraform.tf index 03574ae2..e8a14599 100644 --- a/terraform/environment/terraform.tf +++ b/terraform/environment/terraform.tf @@ -59,6 +59,20 @@ provider "aws" { } } +provider "aws" { + alias = "opg_backup" + region = "eu-west-2" + + assume_role { + role_arn = "arn:aws:iam::${local.backup_account_id}:role/${var.default_role}" + session_name = "terraform-session" + } + + default_tags { + tags = local.default_tags + } +} + provider "aws" { alias = "management_eu_west_1" region = "eu-west-1" diff --git a/terraform/environment/variables.tf b/terraform/environment/variables.tf index bcd03f67..63be2c48 100644 --- a/terraform/environment/variables.tf +++ b/terraform/environment/variables.tf @@ -1,6 +1,11 @@ locals { - environment_name = lower(replace(terraform.workspace, "_", "-")) - environment = contains(keys(var.environments), local.environment_name) ? var.environments[local.environment_name] : var.environments["default"] + backup_account_id = 238302996107 + environment_name = lower(replace(terraform.workspace, "_", "-")) + environment = contains(keys(var.environments), local.environment_name) ? var.environments[local.environment_name] : var.environments["default"] + + is_ephemeral = !contains(keys(var.environments), local.environment_name) + + cross_account_backup_enabled = !local.is_ephemeral default_tags = merge(local.mandatory_moj_tags, local.optional_tags) mandatory_moj_tags = { diff --git a/terraform/modules/lambda/outputs.tf b/terraform/modules/lambda/outputs.tf index 51a7b5da..f7f70acc 100644 --- a/terraform/modules/lambda/outputs.tf +++ b/terraform/modules/lambda/outputs.tf @@ -1,6 +1,6 @@ -output "iam_role_id" { - description = "ID of IAM role created for lambda" - value = aws_iam_role.lambda.id +output "iam_role" { + description = "IAM role object created for lambda" + value = aws_iam_role.lambda } output "invoke_arn" { diff --git a/terraform/modules/s3_cross_account_backup/data_sources.tf b/terraform/modules/s3_cross_account_backup/data_sources.tf new file mode 100644 index 00000000..3d31af3e --- /dev/null +++ b/terraform/modules/s3_cross_account_backup/data_sources.tf @@ -0,0 +1,11 @@ +data "aws_caller_identity" "backup_account" { + provider = aws.backup-account +} + +data "aws_caller_identity" "source_account" { + provider = aws.source-account +} + +data "aws_region" "backup_account" { + provider = aws.backup-account +} diff --git a/terraform/modules/s3_cross_account_backup/iam.tf b/terraform/modules/s3_cross_account_backup/iam.tf new file mode 100644 index 00000000..d20bdc9e --- /dev/null +++ b/terraform/modules/s3_cross_account_backup/iam.tf @@ -0,0 +1,74 @@ +resource "aws_iam_role_policy_attachment" "cross_account_policy_attachment" { + role = var.s3_replication_role.name + policy_arn = aws_iam_policy.cross_account_backup_policy.arn + provider = aws.source-account +} + +resource "aws_iam_policy" "cross_account_backup_policy" { + name = "cross-account-s3-backup-policy-${var.environment_name}" + description = "IAM Policy for s3 replication in ${var.environment_name}" + policy = data.aws_iam_policy_document.cross_account_policy.json + provider = aws.source-account +} + +data "aws_iam_policy_document" "cross_account_policy" { + provider = aws.source-account + statement { + sid = "AllowReplication" + effect = "Allow" + actions = [ + "s3:ReplicateObject", + "s3:ReplicateDelete", + "s3:ReplicateTags", + "s3:GetObjectVersionTagging", + "s3:ObjectOwnerOverrideToBucketOwner" + ] + + condition { + test = "StringLikeIfExists" + variable = "s3:x-amz-server-side-encryption" + values = [ + "aws:kms", + "AES256" + ] + } + + condition { + test = "StringLikeIfExists" + variable = "s3:x-amz-server-side-encryption-aws-kms-key-id" + values = [ + aws_kms_key.key.arn + ] + } + resources = ["${aws_s3_bucket.bucket.arn}/*"] + } + + statement { + sid = "AllowEncryptCrossAccount" + effect = "Allow" + actions = [ + "kms:Encrypt" + ] + + condition { + test = "StringLike" + variable = "kms:ViaService" + + values = [ + "s3.eu-west-1.amazonaws.com", + "s3.eu-west-2.amazonaws.com", + ] + } + + condition { + test = "StringLike" + variable = "kms:EncryptionContext:aws:s3:arn" + + values = [ + "${aws_s3_bucket.bucket.arn}/*", + ] + } + + resources = [aws_kms_key.key.arn] + } +} diff --git a/terraform/modules/s3_cross_account_backup/kms.tf b/terraform/modules/s3_cross_account_backup/kms.tf new file mode 100644 index 00000000..88f44bd2 --- /dev/null +++ b/terraform/modules/s3_cross_account_backup/kms.tf @@ -0,0 +1,39 @@ +resource "aws_kms_alias" "alias" { + name = "alias/${var.environment_name}/s3-replication-key" + target_key_id = aws_kms_key.key.id + provider = aws.backup-account +} + +resource "aws_kms_key" "key" { + description = "KMS Key for ${var.environment_name} cross account S3 replication" + enable_key_rotation = true + policy = data.aws_iam_policy_document.key.json + provider = aws.backup-account +} + +data "aws_iam_policy_document" "key" { + provider = aws.backup-account + statement { + sid = "Enable IAM User Permissions" + effect = "Allow" + actions = ["kms:*"] + resources = ["*"] + + principals { + type = "AWS" + identifiers = ["arn:aws:iam::${data.aws_caller_identity.backup_account.account_id}:root"] + } + } + + statement { + sid = "Enable cross account encrypt access for S3 Cross Region Replication" + effect = "Allow" + actions = ["kms:Encrypt"] + resources = ["*"] + + principals { + type = "AWS" + identifiers = ["arn:aws:iam::${data.aws_caller_identity.source_account.account_id}:root"] + } + } +} diff --git a/terraform/modules/s3_cross_account_backup/outputs.tf b/terraform/modules/s3_cross_account_backup/outputs.tf new file mode 100644 index 00000000..9f83ab9a --- /dev/null +++ b/terraform/modules/s3_cross_account_backup/outputs.tf @@ -0,0 +1,7 @@ +output "bucket" { + value = aws_s3_bucket.bucket +} + +output "kms_key" { + value = aws_kms_key.key +} diff --git a/terraform/modules/s3_cross_account_backup/s3.tf b/terraform/modules/s3_cross_account_backup/s3.tf new file mode 100644 index 00000000..85060fa2 --- /dev/null +++ b/terraform/modules/s3_cross_account_backup/s3.tf @@ -0,0 +1,128 @@ +resource "aws_s3_bucket" "bucket" { + bucket = var.bucket_name + force_destroy = var.force_destroy + provider = aws.backup-account +} + +resource "aws_s3_bucket_ownership_controls" "bucket_object_ownership" { + bucket = aws_s3_bucket.bucket.id + rule { + object_ownership = "BucketOwnerEnforced" + } + provider = aws.backup-account +} + +resource "aws_s3_bucket_versioning" "bucket_versioning" { + bucket = aws_s3_bucket.bucket.id + + versioning_configuration { + status = "Enabled" + } + provider = aws.backup-account +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "bucket_encryption_configuration" { + bucket = aws_s3_bucket.bucket.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } + provider = aws.backup-account +} + +resource "aws_s3_bucket_public_access_block" "public_access_policy" { + bucket = aws_s3_bucket.bucket.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true + + provider = aws.backup-account +} + +resource "aws_s3_bucket_logging" "bucket" { + bucket = aws_s3_bucket.bucket.id + + target_bucket = local.s3_logging_bucket_name + target_prefix = "log/${aws_s3_bucket.bucket.id}/" + + provider = aws.backup-account +} + +resource "aws_s3_bucket_policy" "bucket" { + depends_on = [aws_s3_bucket_public_access_block.public_access_policy] + bucket = aws_s3_bucket.bucket.id + policy = data.aws_iam_policy_document.bucket_default.json + provider = aws.backup-account +} + +data "aws_iam_policy_document" "bucket_default" { + provider = aws.backup-account + policy_id = "PutObjPolicy" + statement { + sid = "allowReplication" + effect = "Allow" + + principals { + identifiers = ["arn:aws:iam::${data.aws_caller_identity.source_account.account_id}:root"] + type = "AWS" + } + + actions = [ + "s3:GetBucketVersioning", + "s3:PutBucketVersioning", + "s3:ReplicateObject", + "s3:ReplicateDelete", + "s3:ObjectOwnerOverrideToBucketOwner" + ] + resources = [ + aws_s3_bucket.bucket.arn, + "${aws_s3_bucket.bucket.arn}/*" + ] + } + statement { + sid = "DenyNoneSSLRequests" + effect = "Deny" + actions = ["s3:*"] + resources = [ + aws_s3_bucket.bucket.arn, + "${aws_s3_bucket.bucket.arn}/*" + ] + + condition { + test = "Bool" + variable = "aws:SecureTransport" + values = [false] + } + + principals { + type = "AWS" + identifiers = ["*"] + } + } + + statement { + sid = "AllowCrossAccountReadAccess" + effect = "Allow" + + principals { + identifiers = ["arn:aws:iam::${data.aws_caller_identity.source_account.account_id}:root"] + type = "AWS" + } + + actions = [ + "s3:ListBucket", + "s3:GetObjectVersion", + "s3:GetObject", + "s3:GetBucketVersioning", + "s3:GetBucketLocation" + ] + resources = [ + aws_s3_bucket.bucket.arn, + "${aws_s3_bucket.bucket.arn}/*" + ] + } +} diff --git a/terraform/modules/s3_cross_account_backup/terraform.tf b/terraform/modules/s3_cross_account_backup/terraform.tf new file mode 100644 index 00000000..e7c18d05 --- /dev/null +++ b/terraform/modules/s3_cross_account_backup/terraform.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.15.0" + configuration_aliases = [ + aws.backup-account, + aws.source-account + ] + } + } + required_version = ">= 1.0.0" +} diff --git a/terraform/modules/s3_cross_account_backup/variables.tf b/terraform/modules/s3_cross_account_backup/variables.tf new file mode 100644 index 00000000..3119874f --- /dev/null +++ b/terraform/modules/s3_cross_account_backup/variables.tf @@ -0,0 +1,26 @@ +locals { + s3_logging_bucket_name = "${var.s3_access_logging_bucket_prefix}-${data.aws_region.backup_account.name}" +} + +variable "bucket_name" { + type = string +} + +variable "environment_name" { + type = string +} + +variable "force_destroy" { + type = bool + default = false +} + +variable "s3_access_logging_bucket_prefix" { + type = string +} + +variable "s3_replication_role" { + type = object({ + name = string + }) +} diff --git a/terraform/modules/s3_multi_region_replica_bucket/data_sources.tf b/terraform/modules/s3_multi_region_replica_bucket/data_sources.tf new file mode 100644 index 00000000..eb58f218 --- /dev/null +++ b/terraform/modules/s3_multi_region_replica_bucket/data_sources.tf @@ -0,0 +1,3 @@ +data "aws_region" "current" {} + +data "aws_caller_identity" "current" {} diff --git a/terraform/modules/s3_multi_region_replica_bucket/iam_replication.tf b/terraform/modules/s3_multi_region_replica_bucket/iam_replication.tf new file mode 100644 index 00000000..7937297f --- /dev/null +++ b/terraform/modules/s3_multi_region_replica_bucket/iam_replication.tf @@ -0,0 +1,79 @@ +resource "aws_iam_role_policy_attachment" "replication_role_s3_permissions" { + role = var.s3_replication_role.name + policy_arn = aws_iam_policy.replication_role_s3_permissions.arn +} + +resource "aws_iam_policy" "replication_role_s3_permissions" { + name = "s3-replication-policy-${var.bucket_name}" + description = "S3 Replication Policy for ${var.bucket_name}" + policy = data.aws_iam_policy_document.replication_role_s3_permissions.json +} + +data "aws_iam_policy_document" "replication_role_s3_permissions" { + + statement { + sid = "AllowReplicationConfiguration" + effect = "Allow" + actions = [ + "s3:ListBucket", + "s3:GetReplicationConfiguration", + "s3:GetObjectVersionForReplication", + "s3:GetObjectVersionAcl", + "s3:GetObjectVersionTagging", + "s3:GetObjectRetention", + "s3:GetObjectLegalHold" + ] + resources = [ + "${aws_s3_bucket.bucket.arn}/*", + aws_s3_bucket.bucket.arn + ] + } + statement { + sid = "AllowCrossRegionReplication" + effect = "Allow" + actions = [ + "s3:ReplicateObject", + "s3:ReplicateDelete", + "s3:ReplicateTags", + "s3:GetObjectVersionTagging", + "s3:ObjectOwnerOverrideToBucketOwner" + ] + + condition { + test = "StringLikeIfExists" + variable = "s3:x-amz-server-side-encryption" + values = [ + "aws:kms", + "AES256" + ] + } + + condition { + test = "StringLikeIfExists" + variable = "s3:x-amz-server-side-encryption-aws-kms-key-id" + values = concat(var.replication_kms_key_arns, [aws_kms_key.s3.arn]) + } + resources = ["${aws_s3_bucket.bucket.arn}/*"] + } + statement { + sid = "AllowKeysEncryptDecryptForS3CrossRegion" + effect = "Allow" + actions = [ + "kms:Decrypt", + "kms:Encrypt" + ] + + condition { + test = "StringLike" + variable = "kms:ViaService" + + values = [ + "s3.eu-west-1.amazonaws.com", + "s3.eu-west-2.amazonaws.com", + ] + } + resources = [ + aws_kms_key.s3.arn + ] + } +} diff --git a/terraform/modules/s3_multi_region_replica_bucket/kms.tf b/terraform/modules/s3_multi_region_replica_bucket/kms.tf new file mode 100644 index 00000000..242ca463 --- /dev/null +++ b/terraform/modules/s3_multi_region_replica_bucket/kms.tf @@ -0,0 +1,71 @@ +resource "aws_kms_key" "s3" { + description = "KMS Key for Encryption at Rest for S3 Bucket ${var.bucket_name}" + deletion_window_in_days = 10 + enable_key_rotation = true + policy = data.aws_iam_policy_document.s3_kms.json +} + +resource "aws_kms_alias" "s3_alias" { + name = "alias/S3-Encryption-${var.bucket_name}" + target_key_id = aws_kms_key.s3.key_id +} + +# See the following link for further information +# https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html +data "aws_iam_policy_document" "s3_kms" { + statement { + sid = "Enable Root account permissions on Key" + effect = "Allow" + actions = ["kms:*"] + resources = ["*"] + + principals { + type = "AWS" + identifiers = [ + "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root", + ] + } + } + + statement { + sid = "Allow Key to be used for Encryption by Lambda" + effect = "Allow" + resources = ["*"] + actions = [ + "kms:Encrypt", + "kms:GenerateDataKey*", + ] + + principals { + type = "AWS" + identifiers = var.kms_allowed_iam_roles + } + } + + statement { + sid = "Key Administrator" + effect = "Allow" + resources = ["*"] + actions = [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:TagResource", + "kms:UntagResource", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion" + ] + + principals { + type = "AWS" + identifiers = ["arn:aws:iam::${data.aws_caller_identity.current.account_id}:role/breakglass"] + } + } +} diff --git a/terraform/modules/s3_multi_region_replica_bucket/outputs.tf b/terraform/modules/s3_multi_region_replica_bucket/outputs.tf new file mode 100644 index 00000000..ef5e1674 --- /dev/null +++ b/terraform/modules/s3_multi_region_replica_bucket/outputs.tf @@ -0,0 +1,7 @@ +output "bucket" { + value = aws_s3_bucket.bucket +} + +output "encryption_kms_key" { + value = aws_kms_key.s3 +} diff --git a/terraform/modules/s3_multi_region_replica_bucket/s3.tf b/terraform/modules/s3_multi_region_replica_bucket/s3.tf new file mode 100644 index 00000000..84e22c78 --- /dev/null +++ b/terraform/modules/s3_multi_region_replica_bucket/s3.tf @@ -0,0 +1,162 @@ +resource "aws_s3_bucket" "bucket" { + bucket = var.bucket_name + force_destroy = var.force_destroy +} + +resource "aws_s3_bucket_ownership_controls" "bucket_object_ownership" { + bucket = aws_s3_bucket.bucket.id + rule { + object_ownership = "BucketOwnerEnforced" + } +} + +resource "aws_s3_bucket_versioning" "bucket_versioning" { + bucket = aws_s3_bucket.bucket.id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "bucket_encryption_configuration" { + bucket = aws_s3_bucket.bucket.id + + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = aws_kms_key.s3.id + sse_algorithm = "aws:kms" + } + } +} + +resource "aws_s3_bucket_replication_configuration" "bucket_replication" { + depends_on = [aws_s3_bucket_versioning.bucket_versioning] + + role = var.s3_replication_role.arn + bucket = aws_s3_bucket.bucket.id + + dynamic "rule" { + for_each = var.replication_configuration + content { + id = "ReplicationTo${rule.value["bucket"].id}" + priority = rule.key + status = "Enabled" + + + destination { + account = rule.value["account_id"] + bucket = rule.value["bucket"].arn + + encryption_configuration { + replica_kms_key_id = rule.value["kms_key_arn"] + } + + access_control_translation { + owner = "Destination" + } + } + + delete_marker_replication { + status = "Enabled" + } + + filter {} + + source_selection_criteria { + sse_kms_encrypted_objects { + status = "Enabled" + } + } + } + } +} + +resource "aws_s3_bucket_public_access_block" "public_access_policy" { + bucket = aws_s3_bucket.bucket.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_logging" "bucket" { + bucket = aws_s3_bucket.bucket.id + + target_bucket = var.s3_access_logging_bucket + target_prefix = "log/${aws_s3_bucket.bucket.id}/" +} + +resource "aws_s3_bucket_policy" "bucket" { + depends_on = [aws_s3_bucket_public_access_block.public_access_policy] + bucket = aws_s3_bucket.bucket.id + policy = local.cross_account_read ? data.aws_iam_policy_document.cross_account_read[0].json : data.aws_iam_policy_document.bucket_default.json +} + +data "aws_iam_policy_document" "bucket_default" { + policy_id = "PutObjPolicy" + + statement { + sid = "DenyUnEncryptedObjectUploads" + effect = "Deny" + actions = ["s3:PutObject"] + resources = ["${aws_s3_bucket.bucket.arn}/*"] + + condition { + test = "StringNotEquals" + variable = "s3:x-amz-server-side-encryption" + values = ["aws:kms"] + } + + principals { + type = "AWS" + identifiers = ["*"] + } + } + + statement { + sid = "DenyNoneSSLRequests" + effect = "Deny" + actions = ["s3:*"] + resources = [ + aws_s3_bucket.bucket.arn, + "${aws_s3_bucket.bucket.arn}/*" + ] + + condition { + test = "Bool" + variable = "aws:SecureTransport" + values = [false] + } + + principals { + type = "AWS" + identifiers = ["*"] + } + } +} + +data "aws_iam_policy_document" "cross_account_read" { + count = local.cross_account_read ? 1 : 0 + source_policy_documents = [data.aws_iam_policy_document.bucket_default.json] + statement { + sid = "DelegateS3Access" + effect = "Allow" + actions = [ + "s3:ListBucket", + "s3:GetObject", + "s3:GetObjectTagging", + "s3:GetObjectVersionTagging" + ] + + principals { + type = "AWS" + identifiers = [for account in var.accounts_allowed_to_read : "arn:aws:iam::${account}:root"] + } + + resources = [ + aws_s3_bucket.bucket.arn, + "${aws_s3_bucket.bucket.arn}/*" + ] + } +} diff --git a/terraform/modules/s3_multi_region_replica_bucket/terraform.tf b/terraform/modules/s3_multi_region_replica_bucket/terraform.tf new file mode 100644 index 00000000..e563d94a --- /dev/null +++ b/terraform/modules/s3_multi_region_replica_bucket/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 5.15.0" + } + } + required_version = ">= 1.0.0" +} diff --git a/terraform/modules/s3_multi_region_replica_bucket/variables.tf b/terraform/modules/s3_multi_region_replica_bucket/variables.tf new file mode 100644 index 00000000..adc26472 --- /dev/null +++ b/terraform/modules/s3_multi_region_replica_bucket/variables.tf @@ -0,0 +1,49 @@ +locals { + cross_account_read = length(var.accounts_allowed_to_read) != 0 ? true : false +} + +variable "accounts_allowed_to_read" { + default = [] + type = list(string) +} + +variable "bucket_name" { + type = string +} + +variable "force_destroy" { + type = bool + default = false +} + +variable "kms_allowed_iam_roles" { + default = [] + type = list(string) +} + +variable "replication_configuration" { + default = [] + type = list(object({ + account_id = string + bucket = object({ + arn = string + id = string + }) + kms_key_arn = string + })) +} + +variable "replication_kms_key_arns" { + type = list(string) +} + +variable "s3_access_logging_bucket" { + type = string +} + +variable "s3_replication_role" { + type = object({ + arn = string + name = string + }) +}