From 77d9d6c574f750bc5593c371da4491a330db96aa Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Mon, 8 Jun 2026 19:04:36 -0400 Subject: [PATCH] [INFRA-96] CMK-encrypt sensitive CloudWatch log groups (M-24) (#20) * [INFRA-96] CMK-encrypt sensitive CloudWatch log groups (M-24) Add a dedicated customer-managed CMK (alias/seahaven-logs) for encrypting the sensitive CloudWatch Logs groups (CloudTrail + finance/PII Lambdas). - lib/logs-key.ts: LogsKey construct. Key policy grants the CloudWatch Logs service principal (logs.us-east-1.amazonaws.com) Encrypt*/Decrypt*/ ReEncrypt*/GenerateDataKey*/DescribeKey, scoped by the kms:EncryptionContext:aws:logs:arn condition (REQUIRED per AWS docs or log delivery breaks). Cross-reviewed (GPT-4.1): tightened Describe* -> DescribeKey; CreateGrant omitted (not needed for plain log-group encryption). - account-baseline-stack.ts: instantiate LogsKey and set KmsKeyId on the L2 Trail's CloudWatch log group in place (escape hatch on the existing AWS::Logs::LogGroup) so it keeps the same logical id + physical name - additive, no replacement, CIS Section-4 metric filters (which import the group by name) keep working, live audit trail not disrupted. Gated by context `encryptTrailLogGroup` so the CMK can be smoke-tested on a low-risk Lambda group before the most-sensitive CloudTrail group. Finance/PII Lambda log groups (exec-aide-*, payments-*, po-email-processor, vendor-reply-processor) are owned by other stacks and associated to this CMK via the CLI for now; codifying KmsKeyId in those repos is tracked as drift. * [INFRA-96] Document sensitive-logs CMK (M-24) in README --- README.md | 18 +++++++++ lib/account-baseline-stack.ts | 25 +++++++++++++ lib/logs-key.ts | 69 +++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 lib/logs-key.ts diff --git a/README.md b/README.md index 9f2a0f8..f94b89a 100644 --- a/README.md +++ b/README.md @@ -179,6 +179,24 @@ this only affects the legacy `AWS-MonthlyBilling` CloudWatch alarm (L-8). | VPC flow logs | `FlowLogs/FlowLog0..4` | H-14 | ALL traffic on all 5 VPCs → S3 | | Flow-logs bucket | `seahaven-vpc-flow-logs-328440206208` | H-14 | Private, SSE-S3, TLS-only, Glacier @90d / expire @365d; delivery bucket policy cross-reviewed | | SES config set | `seahaven-email-events` | M-13 | Bounce/complaint/reject → CloudWatch metrics for reputation visibility | +| Sensitive-logs CMK | `LogsKey/Key` (`alias/seahaven-logs`) | M-24 | Encrypts sensitive CloudWatch Logs groups. Key policy grants `logs.us-east-1.amazonaws.com` Encrypt*/Decrypt*/ReEncrypt*/GenerateDataKey*/DescribeKey scoped by `kms:EncryptionContext:aws:logs:arn` (required or log delivery breaks). Rotation on, RETAIN. Applied in place to `TrailLogGroup` via escape hatch (same logical id/name). Cross-reviewed | + +**M-24 sensitive log groups:** `alias/seahaven-logs` encrypts the CloudTrail CW +log group (codified here) plus the finance/PII Lambda groups owned by other +stacks — `exec-aide-*`, `payments-*`, `po-email-processor`, `vendor-reply-processor` +— which are associated via `aws logs associate-kms-key` and tracked as drift to +codify in their owning repos. The CloudTrail group is encrypted in place (escape +hatch on the existing `AWS::Logs::LogGroup`) so it is additive: same logical id + +physical name, no replacement, CIS Section-4 metric filters keep working. A +context flag `encryptTrailLogGroup` (default `true`) allows rolling the CMK out +and smoke-testing it on a low-risk Lambda group before the CloudTrail group: + +```bash +# Phase 1: deploy CMK only, validate on a low-risk group +cdk deploy account-baseline --context encryptTrailLogGroup=false +# Phase 2: encrypt the CloudTrail group (default) +cdk deploy account-baseline +``` **H-1 log group:** the metric filters attach to the existing CloudTrail CloudWatch Logs group by name (`seahaven-account-baseline-TrailLogGroup4CBE3AF5-…`), diff --git a/lib/account-baseline-stack.ts b/lib/account-baseline-stack.ts index 128d423..7579625 100644 --- a/lib/account-baseline-stack.ts +++ b/lib/account-baseline-stack.ts @@ -5,6 +5,7 @@ import * as iam from "aws-cdk-lib/aws-iam"; import * as logs from "aws-cdk-lib/aws-logs"; import * as cloudtrail from "aws-cdk-lib/aws-cloudtrail"; import { Construct } from "constructs"; +import { LogsKey } from "./logs-key"; import { DetectiveControls } from "./detective-controls"; import { GovernanceToggles } from "./governance-toggles"; import { BedrockLogging } from "./bedrock-logging"; @@ -177,6 +178,30 @@ export class AccountBaselineStack extends cdk.Stack { managementEvents: cloudtrail.ReadWriteType.ALL, }); + // ── Sensitive CloudWatch Logs CMK (M-24 / INFRA-96) ── + // Dedicated key for encrypting the CloudTrail CW log group and the + // finance/PII Lambda log groups (those live in other stacks and are + // associated via CLI until codified in their owning repos — see README). + const logsKey = new LogsKey(this, "LogsKey"); + + // CMK-encrypt the Trail's CloudWatch Logs group in place. The L2 Trail + // construct owns this group (path Trail/LogGroup) and does not expose an + // encryptionKey prop for it, so we set KmsKeyId via escape hatch. This keeps + // the same logical ID and physical name, so it is additive (no replacement) + // and the CIS Section 4 metric filters that import the group by name (H-1) + // keep working, and the live audit trail is never disrupted. + // + // Gated by context `encryptTrailLogGroup` (default true) so the CMK can be + // rolled out and smoke-tested on a low-risk Lambda log group first, before + // applying it to the most-sensitive CloudTrail group (INFRA-96 step 3). + const encryptTrailLogGroup = + this.node.tryGetContext("encryptTrailLogGroup") !== "false"; + if (encryptTrailLogGroup && trail.logGroup) { + const cfnTrailLogGroup = trail.logGroup.node + .defaultChild as logs.CfnLogGroup; + cfnTrailLogGroup.kmsKeyId = logsKey.key.keyArn; + } + // ── Day 1 detective layer + governance toggles ── // Config (H-2), GuardDuty (H-3), Security Hub (H-4), Access Analyzer (M-5). new DetectiveControls(this, "DetectiveControls"); diff --git a/lib/logs-key.ts b/lib/logs-key.ts new file mode 100644 index 0000000..9c863da --- /dev/null +++ b/lib/logs-key.ts @@ -0,0 +1,69 @@ +import * as cdk from "aws-cdk-lib"; +import * as kms from "aws-cdk-lib/aws-kms"; +import * as iam from "aws-cdk-lib/aws-iam"; +import { Construct } from "constructs"; + +/** + * Dedicated customer-managed CMK for encrypting sensitive CloudWatch Logs + * groups (audit M-24 / INFRA-96). + * + * Used by the account CloudTrail log group and the finance/PII Lambda log + * groups (exec-aide, payments, po/vendor email processors, qbo). This is a + * SEPARATE key from `alias/seahaven-alarm-topics` (SNS alarm topics) and + * `alias/cloudtrail-logs` (the CloudTrail S3 log-file CMK) — those have + * different service principals and encryption contexts. + * + * The key policy MUST grant the CloudWatch Logs service principal use of the + * key, scoped by the `kms:EncryptionContext:aws:logs:arn` condition, or log + * delivery to any CMK-encrypted group silently stops. (AWS docs: "Encrypt log + * data in CloudWatch Logs using AWS KMS".) Cross-reviewed 2026-06-08 + * (INFRA-96): dropped `Describe*` to the specific `DescribeKey`; CreateGrant is + * not required for plain log-group encryption so it is omitted. + */ +export class LogsKey extends Construct { + public readonly key: kms.Key; + + constructor(scope: Construct, id: string) { + super(scope, id); + + const stack = cdk.Stack.of(this); + + this.key = new kms.Key(this, "Key", { + alias: "seahaven-logs", + description: + "Encrypts sensitive CloudWatch Logs groups (CloudTrail + finance/PII Lambdas) — M-24/INFRA-96", + enableKeyRotation: true, + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); + + // CloudWatch Logs service principal must be able to use the key to deliver + // (encrypt) and read (decrypt) log events. The encryption-context condition + // binds the grant to this account's log groups only, so the key cannot be + // used to decrypt arbitrary data under the logs service principal. + this.key.addToResourcePolicy( + new iam.PolicyStatement({ + sid: "AllowCloudWatchLogsUseOfKey", + effect: iam.Effect.ALLOW, + principals: [ + new iam.ServicePrincipal(`logs.${stack.region}.amazonaws.com`), + ], + actions: [ + "kms:Encrypt*", + "kms:Decrypt*", + "kms:ReEncrypt*", + "kms:GenerateDataKey*", + "kms:DescribeKey", + ], + resources: ["*"], + conditions: { + ArnLike: { + "kms:EncryptionContext:aws:logs:arn": `arn:${stack.partition}:logs:${stack.region}:${stack.account}:log-group:*`, + }, + }, + }) + ); + + new cdk.CfnOutput(this, "LogsKeyArn", { value: this.key.keyArn }); + new cdk.CfnOutput(this, "LogsKeyAlias", { value: "alias/seahaven-logs" }); + } +}