import * as cdk from "aws-cdk-lib"; import * as s3 from "aws-cdk-lib/aws-s3"; import * as kms from "aws-cdk-lib/aws-kms"; 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"; import { CisMonitoring } from "./cis-monitoring"; import { FlowLogs } from "./flow-logs"; import { SesMonitoring } from "./ses-monitoring"; import { AppWebAcl } from "./web-acl"; /** * Account-level security baseline for Sea Haven (account 328440206208). * * First resident: a multi-region CloudTrail with log-file validation, KMS * encryption, an Object-Lock'd S3 log bucket, and CloudWatch Logs delivery. * Closes audit finding C-1 and CIS 3.1/3.2/3.4/3.6/3.7 (+3.8 via key rotation), * and provides the CloudWatch Logs group that the CIS Section 4 metric filters * (H-1) attach to. * * Future residents (same stack): AWS Config (H-2), GuardDuty (H-3), * Security Hub (H-4), IAM Access Analyzer (M-5), Inspector2 (M-6). */ export interface AccountBaselineStackProps extends cdk.StackProps { /** Monthly cost budget ceiling in USD (M-10). */ readonly monthlyBudgetUsd: number; /** Email for budget threshold alerts (M-10). */ readonly budgetAlertEmail: string; /** * VPC ids to attach ALL-traffic flow logs to (H-14). Logical IDs are * index-derived — only append, never reorder (see lib/flow-logs.ts). */ readonly flowLogVpcIds: string[]; } export class AccountBaselineStack extends cdk.Stack { constructor(scope: Construct, id: string, props: AccountBaselineStackProps) { super(scope, id, props); const trailName = "seahaven-org-trail"; // AWS Organizations org id (o-9kufuzz6b4). CloudTrail org trusted-access is // already enabled on the management account; promoting this trail to an org // trail (INFRA-73) makes it collect member-account events into this bucket. const orgId = "o-9kufuzz6b4"; // Static trail ARN (built from name, not trail.trailArn) so the key policy // does not create a circular dependency with the Trail resource. const trailArn = cdk.Arn.format( { service: "cloudtrail", resource: "trail", resourceName: trailName }, this ); // ── KMS CMK ── encrypts CloudTrail log files (CIS 3.7); rotation = CIS 3.8. const trailKey = new kms.Key(this, "TrailKey", { alias: "cloudtrail-logs", description: "Encrypts CloudTrail log files for the account-wide trail", enableKeyRotation: true, removalPolicy: cdk.RemovalPolicy.RETAIN, }); // The L2 Trail construct does NOT grant the CloudTrail service principal use // of a customer-provided key, so delivery of encrypted logs would fail. // Grant it explicitly, scoped to this account's trail via SourceArn and the // CloudTrail encryption context. (Caught by cross-review 2026-05-29.) trailKey.addToResourcePolicy( new iam.PolicyStatement({ sid: "AllowCloudTrailEncrypt", effect: iam.Effect.ALLOW, principals: [new iam.ServicePrincipal("cloudtrail.amazonaws.com")], actions: ["kms:GenerateDataKey*"], resources: ["*"], conditions: { StringEquals: { "aws:SourceArn": trailArn }, StringLike: { "kms:EncryptionContext:aws:cloudtrail:arn": `arn:${this.partition}:cloudtrail:*:${this.account}:trail/*`, }, }, }) ); // INFRA-73 (org trail): member accounts deliver their CloudTrail events to // this CMK-encrypted bucket, so the CloudTrail service principal must be able // to GenerateDataKey using each member trail's own encryption context. // // Cross-review BLOCK (GPT-4.1): the SourceArn must NOT be pinned to the // management account 328440206208 — org-trail shadow trails in member // accounts present their OWN account id in both the SourceArn and the // encryption-context arn, so pinning to the management account would silently // block all member-account delivery. Both are wildcarded across accounts and // the statement is org-scoped by aws:PrincipalOrgID so only accounts in // o-9kufuzz6b4 — not arbitrary CloudTrail principals — can use the key. trailKey.addToResourcePolicy( new iam.PolicyStatement({ sid: "AllowOrgMemberCloudTrailEncrypt", effect: iam.Effect.ALLOW, principals: [new iam.ServicePrincipal("cloudtrail.amazonaws.com")], actions: ["kms:GenerateDataKey*", "kms:DescribeKey"], resources: ["*"], conditions: { StringEquals: { "aws:PrincipalOrgID": orgId }, StringLike: { "kms:EncryptionContext:aws:cloudtrail:arn": `arn:${this.partition}:cloudtrail:*:*:trail/*`, "aws:SourceArn": `arn:${this.partition}:cloudtrail:*:*:trail/*`, }, }, }) ); trailKey.addToResourcePolicy( new iam.PolicyStatement({ sid: "AllowCloudTrailDescribeKey", effect: iam.Effect.ALLOW, principals: [new iam.ServicePrincipal("cloudtrail.amazonaws.com")], actions: ["kms:DescribeKey"], resources: ["*"], }) ); // Central, pre-existing access-logs bucket (manually created, imported // read-only) — receives S3 server access logs for the trail bucket (CIS 3.6). const accessLogsBucket = s3.Bucket.fromBucketName( this, "AccessLogsBucket", "seahaven-s3-access-logs" ); // ── Hardened, tamper-resistant log bucket ── // Private (BPA all on), KMS-encrypted, versioned, TLS-only, Object-Lock // GOVERNANCE 365d so logs cannot be silently deleted/overwritten. const logBucket = new s3.Bucket(this, "TrailLogBucket", { bucketName: `seahaven-cloudtrail-logs-${this.account}`, encryption: s3.BucketEncryption.KMS, encryptionKey: trailKey, bucketKeyEnabled: true, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, enforceSSL: true, versioned: true, objectLockEnabled: true, objectLockDefaultRetention: s3.ObjectLockRetention.governance( cdk.Duration.days(365) ), serverAccessLogsBucket: accessLogsBucket, serverAccessLogsPrefix: "cloudtrail-bucket-access/", lifecycleRules: [ { id: "transition-and-expire", transitions: [ { storageClass: s3.StorageClass.GLACIER, transitionAfter: cdk.Duration.days(90), }, ], expiration: cdk.Duration.days(365), abortIncompleteMultipartUploadAfter: cdk.Duration.days(7), }, ], 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. // Data events (CIS 3.10/3.11) intentionally deferred — management events // only for now to control cost (see README). const trail = new cloudtrail.Trail(this, "Trail", { trailName, bucket: logBucket, encryptionKey: trailKey, isMultiRegionTrail: true, // INFRA-73: promote to an organization trail. CloudTrail org // trusted-access is already enabled on the management account; this makes // the trail collect every member account's events into this bucket. // Passing orgId lets the L2 construct auto-attach the AWSLogs//* // bucket-policy PutObject statement (scoped to this trail's SourceArn). isOrganizationTrail: true, orgId, includeGlobalServiceEvents: true, enableFileValidation: true, sendToCloudWatchLogs: true, cloudWatchLogGroup: trailLogGroup, managementEvents: cloudtrail.ReadWriteType.ALL, // CloudTrail Insights (§37): compensating control for the residual risk // accepted in #36 (CFN/Config-proxied denials excluded from CIS 4.1) and // the low-and-slow evasion surface in the UnauthorizedApiCalls alarm. // ApiCallRateInsight flags anomalous write-API spikes; ApiErrorRateInsight // flags anomalous errored/denied call rates — including the denials CIS // 4.1 intentionally filters out. Per-event cost (≈$0.35/100k management // events); an org trail with 10–15M management events/month adds roughly // $35–$53/month. CIS 4.1 alarm + GuardDuty + Security Hub (CIS v3.0) are // already live, so this is defence-in-depth, not an urgent gap-fill. insightTypes: [ cloudtrail.InsightType.API_CALL_RATE, cloudtrail.InsightType.API_ERROR_RATE, ], }); // ── Day 1 detective layer + governance toggles ── // Config (H-2), GuardDuty (H-3), Security Hub (H-4), Access Analyzer (M-5). new DetectiveControls(this, "DetectiveControls", { namePrefix: "seahaven", }); // Monthly cost budget (M-10). Other governance toggles are CLI + documented. new GovernanceToggles(this, "GovernanceToggles", { budgetName: "seahaven-monthly-cost", monthlyLimitUsd: props.monthlyBudgetUsd, alertEmail: props.budgetAlertEmail, }); // ── Day 2 monitoring + logging ── // CIS Section 4 metric filters/alarms (H-1), VPC flow logs (H-14), // SES bounce/complaint config set (M-13). new CisMonitoring(this, "CisMonitoring", { alarmEmail: props.budgetAlertEmail, trailLogGroup, }); new FlowLogs(this, "FlowLogs", { namePrefix: "seahaven", vpcIds: props.flowLogVpcIds, }); new SesMonitoring(this, "SesMonitoring"); // Shared CloudFront WAF WebACL (M-17); ARN published to SSM for app stacks. new AppWebAcl(this, "AppWebAcl"); // ── Day 5 AI governance ── // Bedrock model invocation logging destinations + delivery role (H-20). // The account-level logging configuration itself has no CFN resource type; // applied via CLI post-deploy (see lib/bedrock-logging.ts header). new BedrockLogging(this, "BedrockLogging"); cdk.Tags.of(this).add("Project", "account-baseline"); cdk.Tags.of(this).add("Owner", "adam@seahavenind.com"); cdk.Tags.of(this).add("Environment", "prod"); cdk.Tags.of(this).add("ManagedBy", "cdk"); new cdk.CfnOutput(this, "TrailArn", { value: trail.trailArn }); new cdk.CfnOutput(this, "LogBucketName", { value: logBucket.bucketName }); new cdk.CfnOutput(this, "TrailKmsKeyArn", { value: trailKey.keyArn }); } }