[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
This commit is contained in:
Adam Moussa 2026-06-08 19:04:36 -04:00 • committed by GitHub
parent 5002ed86d8
commit 77d9d6c574
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 112 additions and 0 deletions

View file

@ -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 | | 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 | | 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 | | 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 **H-1 log group:** the metric filters attach to the existing CloudTrail
CloudWatch Logs group by name (`seahaven-account-baseline-TrailLogGroup4CBE3AF5-…`), CloudWatch Logs group by name (`seahaven-account-baseline-TrailLogGroup4CBE3AF5-…`),

View file

@ -5,6 +5,7 @@ import * as iam from "aws-cdk-lib/aws-iam";
import * as logs from "aws-cdk-lib/aws-logs"; import * as logs from "aws-cdk-lib/aws-logs";
import * as cloudtrail from "aws-cdk-lib/aws-cloudtrail"; import * as cloudtrail from "aws-cdk-lib/aws-cloudtrail";
import { Construct } from "constructs"; import { Construct } from "constructs";
import { LogsKey } from "./logs-key";
import { DetectiveControls } from "./detective-controls"; import { DetectiveControls } from "./detective-controls";
import { GovernanceToggles } from "./governance-toggles"; import { GovernanceToggles } from "./governance-toggles";
import { BedrockLogging } from "./bedrock-logging"; import { BedrockLogging } from "./bedrock-logging";
@ -177,6 +178,30 @@ export class AccountBaselineStack extends cdk.Stack {
managementEvents: cloudtrail.ReadWriteType.ALL, 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 ── // ── Day 1 detective layer + governance toggles ──
// Config (H-2), GuardDuty (H-3), Security Hub (H-4), Access Analyzer (M-5). // Config (H-2), GuardDuty (H-3), Security Hub (H-4), Access Analyzer (M-5).
new DetectiveControls(this, "DetectiveControls"); new DetectiveControls(this, "DetectiveControls");

69
lib/logs-key.ts Normal file
View file

@ -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" });
}
}