From 47fa35688df261070a59f3a6e30f6cdb9b52101c Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:31:55 -0400 Subject: [PATCH] fix(po): grant KMS on seahaven-dynamodb CMK to purchase-orders consumers (INFRA-104) (#56) The purchase-orders table was migrated to SSE-KMS (alias/seahaven-dynamodb, INFRA-95/M-3) out-of-band, but po_stack never declared the key, so grant_read_write_data did not propagate kms perms. po-email-processor failed ~99.6% of invocations with kms:Decrypt AccessDeniedException, a data-loss outage on the PO ingestion write path. - po_stack: declare encryption_key on purchase-orders (reconciles SSE drift; no-op against the already-encrypted live table) so the existing grants add kms:Decrypt/GenerateDataKey/DescribeKey to EmailProcessor, WebUI, SiteExtractor. - wo_stack: pre-emptive grant_encrypt_decrypt on the WO processor role ahead of the WorkOrders CMK migration (INFRA-6); tables left unencrypted, no table change. GPT-4.1 cross-review: no blockers. --- cdk/po_stack.py | 18 ++++++++++++++++++ cdk/wo_stack.py | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/cdk/po_stack.py b/cdk/po_stack.py index 144d9e9..82a6bd6 100644 --- a/cdk/po_stack.py +++ b/cdk/po_stack.py @@ -8,6 +8,7 @@ from aws_cdk import ( aws_cloudwatch as cloudwatch, aws_cloudwatch_actions as cw_actions, aws_dynamodb as dynamodb, + aws_kms as kms, aws_lambda as lambda_, aws_lambda_event_sources as lambda_event_sources, aws_logs as logs, @@ -18,6 +19,7 @@ from aws_cdk import ( aws_secretsmanager as secretsmanager, aws_sns as sns, aws_sqs as sqs, + aws_ssm as ssm, ) from constructs import Construct @@ -37,6 +39,20 @@ class PoIngestStack(Stack): ], ) + # --- Shared customer-managed CMK for sensitive DynamoDB tables --- + # Owned by the account-baseline app (alias/seahaven-dynamodb, INFRA-95 / + # M-3); ARN published to SSM. The purchase-orders table was migrated to + # SSE-KMS out-of-band, so declaring encryption_key here reconciles the + # drift and — via grant_read_write_data below — propagates the required + # kms:Decrypt/GenerateDataKey/DescribeKey to the consumer roles. + dynamodb_cmk = kms.Key.from_key_arn( + self, + "DynamoDbCmk", + ssm.StringParameter.value_for_string_parameter( + self, "/seahaven/dynamodb/cmk-arn" + ), + ) + # --- Purchase-orders DynamoDB table --- # Owned by this stack. Streams enabled for the site-extractor pipeline. # Other stacks (seahaven-slack-bot) reference this table via fromTableName(). @@ -51,6 +67,8 @@ class PoIngestStack(Stack): billing_mode=dynamodb.BillingMode.PAY_PER_REQUEST, removal_policy=RemovalPolicy.RETAIN, stream=dynamodb.StreamViewType.NEW_IMAGE, + encryption=dynamodb.TableEncryption.CUSTOMER_MANAGED, + encryption_key=dynamodb_cmk, ) # --- Secrets Manager for Anthropic API key --- diff --git a/cdk/wo_stack.py b/cdk/wo_stack.py index 4cba41b..c26313c 100644 --- a/cdk/wo_stack.py +++ b/cdk/wo_stack.py @@ -8,6 +8,7 @@ from aws_cdk import ( aws_cloudwatch as cloudwatch, aws_cloudwatch_actions as cw_actions, aws_dynamodb as dynamodb, + aws_kms as kms, aws_lambda as lambda_, aws_logs as logs, aws_s3 as s3, @@ -17,6 +18,7 @@ from aws_cdk import ( aws_secretsmanager as secretsmanager, aws_sns as sns, aws_sqs as sqs, + aws_ssm as ssm, ) from constructs import Construct @@ -127,6 +129,22 @@ class WorkorderIngestStack(Stack): comments_table.grant_read_write_data(email_processor) anthropic_secret.grant_read(email_processor) + # Pre-emptive KMS grant on the shared DynamoDB CMK (alias/seahaven-dynamodb, + # /seahaven/dynamodb/cmk-arn). The WorkOrders/WorkOrderComments tables are + # NOT yet SSE-KMS encrypted, so this grant is currently unused; it is added + # ahead of the CMK migration so the processor role does not hit AccessDenied + # the moment those tables are migrated. The actual table migration + kebab + # rename is tracked in INFRA-6 (and the set-aside wo_stack CMK WIP); fold the + # web-ui read grant in there. + dynamodb_cmk = kms.Key.from_key_arn( + self, + "DynamoDbCmk", + ssm.StringParameter.value_for_string_parameter( + self, "/seahaven/dynamodb/cmk-arn" + ), + ) + dynamodb_cmk.grant_encrypt_decrypt(email_processor) + # --- Errors alarm (INFRA-41 / audit H-8) --- # ALARM-only (no OK action, per the CloudWatch-alarm preference) to the # shared site-alerts topic (CMK alias/seahaven-alarm-topics lives on the