From 0fe1bf0947e53e2e501085f3863177d8e0e1adab Mon Sep 17 00:00:00 2001 From: Dustin Smith Date: Sat, 14 Mar 2026 11:15:38 +0700 Subject: [PATCH 1/2] security: enable encryption at rest for DynamoDB tables and SQS queues DynamoDB tables now explicitly enable server-side encryption (AWS-managed key by default). SQS queues (alerts + DLQs) now use KMS encryption (alias/aws/sqs by default). Optional kms_key_arn variable allows bringing a custom CMK for full key control and rotation. Conditional IAM policies grant Lambda roles kms:Decrypt and kms:GenerateDataKey only when a custom CMK is provided. --- deploy/terraform/alerting.tf | 18 ++++++++----- deploy/terraform/dynamodb.tf | 20 ++++++++++++++ deploy/terraform/eventbridge.tf | 4 +-- deploy/terraform/lambda.tf | 48 ++++++++++++++++++++++++++++----- deploy/terraform/variables.tf | 6 +++++ 5 files changed, 81 insertions(+), 15 deletions(-) diff --git a/deploy/terraform/alerting.tf b/deploy/terraform/alerting.tf index 696eb3a..23c478f 100644 --- a/deploy/terraform/alerting.tf +++ b/deploy/terraform/alerting.tf @@ -3,16 +3,20 @@ # ----------------------------------------------------------------------------- resource "aws_sqs_queue" "alert_dlq" { - name = "${var.environment}-interlock-alert-dlq" - message_retention_seconds = 1209600 # 14 days - tags = var.tags + name = "${var.environment}-interlock-alert-dlq" + message_retention_seconds = 1209600 # 14 days + kms_master_key_id = var.kms_key_arn != "" ? var.kms_key_arn : "alias/aws/sqs" + kms_data_key_reuse_period_seconds = 300 + tags = var.tags } resource "aws_sqs_queue" "alert" { - name = "${var.environment}-interlock-alerts" - message_retention_seconds = 86400 # 1 day - visibility_timeout_seconds = 60 - tags = var.tags + name = "${var.environment}-interlock-alerts" + message_retention_seconds = 86400 # 1 day + visibility_timeout_seconds = 60 + kms_master_key_id = var.kms_key_arn != "" ? var.kms_key_arn : "alias/aws/sqs" + kms_data_key_reuse_period_seconds = 300 + tags = var.tags redrive_policy = jsonencode({ deadLetterTargetArn = aws_sqs_queue.alert_dlq.arn diff --git a/deploy/terraform/dynamodb.tf b/deploy/terraform/dynamodb.tf index 84f04b1..438a947 100644 --- a/deploy/terraform/dynamodb.tf +++ b/deploy/terraform/dynamodb.tf @@ -16,6 +16,11 @@ resource "aws_dynamodb_table" "control" { type = "S" } + server_side_encryption { + enabled = true + kms_key_arn = var.kms_key_arn != "" ? var.kms_key_arn : null + } + ttl { attribute_name = "ttl" enabled = true @@ -49,6 +54,11 @@ resource "aws_dynamodb_table" "joblog" { type = "S" } + server_side_encryption { + enabled = true + kms_key_arn = var.kms_key_arn != "" ? var.kms_key_arn : null + } + ttl { attribute_name = "ttl" enabled = true @@ -92,6 +102,11 @@ resource "aws_dynamodb_table" "events" { type = "N" } + server_side_encryption { + enabled = true + kms_key_arn = var.kms_key_arn != "" ? var.kms_key_arn : null + } + global_secondary_index { name = "GSI1" hash_key = "eventType" @@ -129,6 +144,11 @@ resource "aws_dynamodb_table" "rerun" { type = "S" } + server_side_encryption { + enabled = true + kms_key_arn = var.kms_key_arn != "" ? var.kms_key_arn : null + } + ttl { attribute_name = "ttl" enabled = true diff --git a/deploy/terraform/eventbridge.tf b/deploy/terraform/eventbridge.tf index 5fb8e79..33d67e3 100644 --- a/deploy/terraform/eventbridge.tf +++ b/deploy/terraform/eventbridge.tf @@ -10,8 +10,8 @@ resource "aws_cloudwatch_event_bus_policy" "interlock_bus" { Version = "2012-10-17" Statement = [ { - Sid = "AllowInterlockLambdas" - Effect = "Allow" + Sid = "AllowInterlockLambdas" + Effect = "Allow" Principal = { AWS = [for name in local.lambda_names : aws_iam_role.lambda[name].arn] } diff --git a/deploy/terraform/lambda.tf b/deploy/terraform/lambda.tf index d3cf012..780e4de 100644 --- a/deploy/terraform/lambda.tf +++ b/deploy/terraform/lambda.tf @@ -489,20 +489,56 @@ resource "aws_iam_role_policy" "sqs_dlq" { policy = data.aws_iam_policy_document.sqs_dlq.json } +# ----------------------------------------------------------------------------- +# KMS — grant Lambda roles decrypt/encrypt access when a custom CMK is provided +# ----------------------------------------------------------------------------- + +resource "aws_iam_role_policy" "kms_sqs_stream_router" { + count = var.kms_key_arn != "" ? 1 : 0 + name = "kms-sqs" + role = aws_iam_role.lambda["stream-router"].id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = ["kms:Decrypt", "kms:GenerateDataKey"] + Resource = [var.kms_key_arn] + }] + }) +} + +resource "aws_iam_role_policy" "kms_sqs_alert_dispatcher" { + count = var.kms_key_arn != "" ? 1 : 0 + name = "kms-sqs" + role = aws_iam_role.lambda["alert-dispatcher"].id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Action = ["kms:Decrypt", "kms:GenerateDataKey"] + Resource = [var.kms_key_arn] + }] + }) +} + # ============================================================================= # SQS Dead-Letter Queues for stream event source mappings # ============================================================================= resource "aws_sqs_queue" "stream_router_control_dlq" { - name = "${var.environment}-interlock-sr-control-dlq" - message_retention_seconds = 1209600 # 14 days - tags = var.tags + name = "${var.environment}-interlock-sr-control-dlq" + message_retention_seconds = 1209600 # 14 days + kms_master_key_id = var.kms_key_arn != "" ? var.kms_key_arn : "alias/aws/sqs" + kms_data_key_reuse_period_seconds = 300 + tags = var.tags } resource "aws_sqs_queue" "stream_router_joblog_dlq" { - name = "${var.environment}-interlock-sr-joblog-dlq" - message_retention_seconds = 1209600 # 14 days - tags = var.tags + name = "${var.environment}-interlock-sr-joblog-dlq" + message_retention_seconds = 1209600 # 14 days + kms_master_key_id = var.kms_key_arn != "" ? var.kms_key_arn : "alias/aws/sqs" + kms_data_key_reuse_period_seconds = 300 + tags = var.tags } # ============================================================================= diff --git a/deploy/terraform/variables.tf b/deploy/terraform/variables.tf index eb7615a..84f11e5 100644 --- a/deploy/terraform/variables.tf +++ b/deploy/terraform/variables.tf @@ -100,6 +100,12 @@ variable "lambda_concurrency" { } } +variable "kms_key_arn" { + description = "ARN of a KMS key for encrypting DynamoDB tables and SQS queues at rest. Empty = AWS-managed encryption." + type = string + default = "" +} + variable "sns_alarm_topic_arn" { description = "SNS topic ARN for CloudWatch alarm notifications (empty = alarms fire but no notifications)" type = string From 1a0b859b2fa4102b2fe4276e284061dc6502a5fa Mon Sep 17 00:00:00 2001 From: Dustin Smith Date: Sat, 14 Mar 2026 11:16:38 +0700 Subject: [PATCH 2/2] docs: update CHANGELOG for v0.9.3 security hardening --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f796f43..d12aee0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [0.9.3] - 2026-03-13 +## [0.9.3] - 2026-03-14 ### Changed @@ -15,7 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Security -- **Command trigger shell injection eliminated (SEC-3)** — Replaced `sh -c` with direct `exec.CommandContext` + `strings.Fields` argument splitting. No shell interpretation of pipes, redirects, or variable expansion. +- **Encryption at rest for DynamoDB and SQS** — All DynamoDB tables now explicitly enable server-side encryption. SQS queues (alert queue, alert DLQ, stream-router DLQs) now use KMS encryption. New optional `kms_key_arn` variable for custom CMK when full key control and rotation are needed; defaults to AWS-managed keys (free, no configuration required). +- **SSRF protection on trigger HTTP clients** — Custom `http.Transport` with dial-time IP validation rejects connections to private, loopback, link-local, and multicast addresses. Protects HTTP, Airflow, and Databricks triggers against targeting internal endpoints. +- **EventBridge PutEvents partial failure detection** — `publishEvent` now checks `FailedEntryCount` on the response. Previously, partial failures were silently discarded. +- **Command trigger shell injection eliminated** — Replaced `sh -c` with direct `exec.CommandContext` + `strings.Fields` argument splitting. No shell interpretation of pipes, redirects, or variable expansion. ## [0.9.2] - 2026-03-13