diff --git a/docs/platform-modules.md b/docs/platform-modules.md index 5405329..b52b603 100644 --- a/docs/platform-modules.md +++ b/docs/platform-modules.md @@ -38,6 +38,8 @@ ALB, ACM wildcard certificate, Route53 DNS. | HTTPS listener | Port 443, default action = fixed 404 | | Route53 | Validation records for ACM | +The ALB drops request headers whose names contain characters other than letters, digits and `-` (`drop_invalid_header_fields`, Security Hub ELB.4). Headers with underscores, such as `X_Api_Key`, never reach the app. Clients of an app moving onto the platform ALB must send hyphenated header names. + **Inputs:** `public_subnet_ids`, `alb_sg_id` from networking; `extra_domains` (domain → zone ID) from dns **Outputs:** `alb_arn`, `https_listener_arn`, `alb_dns_name`, `alb_zone_id` diff --git a/terraform/platform/cost-analytics/main.tf b/terraform/platform/cost-analytics/main.tf index 605b7b8..adfe29b 100644 --- a/terraform/platform/cost-analytics/main.tf +++ b/terraform/platform/cost-analytics/main.tf @@ -99,6 +99,20 @@ resource "aws_s3_bucket_policy" "cur_data" { } } }, + { + # S3.5 + Sid = "DenyInsecureTransport" + Effect = "Deny" + Principal = "*" + Action = "s3:*" + Resource = [ + aws_s3_bucket.cur_data.arn, + "${aws_s3_bucket.cur_data.arn}/*", + ] + Condition = { + Bool = { "aws:SecureTransport" = "false" } + } + }, ] }) } @@ -129,6 +143,30 @@ resource "aws_s3_bucket_server_side_encryption_configuration" "athena_results" { } } +# S3.5 +resource "aws_s3_bucket_policy" "athena_results" { + bucket = aws_s3_bucket.athena_results.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Sid = "DenyInsecureTransport" + Effect = "Deny" + Principal = "*" + Action = "s3:*" + Resource = [ + aws_s3_bucket.athena_results.arn, + "${aws_s3_bucket.athena_results.arn}/*", + ] + Condition = { + Bool = { "aws:SecureTransport" = "false" } + } + }] + }) + + depends_on = [aws_s3_bucket_public_access_block.athena_results] +} + resource "aws_s3_bucket_lifecycle_configuration" "athena_results" { bucket = aws_s3_bucket.athena_results.id diff --git a/terraform/platform/identity/main.tf b/terraform/platform/identity/main.tf index 137612b..27007ec 100644 --- a/terraform/platform/identity/main.tf +++ b/terraform/platform/identity/main.tf @@ -16,7 +16,8 @@ ################################################################################ resource "aws_cognito_user_pool" "internal" { - name = "${var.project}-internal" + name = "${var.project}-internal" + deletion_protection = "ACTIVE" # Cognito.6 lambda_config { pre_token_generation_config { @@ -139,7 +140,8 @@ removed { ################################################################################ resource "aws_cognito_user_pool" "external" { - name = "${var.project}-external" + name = "${var.project}-external" + deletion_protection = "ACTIVE" # Cognito.6 auto_verified_attributes = ["email"] username_attributes = ["email"] diff --git a/terraform/platform/ingress/main.tf b/terraform/platform/ingress/main.tf index 8e06906..c45de7c 100644 --- a/terraform/platform/ingress/main.tf +++ b/terraform/platform/ingress/main.tf @@ -106,6 +106,8 @@ resource "aws_lb" "main" { security_groups = [var.alb_sg_id] subnets = var.public_subnet_ids enable_deletion_protection = true + # ELB.4: drop headers with invalid field names (request smuggling hygiene) + drop_invalid_header_fields = true tags = { Name = "${var.project}-platform-alb" diff --git a/terraform/platform/networking/main.tf b/terraform/platform/networking/main.tf index 323109f..9924e62 100644 --- a/terraform/platform/networking/main.tf +++ b/terraform/platform/networking/main.tf @@ -21,15 +21,29 @@ resource "aws_vpc" "main" { } } +# EC2.2: the VPC's default security group allows nothing. Every platform and +# app resource uses its own security group; nothing is attached to this one. +resource "aws_default_security_group" "main" { + vpc_id = aws_vpc.main.id + + tags = { + Name = "${var.project}-default-deny" + team = "shared" + } +} + ################################################################################ # Subnets ################################################################################ +# EC2.15: public subnets do not auto-assign public IPs. Nothing relies on it: +# NAT has an EIP, the ALB manages its own, and ECS tasks run in private +# subnets without public IPs. resource "aws_subnet" "public_a" { vpc_id = aws_vpc.main.id cidr_block = var.public_subnet_cidrs[0] availability_zone = data.aws_availability_zones.available.names[0] - map_public_ip_on_launch = true + map_public_ip_on_launch = false tags = { Name = "${var.project}-public-a" @@ -42,7 +56,7 @@ resource "aws_subnet" "public_b" { vpc_id = aws_vpc.main.id cidr_block = var.public_subnet_cidrs[1] availability_zone = data.aws_availability_zones.available.names[1] - map_public_ip_on_launch = true + map_public_ip_on_launch = false tags = { Name = "${var.project}-public-b"