From 23182eb49f8cca0698fc2ac84f6cbbca938daabf Mon Sep 17 00:00:00 2001 From: Alexanderamiri Date: Sat, 24 Oct 2026 15:38:34 +0200 Subject: [PATCH 1/2] fix(platform): harden ALB, Cognito pools, subnets and bucket TLS drop_invalid_header_fields on the ALB, deletion protection on both Cognito pools, no auto-assigned public IPs on public subnets, rule-less default SG for the platform VPC, TLS-only policies on the CUR and Athena buckets. Co-Authored-By: Claude Opus 5.5 (1M context) --- terraform/platform/cost-analytics/main.tf | 38 +++++++++++++++++++++++ terraform/platform/identity/main.tf | 6 ++-- terraform/platform/ingress/main.tf | 2 ++ terraform/platform/networking/main.tf | 18 +++++++++-- 4 files changed, 60 insertions(+), 4 deletions(-) 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 e176986..35f188d 100644 --- a/terraform/platform/identity/main.tf +++ b/terraform/platform/identity/main.tf @@ -12,7 +12,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 { @@ -104,7 +105,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" From 84c0524b34ca93ea8af90e52a5ecb5e8975dfbae Mon Sep 17 00:00:00 2001 From: Alexanderamiri Date: Sat, 24 Oct 2026 16:03:02 +0200 Subject: [PATCH 2/2] docs(ingress): note that the ALB drops underscore header names Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/platform-modules.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/platform-modules.md b/docs/platform-modules.md index 0c18eaf..d6d0a24 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`