Stabilize CloudTrail log group name to prevent filter detachment (#23)

Previously the L2 cloudtrail.Trail auto-created a log group with a
CDK-generated hash suffix in its name. The 15 CIS Section 4 metric
filters in CisMonitoring imported that group by the hardcoded generated
name. If the Trail or log group was ever recreated the suffix changes
and all 15 filters would silently detach with no error, leaving the
account unmonitored.

Replace with an explicit logs.LogGroup named
seahaven-account-baseline-trail-logs (stable, no hash suffix) with
RemovalPolicy.RETAIN. Pass the CDK object — not a name constant — to
the Trail via cloudWatchLogGroup and forward it to CisMonitoring via a
new trailLogGroup prop. All 15 filters now reference the CDK object so
they can never drift from the group the Trail actually delivers to.

The old auto-named log group is orphaned by this deploy (CloudFormation
loses track of it and does not delete it). Historical audit logs in the
old group remain accessible in CloudWatch under the old name; no audit
history is destroyed.

Refs: INFRA-19
This commit is contained in:
Adam Moussa 2026-06-10 14:44:33 -04:00 • committed by GitHub
parent e22c4ac005
commit cd82b49f17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 48 additions and 37 deletions

View file

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

View file

@ -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`, {