2026-05-29 17:44:55 -04:00
|
|
|
|
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";
|
2026-06-08 19:04:36 -04:00
|
|
|
|
import { LogsKey } from "./logs-key";
|
2026-06-01 17:56:12 -04:00
|
|
|
|
import { DetectiveControls } from "./detective-controls";
|
|
|
|
|
|
import { GovernanceToggles } from "./governance-toggles";
|
2026-06-03 15:17:39 -04:00
|
|
|
|
import { BedrockLogging } from "./bedrock-logging";
|
2026-06-02 15:16:24 -04:00
|
|
|
|
import { CisMonitoring } from "./cis-monitoring";
|
|
|
|
|
|
import { FlowLogs } from "./flow-logs";
|
|
|
|
|
|
import { SesMonitoring } from "./ses-monitoring";
|
2026-06-02 16:42:24 -04:00
|
|
|
|
import { AppWebAcl } from "./web-acl";
|
2026-05-29 17:44:55 -04:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 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).
|
|
|
|
|
|
*/
|
2026-06-01 17:56:12 -04:00
|
|
|
|
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;
|
2026-07-14 13:53:07 -04:00
|
|
|
|
/**
|
|
|
|
|
|
* 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[];
|
2026-06-01 17:56:12 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-29 17:44:55 -04:00
|
|
|
|
export class AccountBaselineStack extends cdk.Stack {
|
2026-06-01 17:56:12 -04:00
|
|
|
|
constructor(scope: Construct, id: string, props: AccountBaselineStackProps) {
|
2026-05-29 17:44:55 -04:00
|
|
|
|
super(scope, id, props);
|
|
|
|
|
|
|
|
|
|
|
|
const trailName = "seahaven-org-trail";
|
[INFRA-91/89/16/88/73] Reconcile out-of-band baseline changes + add missing detective controls (#18)
* Codify primary vault lock + add backups (INFRA-89, INFRA-88)
INFRA-89: codify the GOVERNANCE Vault Lock applied out-of-band on the
seahaven-primary vault (MinRetention 1d, MaxRetention 2555d, no
changeableFor = admin-removable) so it lives in IaC. Values match the
live lock exactly, so the deploy is a no-op adoption.
Add a scoped vault access policy that denies manual recovery-point
deletion and lock/policy tampering to all principals except the AWS
Backup service role and the break-glass SSO AdministratorAccess role,
so automatic lifecycle expiry still works but humans cannot prune
recovery points by hand.
Cross-review (GPT-4.1) BLOCK: NotPrincipal does not support wildcard
ARN matching, so the SSO exemption is expressed as Effect DENY with
Principal * and a StringNotLike condition on aws:PrincipalArn, which
does support wildcards. This avoids an unrecoverable vault lockout.
INFRA-88: add 6 S3 buckets (kb-docs, payroll-emails [PII], amazon-po,
extracted-amazon-po, proposal-system uploads + generated) to the
phase2-offsite-everything selection. Versioning verified enabled on
all 6 against the live account (S3 backup requires versioning).
Refs: INFRA-89, INFRA-88
* Promote account trail to organization trail (INFRA-73)
INFRA-73: set isOrganizationTrail on seahaven-org-trail and pass orgId
(o-9kufuzz6b4) so the L2 Trail attaches the AWSLogs/<org-id>/* bucket
PutObject statement for member-account delivery. CloudTrail org
trusted-access is already enabled on the management account.
Broaden the KMS key policy with an org-scoped GenerateDataKey/DescribeKey
statement for member-account trail delivery, guarded by
aws:PrincipalOrgID. The existing single-account statements are
preserved so management-account delivery is unaffected.
Cross-review (GPT-4.1) BLOCK: the member KMS SourceArn and encryption
context must be wildcarded across accounts (org-trail shadow trails
present the member account id), not pinned to the management account,
or member delivery silently fails. Fixed before checkpoint.
CHECKPOINT: delicate org-trail KMS/bucket-policy change — code +
diff captured for review, NOT deployed.
Refs: INFRA-73
* Add secondary-region baseline stacks (INFRA-91, INFRA-16)
INFRA-91: codify the Bedrock model-invocation logging applied
out-of-band in us-west-2 and us-east-2 (per-region delivery role
seahaven-bedrock-invocation-logging-<region> + log group
/aws/bedrock/model-invocations 90d, CloudWatch-only). The account-level
logging config itself has no CFN resource type and is applied via CLI
(already live), same as us-east-1.
INFRA-16: add the still-missing us-east-2 detective controls — AWS
Config recorder role + delivery bucket (recorder/channel via CLI to
avoid the CFN stabilization deadlock seen in us-east-1) and Security
Hub with FSBP + CIS v3.0. GuardDuty + flow logs already live in
us-east-2 and are left for a follow-up adoption to keep this change
non-destructive.
The us-east-1 baseline stays region-pinned; these are separate
RegionalBaselineStack instances composed opt-in per region.
CHECKPOINT: new multi-region stacks. The live Bedrock role + log group
already exist (CLI-created), so a plain deploy would collide — these
need cdk import / changeset adoption, not cdk deploy. Code + diff
captured for review, NOT deployed.
Refs: INFRA-91, INFRA-16
* Drop vault access policy from this deploy; tracked in INFRA-94 (kept governance lock codify + selection)
2026-06-08 17:03:18 -04:00
|
|
|
|
// 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";
|
2026-05-29 17:44:55 -04:00
|
|
|
|
// 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-91/89/16/88/73] Reconcile out-of-band baseline changes + add missing detective controls (#18)
* Codify primary vault lock + add backups (INFRA-89, INFRA-88)
INFRA-89: codify the GOVERNANCE Vault Lock applied out-of-band on the
seahaven-primary vault (MinRetention 1d, MaxRetention 2555d, no
changeableFor = admin-removable) so it lives in IaC. Values match the
live lock exactly, so the deploy is a no-op adoption.
Add a scoped vault access policy that denies manual recovery-point
deletion and lock/policy tampering to all principals except the AWS
Backup service role and the break-glass SSO AdministratorAccess role,
so automatic lifecycle expiry still works but humans cannot prune
recovery points by hand.
Cross-review (GPT-4.1) BLOCK: NotPrincipal does not support wildcard
ARN matching, so the SSO exemption is expressed as Effect DENY with
Principal * and a StringNotLike condition on aws:PrincipalArn, which
does support wildcards. This avoids an unrecoverable vault lockout.
INFRA-88: add 6 S3 buckets (kb-docs, payroll-emails [PII], amazon-po,
extracted-amazon-po, proposal-system uploads + generated) to the
phase2-offsite-everything selection. Versioning verified enabled on
all 6 against the live account (S3 backup requires versioning).
Refs: INFRA-89, INFRA-88
* Promote account trail to organization trail (INFRA-73)
INFRA-73: set isOrganizationTrail on seahaven-org-trail and pass orgId
(o-9kufuzz6b4) so the L2 Trail attaches the AWSLogs/<org-id>/* bucket
PutObject statement for member-account delivery. CloudTrail org
trusted-access is already enabled on the management account.
Broaden the KMS key policy with an org-scoped GenerateDataKey/DescribeKey
statement for member-account trail delivery, guarded by
aws:PrincipalOrgID. The existing single-account statements are
preserved so management-account delivery is unaffected.
Cross-review (GPT-4.1) BLOCK: the member KMS SourceArn and encryption
context must be wildcarded across accounts (org-trail shadow trails
present the member account id), not pinned to the management account,
or member delivery silently fails. Fixed before checkpoint.
CHECKPOINT: delicate org-trail KMS/bucket-policy change — code +
diff captured for review, NOT deployed.
Refs: INFRA-73
* Add secondary-region baseline stacks (INFRA-91, INFRA-16)
INFRA-91: codify the Bedrock model-invocation logging applied
out-of-band in us-west-2 and us-east-2 (per-region delivery role
seahaven-bedrock-invocation-logging-<region> + log group
/aws/bedrock/model-invocations 90d, CloudWatch-only). The account-level
logging config itself has no CFN resource type and is applied via CLI
(already live), same as us-east-1.
INFRA-16: add the still-missing us-east-2 detective controls — AWS
Config recorder role + delivery bucket (recorder/channel via CLI to
avoid the CFN stabilization deadlock seen in us-east-1) and Security
Hub with FSBP + CIS v3.0. GuardDuty + flow logs already live in
us-east-2 and are left for a follow-up adoption to keep this change
non-destructive.
The us-east-1 baseline stays region-pinned; these are separate
RegionalBaselineStack instances composed opt-in per region.
CHECKPOINT: new multi-region stacks. The live Bedrock role + log group
already exist (CLI-created), so a plain deploy would collide — these
need cdk import / changeset adoption, not cdk deploy. Code + diff
captured for review, NOT deployed.
Refs: INFRA-91, INFRA-16
* Drop vault access policy from this deploy; tracked in INFRA-94 (kept governance lock codify + selection)
2026-06-08 17:03:18 -04:00
|
|
|
|
// 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/*`,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
})
|
|
|
|
|
|
);
|
2026-05-29 17:44:55 -04:00
|
|
|
|
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,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-10 14:44:33 -04:00
|
|
|
|
// ── 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,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-05-29 17:44:55 -04:00
|
|
|
|
// ── 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-91/89/16/88/73] Reconcile out-of-band baseline changes + add missing detective controls (#18)
* Codify primary vault lock + add backups (INFRA-89, INFRA-88)
INFRA-89: codify the GOVERNANCE Vault Lock applied out-of-band on the
seahaven-primary vault (MinRetention 1d, MaxRetention 2555d, no
changeableFor = admin-removable) so it lives in IaC. Values match the
live lock exactly, so the deploy is a no-op adoption.
Add a scoped vault access policy that denies manual recovery-point
deletion and lock/policy tampering to all principals except the AWS
Backup service role and the break-glass SSO AdministratorAccess role,
so automatic lifecycle expiry still works but humans cannot prune
recovery points by hand.
Cross-review (GPT-4.1) BLOCK: NotPrincipal does not support wildcard
ARN matching, so the SSO exemption is expressed as Effect DENY with
Principal * and a StringNotLike condition on aws:PrincipalArn, which
does support wildcards. This avoids an unrecoverable vault lockout.
INFRA-88: add 6 S3 buckets (kb-docs, payroll-emails [PII], amazon-po,
extracted-amazon-po, proposal-system uploads + generated) to the
phase2-offsite-everything selection. Versioning verified enabled on
all 6 against the live account (S3 backup requires versioning).
Refs: INFRA-89, INFRA-88
* Promote account trail to organization trail (INFRA-73)
INFRA-73: set isOrganizationTrail on seahaven-org-trail and pass orgId
(o-9kufuzz6b4) so the L2 Trail attaches the AWSLogs/<org-id>/* bucket
PutObject statement for member-account delivery. CloudTrail org
trusted-access is already enabled on the management account.
Broaden the KMS key policy with an org-scoped GenerateDataKey/DescribeKey
statement for member-account trail delivery, guarded by
aws:PrincipalOrgID. The existing single-account statements are
preserved so management-account delivery is unaffected.
Cross-review (GPT-4.1) BLOCK: the member KMS SourceArn and encryption
context must be wildcarded across accounts (org-trail shadow trails
present the member account id), not pinned to the management account,
or member delivery silently fails. Fixed before checkpoint.
CHECKPOINT: delicate org-trail KMS/bucket-policy change — code +
diff captured for review, NOT deployed.
Refs: INFRA-73
* Add secondary-region baseline stacks (INFRA-91, INFRA-16)
INFRA-91: codify the Bedrock model-invocation logging applied
out-of-band in us-west-2 and us-east-2 (per-region delivery role
seahaven-bedrock-invocation-logging-<region> + log group
/aws/bedrock/model-invocations 90d, CloudWatch-only). The account-level
logging config itself has no CFN resource type and is applied via CLI
(already live), same as us-east-1.
INFRA-16: add the still-missing us-east-2 detective controls — AWS
Config recorder role + delivery bucket (recorder/channel via CLI to
avoid the CFN stabilization deadlock seen in us-east-1) and Security
Hub with FSBP + CIS v3.0. GuardDuty + flow logs already live in
us-east-2 and are left for a follow-up adoption to keep this change
non-destructive.
The us-east-1 baseline stays region-pinned; these are separate
RegionalBaselineStack instances composed opt-in per region.
CHECKPOINT: new multi-region stacks. The live Bedrock role + log group
already exist (CLI-created), so a plain deploy would collide — these
need cdk import / changeset adoption, not cdk deploy. Code + diff
captured for review, NOT deployed.
Refs: INFRA-91, INFRA-16
* Drop vault access policy from this deploy; tracked in INFRA-94 (kept governance lock codify + selection)
2026-06-08 17:03:18 -04:00
|
|
|
|
// 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/<orgId>/*
|
|
|
|
|
|
// bucket-policy PutObject statement (scoped to this trail's SourceArn).
|
|
|
|
|
|
isOrganizationTrail: true,
|
|
|
|
|
|
orgId,
|
2026-05-29 17:44:55 -04:00
|
|
|
|
includeGlobalServiceEvents: true,
|
|
|
|
|
|
enableFileValidation: true,
|
|
|
|
|
|
sendToCloudWatchLogs: true,
|
2026-06-10 14:44:33 -04:00
|
|
|
|
cloudWatchLogGroup: trailLogGroup,
|
2026-05-29 17:44:55 -04:00
|
|
|
|
managementEvents: cloudtrail.ReadWriteType.ALL,
|
2026-07-07 15:47:41 -04:00
|
|
|
|
// 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,
|
|
|
|
|
|
],
|
2026-05-29 17:44:55 -04:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-01 17:56:12 -04:00
|
|
|
|
// ── Day 1 detective layer + governance toggles ──
|
|
|
|
|
|
// Config (H-2), GuardDuty (H-3), Security Hub (H-4), Access Analyzer (M-5).
|
2026-07-14 13:53:07 -04:00
|
|
|
|
new DetectiveControls(this, "DetectiveControls", {
|
|
|
|
|
|
namePrefix: "seahaven",
|
|
|
|
|
|
});
|
2026-06-01 17:56:12 -04:00
|
|
|
|
// Monthly cost budget (M-10). Other governance toggles are CLI + documented.
|
|
|
|
|
|
new GovernanceToggles(this, "GovernanceToggles", {
|
2026-07-14 13:53:07 -04:00
|
|
|
|
budgetName: "seahaven-monthly-cost",
|
2026-06-01 17:56:12 -04:00
|
|
|
|
monthlyLimitUsd: props.monthlyBudgetUsd,
|
|
|
|
|
|
alertEmail: props.budgetAlertEmail,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-06-02 15:16:24 -04:00
|
|
|
|
// ── 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,
|
2026-06-10 14:44:33 -04:00
|
|
|
|
trailLogGroup,
|
2026-06-02 15:16:24 -04:00
|
|
|
|
});
|
2026-07-14 13:53:07 -04:00
|
|
|
|
new FlowLogs(this, "FlowLogs", {
|
|
|
|
|
|
namePrefix: "seahaven",
|
|
|
|
|
|
vpcIds: props.flowLogVpcIds,
|
|
|
|
|
|
});
|
2026-06-02 15:16:24 -04:00
|
|
|
|
new SesMonitoring(this, "SesMonitoring");
|
2026-06-02 16:42:24 -04:00
|
|
|
|
// Shared CloudFront WAF WebACL (M-17); ARN published to SSM for app stacks.
|
|
|
|
|
|
new AppWebAcl(this, "AppWebAcl");
|
2026-06-02 15:16:24 -04:00
|
|
|
|
|
2026-06-03 15:17:39 -04:00
|
|
|
|
// ── 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");
|
|
|
|
|
|
|
2026-05-29 17:44:55 -04:00
|
|
|
|
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 });
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|