From b4e5b062e03e95945f19c3f178171883d7cfd241 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 6 Jul 2026 14:50:47 +0000 Subject: [PATCH] feat(ha-hot-hot/aws): VPC flow logs, matching single-vm/unlimited-scale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SECURITY-DEFAULTS.md claims "VPC Flow Logs ... enabled by default (enable_flow_logs = true)" for every module, but ha-hot-hot/aws had no enable_flow_logs variable, no aws_flow_log resource, and no flow-log IAM role — tracked in #47 as the AWS-side half of a larger cross-module gap. Ports the existing pattern from unlimited-scale/aws verbatim: aws_cloudwatch_log_group.flow_logs, aws_iam_role.flow_logs, aws_iam_role_policy.flow_logs, aws_flow_log.vpc, all gated on enable_flow_logs (default true). Adds flow_log_group_name output and forwards the variable/output through asm-aws-ha and sat-aws-ha. Adds terraform test coverage: default-on assertion in tests/basic.tftest.hcl, plus enabled/disabled plan checks in tests/feature_flags.tftest.hcl. No existing resources changed; enable_flow_logs=true is additive only. Existing deployments get one new CloudWatch log group, IAM role, and flow log on their next apply. The Azure NSG flow-log side of #47 (single-vm/azure, ha-hot-hot/azure, unlimited-scale/azure — no Azure module has this at all) is out of scope here per #47's own recommendation to split AWS and Azure work. Verified locally: terraform validate and terraform test (8/8 passing, including the 2 new flow-log runs) on ha-hot-hot/aws, asm-aws-ha, and sat-aws-ha. tflint/checkov/trivy could not run in this sandbox (network policy blocks the release-artifact downloads); the change is a verbatim copy of the pattern already tflint/checkov-clean in unlimited-scale/aws, and no new checkov suppression is expected. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01TRS3XkFnAjqmeaK5v9Vv5q --- modules/asm-aws-ha/main.tf | 1 + modules/asm-aws-ha/outputs.tf | 6 ++ modules/asm-aws-ha/variables.tf | 6 ++ modules/ha-hot-hot/aws/main.tf | 70 +++++++++++++++++++ modules/ha-hot-hot/aws/outputs.tf | 5 ++ modules/ha-hot-hot/aws/tests/basic.tftest.hcl | 10 +++ .../aws/tests/feature_flags.tftest.hcl | 32 +++++++++ modules/ha-hot-hot/aws/variables.tf | 6 ++ modules/sat-aws-ha/main.tf | 1 + modules/sat-aws-ha/outputs.tf | 6 ++ modules/sat-aws-ha/variables.tf | 6 ++ 11 files changed, 149 insertions(+) diff --git a/modules/asm-aws-ha/main.tf b/modules/asm-aws-ha/main.tf index be4301b..2c8a05a 100644 --- a/modules/asm-aws-ha/main.tf +++ b/modules/asm-aws-ha/main.tf @@ -20,6 +20,7 @@ module "this" { db_backup_retention_days = var.db_backup_retention_days db_deletion_protection = var.db_deletion_protection enable_customer_managed_key = var.enable_customer_managed_key + enable_flow_logs = var.enable_flow_logs alb_idle_timeout_seconds = var.alb_idle_timeout_seconds alb_min_tls_version = var.alb_min_tls_version enable_management_access = var.enable_management_access diff --git a/modules/asm-aws-ha/outputs.tf b/modules/asm-aws-ha/outputs.tf index e89ceba..ed9df04 100644 --- a/modules/asm-aws-ha/outputs.tf +++ b/modules/asm-aws-ha/outputs.tf @@ -112,3 +112,9 @@ output "redis_mode" { value = module.this.redis_mode sensitive = false } + +output "flow_log_group_name" { + description = "CloudWatch log group name receiving VPC Flow Logs. Empty string when enable_flow_logs is false." + value = module.this.flow_log_group_name + sensitive = false +} diff --git a/modules/asm-aws-ha/variables.tf b/modules/asm-aws-ha/variables.tf index ec42ea9..9dde602 100644 --- a/modules/asm-aws-ha/variables.tf +++ b/modules/asm-aws-ha/variables.tf @@ -98,6 +98,12 @@ variable "enable_customer_managed_key" { default = false } +variable "enable_flow_logs" { + description = "Enable VPC Flow Logs for the provided VPC, sending ALL traffic to a CloudWatch log group named /aws/vpc-flow-logs/. Matches the default stated in SECURITY-DEFAULTS.md." + type = bool + default = true +} + variable "alb_idle_timeout_seconds" { type = number default = 120 diff --git a/modules/ha-hot-hot/aws/main.tf b/modules/ha-hot-hot/aws/main.tf index e2fd772..9c9bc28 100644 --- a/modules/ha-hot-hot/aws/main.tf +++ b/modules/ha-hot-hot/aws/main.tf @@ -1199,3 +1199,73 @@ resource "aws_iam_role_policy_attachment" "rds_monitoring" { role = aws_iam_role.rds_monitoring[0].name policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole" } + +# ----- VPC Flow Logs ----- + +resource "aws_cloudwatch_log_group" "flow_logs" { + count = var.enable_flow_logs ? 1 : 0 + name = "/aws/vpc-flow-logs/${local.name_prefix}" + retention_in_days = 30 + kms_key_id = var.enable_customer_managed_key ? aws_kms_key.main[0].arn : null + tags = local.common_tags +} + +resource "aws_iam_role" "flow_logs" { + count = var.enable_flow_logs ? 1 : 0 + name = "${local.name_prefix}-flow-logs" + tags = local.common_tags + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { Service = "vpc-flow-logs.amazonaws.com" } + }] + }) +} + +# The flow-logs role's first statement is scoped to this module's single +# flow_logs log-group ARN. The second statement uses Resource = "*" only for +# logs:DescribeLogGroups, which the API itself requires — IAM rejects ARN +# scoping on that action. The role can still neither read nor write any other +# log group. +#tfsec:ignore:aws-iam-no-policy-wildcards +resource "aws_iam_role_policy" "flow_logs" { + count = var.enable_flow_logs ? 1 : 0 + name = "${local.name_prefix}-flow-logs" + role = aws_iam_role.flow_logs[0].id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "logs:CreateLogStream", + "logs:PutLogEvents", + "logs:DescribeLogStreams", + ] + Resource = [ + aws_cloudwatch_log_group.flow_logs[0].arn, + "${aws_cloudwatch_log_group.flow_logs[0].arn}:*", + ] + }, + { + Effect = "Allow" + Action = ["logs:DescribeLogGroups"] + Resource = "*" + }, + ] + }) +} + +resource "aws_flow_log" "vpc" { + count = var.enable_flow_logs ? 1 : 0 + iam_role_arn = aws_iam_role.flow_logs[0].arn + log_destination = aws_cloudwatch_log_group.flow_logs[0].arn + log_destination_type = "cloud-watch-logs" + traffic_type = "ALL" + vpc_id = var.vpc_id + tags = local.common_tags +} diff --git a/modules/ha-hot-hot/aws/outputs.tf b/modules/ha-hot-hot/aws/outputs.tf index 1279f05..3ee2619 100644 --- a/modules/ha-hot-hot/aws/outputs.tf +++ b/modules/ha-hot-hot/aws/outputs.tf @@ -96,3 +96,8 @@ output "redis_mode" { description = "How Redis is wired: 'managed' (this module provisioned ElastiCache), 'override' (customer-supplied endpoint), or 'disabled' (HA is not actually safe)." value = local.provision_managed_redis ? "managed" : (var.redis_endpoint_override == null ? "disabled" : "override") } + +output "flow_log_group_name" { + description = "CloudWatch log group name receiving VPC Flow Logs. Empty string when enable_flow_logs is false." + value = var.enable_flow_logs ? aws_cloudwatch_log_group.flow_logs[0].name : "" +} diff --git a/modules/ha-hot-hot/aws/tests/basic.tftest.hcl b/modules/ha-hot-hot/aws/tests/basic.tftest.hcl index 91622cb..297485b 100644 --- a/modules/ha-hot-hot/aws/tests/basic.tftest.hcl +++ b/modules/ha-hot-hot/aws/tests/basic.tftest.hcl @@ -22,6 +22,11 @@ mock_provider "aws" { mock_resource "aws_sns_topic" { defaults = { arn = "arn:aws:sns:us-east-1:123456789012:mock-topic" } } + mock_resource "aws_cloudwatch_log_group" { + defaults = { + arn = "arn:aws:logs:us-east-1:123456789012:log-group:/aws/vpc-flow-logs/mock:*" + } + } } mock_provider "random" {} @@ -70,4 +75,9 @@ run "minimal_inputs_apply" { condition = output.redis_endpoint != "" error_message = "redis_endpoint output must be non-empty when managed Redis is enabled (the default)" } + + assert { + condition = output.flow_log_group_name != "" + error_message = "flow_log_group_name output must be non-empty when enable_flow_logs is true (the default)" + } } diff --git a/modules/ha-hot-hot/aws/tests/feature_flags.tftest.hcl b/modules/ha-hot-hot/aws/tests/feature_flags.tftest.hcl index 400b063..85b615f 100644 --- a/modules/ha-hot-hot/aws/tests/feature_flags.tftest.hcl +++ b/modules/ha-hot-hot/aws/tests/feature_flags.tftest.hcl @@ -90,3 +90,35 @@ run "redis_disabled_creates_nothing" { error_message = "redis_mode must be 'disabled' when managed Redis is off and no override is supplied." } } + +run "flow_logs_enabled_by_default" { + command = plan + + assert { + condition = length(aws_flow_log.vpc) == 1 + error_message = "enable_flow_logs defaults to true and must create exactly one VPC flow log." + } +} + +run "flow_logs_disabled_creates_nothing" { + command = plan + + variables { + enable_flow_logs = false + } + + assert { + condition = length(aws_flow_log.vpc) == 0 + error_message = "enable_flow_logs = false must create zero VPC flow logs." + } + + assert { + condition = length(aws_cloudwatch_log_group.flow_logs) == 0 + error_message = "enable_flow_logs = false must create zero flow-log CloudWatch log groups." + } + + assert { + condition = output.flow_log_group_name == "" + error_message = "flow_log_group_name must be empty string when enable_flow_logs is false." + } +} diff --git a/modules/ha-hot-hot/aws/variables.tf b/modules/ha-hot-hot/aws/variables.tf index 31283a4..19228e0 100644 --- a/modules/ha-hot-hot/aws/variables.tf +++ b/modules/ha-hot-hot/aws/variables.tf @@ -171,6 +171,12 @@ variable "enable_customer_managed_key" { default = false } +variable "enable_flow_logs" { + description = "Enable VPC Flow Logs for the provided VPC, sending ALL traffic to a CloudWatch log group named /aws/vpc-flow-logs/. Matches the default stated in SECURITY-DEFAULTS.md." + type = bool + default = true +} + variable "alb_idle_timeout_seconds" { type = number default = 120 diff --git a/modules/sat-aws-ha/main.tf b/modules/sat-aws-ha/main.tf index e015a6e..a991e95 100644 --- a/modules/sat-aws-ha/main.tf +++ b/modules/sat-aws-ha/main.tf @@ -20,6 +20,7 @@ module "this" { db_backup_retention_days = var.db_backup_retention_days db_deletion_protection = var.db_deletion_protection enable_customer_managed_key = var.enable_customer_managed_key + enable_flow_logs = var.enable_flow_logs alb_idle_timeout_seconds = var.alb_idle_timeout_seconds alb_min_tls_version = var.alb_min_tls_version enable_management_access = var.enable_management_access diff --git a/modules/sat-aws-ha/outputs.tf b/modules/sat-aws-ha/outputs.tf index e89ceba..ed9df04 100644 --- a/modules/sat-aws-ha/outputs.tf +++ b/modules/sat-aws-ha/outputs.tf @@ -112,3 +112,9 @@ output "redis_mode" { value = module.this.redis_mode sensitive = false } + +output "flow_log_group_name" { + description = "CloudWatch log group name receiving VPC Flow Logs. Empty string when enable_flow_logs is false." + value = module.this.flow_log_group_name + sensitive = false +} diff --git a/modules/sat-aws-ha/variables.tf b/modules/sat-aws-ha/variables.tf index 34fec44..f622580 100644 --- a/modules/sat-aws-ha/variables.tf +++ b/modules/sat-aws-ha/variables.tf @@ -98,6 +98,12 @@ variable "enable_customer_managed_key" { default = false } +variable "enable_flow_logs" { + description = "Enable VPC Flow Logs for the provided VPC, sending ALL traffic to a CloudWatch log group named /aws/vpc-flow-logs/. Matches the default stated in SECURITY-DEFAULTS.md." + type = bool + default = true +} + variable "alb_idle_timeout_seconds" { type = number default = 120