diff --git a/lib/account-baseline-stack.ts b/lib/account-baseline-stack.ts index 7579625..7d960a0 100644 --- a/lib/account-baseline-stack.ts +++ b/lib/account-baseline-stack.ts @@ -154,6 +154,38 @@ export class AccountBaselineStack extends cdk.Stack { removalPolicy: cdk.RemovalPolicy.RETAIN, }); + // ── 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"); + + // ── Stable-named CloudTrail log group (INFRA-19) ── + // Previously the L2 Trail construct auto-created an anonymous log group + // (CDK-generated name with a hash suffix). The 15 CIS Section 4 metric + // filters in CisMonitoring imported it by that hardcoded generated name, + // which changes if the Trail/log group is ever recreated — causing all 15 + // filters to silently detach with no error. + // + // This explicit LogGroup uses a stable, human-readable name so the filters + // can reference the CDK object (not a string constant). The group is passed + // to the Trail via cloudWatchLogGroup, and the same object is forwarded to + // CisMonitoring. RETAIN ensures historical audit logs are never destroyed + // when the stack is updated or deleted. + // + // DEPLOY NOTE: This is a one-time replacement of the auto-created log group + // with an explicit named one. CloudFormation will DELETE the old auto-named + // group and CREATE this new stable-named group. The old group (with its + // historical audit logs) is ORPHANED in AWS — it will NOT be deleted because + // CloudFormation loses track of it; the logs remain accessible in the + // CloudWatch console under the old name. No audit history is destroyed. + const trailLogGroup = new logs.LogGroup(this, "TrailLogGroup", { + logGroupName: "seahaven-account-baseline-trail-logs", + retention: logs.RetentionDays.ONE_YEAR, + encryptionKey: logsKey.key, + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); + // ── Multi-region trail ── // Management events (read + write), log-file validation, global service // events, delivered to the KMS-encrypted bucket and to CloudWatch Logs. @@ -174,34 +206,10 @@ export class AccountBaselineStack extends cdk.Stack { includeGlobalServiceEvents: true, enableFileValidation: true, sendToCloudWatchLogs: true, - cloudWatchLogsRetention: logs.RetentionDays.ONE_YEAR, + cloudWatchLogGroup: trailLogGroup, 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"); @@ -216,6 +224,7 @@ export class AccountBaselineStack extends cdk.Stack { // SES bounce/complaint config set (M-13). new CisMonitoring(this, "CisMonitoring", { alarmEmail: props.budgetAlertEmail, + trailLogGroup, }); new FlowLogs(this, "FlowLogs"); new SesMonitoring(this, "SesMonitoring"); diff --git a/lib/cis-monitoring.ts b/lib/cis-monitoring.ts index 3d9d0fb..af41368 100644 --- a/lib/cis-monitoring.ts +++ b/lib/cis-monitoring.ts @@ -15,15 +15,14 @@ import { Construct } from "constructs"; * (4.16 "Security Hub enabled" is not a metric filter — done Day 1, H-4.) * * Alarms fire on ALARM only (no OK/recovery actions) per Sea Haven preference. + * + * The trail log group is injected via props (INFRA-19). The previous approach + * imported the group by a hardcoded CDK-generated name constant — if the Trail + * or log group was ever recreated the generated suffix would change and all 15 + * filters would silently detach. The caller now creates an explicit LogGroup with + * a stable name and passes the CDK object here. */ -// The account CloudTrail (C-1) delivers to this CloudWatch Logs group. It is -// created by the L2 cloudtrail.Trail in account-baseline-stack.ts; we import it -// by name rather than replace it, so the live audit trail is never disrupted. -// Stable as long as the Trail is not recreated. -const TRAIL_LOG_GROUP_NAME = - "seahaven-account-baseline-TrailLogGroup4CBE3AF5-e7hMDCzj8e4d"; - interface CisControl { readonly id: string; readonly metricName: string; @@ -142,6 +141,13 @@ const CIS_CONTROLS: CisControl[] = [ export interface CisMonitoringProps { /** Email subscribed to the CIS alarm topic. */ readonly alarmEmail: string; + /** + * The CloudTrail CloudWatch Logs group. Must be the explicit stable-named + * LogGroup created in account-baseline-stack.ts (INFRA-19) — not an imported + * reference, so the metric filters are bound to the CDK object rather than a + * hardcoded generated name. + */ + readonly trailLogGroup: logs.ILogGroup; } export class CisMonitoring extends Construct { @@ -180,11 +186,7 @@ export class CisMonitoring extends Construct { }); topic.addSubscription(new subscriptions.EmailSubscription(props.alarmEmail)); - const logGroup = logs.LogGroup.fromLogGroupName( - this, - "TrailLogGroup", - TRAIL_LOG_GROUP_NAME - ); + const logGroup = props.trailLogGroup; for (const c of CIS_CONTROLS) { const mf = new logs.MetricFilter(this, `${c.id}Filter`, {