Account-level AWS security baseline (CloudTrail C-1, AWS Backup C-7) — CDK TypeScript
Find a file
Adam Moussa 7cce026f4a
Some checks are pending
Deploy / deploy-management (push) Waiting to run
Deploy / deploy-external-dev (push) Waiting to run
Deploy / deploy-security (push) Waiting to run
Deploy / deploy-dev (push) Waiting to run
Deploy / deploy-prod (push) Waiting to run
chore: drop deleted tables from Phase2 backup selection (#59)
seahaven-conversations + seahaven-unanswered-questions (seahaven-slack-bot
teardown 2026-07-23), exec-aide (decommissioned 2026-07-10), and
internal-portal-data (internal-portal decommission) no longer exist;
their backup selections would fail nightly.
2026-07-23 16:21:04 -04:00
.github feat(prod): seahaven-prod DynamoDB CMK + site-alerts alarm topic (procurement-ingest migration Phase 0a) (#57) 2026-07-23 15:29:55 -04:00
bin feat(prod): seahaven-prod DynamoDB CMK + site-alerts alarm topic (procurement-ingest migration Phase 0a) (#57) 2026-07-23 15:29:55 -04:00
lib chore: drop deleted tables from Phase2 backup selection (#59) 2026-07-23 16:21:04 -04:00
scripts feat(prod): seahaven-prod DynamoDB CMK + site-alerts alarm topic (procurement-ingest migration Phase 0a) (#57) 2026-07-23 15:29:55 -04:00
.gitignore Initial account-baseline stack with CloudTrail (audit C-1) 2026-05-29 17:44:55 -04:00
cdk.json build(deps): migrate CDK app ts-node->tsx, adopt typescript 7 (INFRA-183) (#39) 2026-07-08 17:48:30 -04:00
package-lock.json chore(security): add explicit workflow permissions and bump aws-cdk-lib to 2.262.0 (#56) 2026-07-23 17:38:17 +00:00
package.json chore(security): add explicit workflow permissions and bump aws-cdk-lib to 2.262.0 (#56) 2026-07-23 17:38:17 +00:00
README.md Document centralized root access: README runbook + stack comment updates (#52) 2026-07-14 18:32:40 -04:00
tsconfig.json Initial account-baseline stack with CloudTrail (audit C-1) 2026-05-29 17:44:55 -04:00

seahaven-org-baseline

TypeScript AWS CDK CI

Organization-wide security and governance baseline for Sea Haven Industries, managed as a single CDK TypeScript app. Covers the management account (328440206208: primary baseline in us-east-1, secondary-region baselines in us-east-2/us-west-2, offsite backup vault in us-west-2) and org member accounts (first tenant: seahaven-external-dev 396287094661, absorbed from the retired seahaven-external-dev-baseline repo). This is where account-wide detective and recovery controls live, so they are versioned, reviewed, and drift-checked like any other stack.

History: this repo was seahaven-account-baseline (management account only) until 2026-07-14, when the external-dev member baseline was merged in and the repo renamed. Deployed CloudFormation stack names are unchanged.

Stacks (deployed by the CD workflow — one job per target account):

Stack Account Region Purpose
seahaven-account-baseline 328440206208 us-east-1 CloudTrail + detective controls (C-1)
seahaven-dynamodb-cmk 328440206208 us-east-1 Shared customer-managed KMS key for finance/PII DynamoDB tables; ARN published to SSM /seahaven/dynamodb/cmk-arn (INFRA-95 / M-3)
seahaven-regional-baseline-us-west-2 328440206208 us-west-2 Bedrock invocation logging (INFRA-91)
seahaven-regional-baseline-us-east-2 328440206208 us-east-2 Bedrock invocation logging + AWS Config recorder + Security Hub (INFRA-91 / INFRA-16)
seahaven-backup 328440206208 us-east-1 Primary AWS Backup vault + plan + role (C-7)
seahaven-backup-offsite 328440206208 us-west-2 Governance-locked offsite copy vault (C-7)
seahaven-org-governance 328440206208 us-east-1 AWS Organizations OU tree + SCPs (incl. the cdk-imported external-dev guardrails)
seahaven-external-dev-baseline 396287094661 us-east-1 Member-account baseline: Config, GuardDuty, Security Hub (FSBP + CIS v3.0), Access Analyzer, flow logs, budget
seahaven-security-baseline 001520130573 us-east-1 Member-account baseline for the delegated security-admin account (same construct set)
seahaven-dev-baseline 710827005802 us-east-1 Member-account baseline for internal dev/staging (org-managed detection — no local GuardDuty/SecurityHub)
seahaven-prod-baseline 011934824531 us-east-1 Member-account baseline for production workloads (org-managed detection; mgmt account frozen for new workloads)

CDK app

The repo is a single AWS CDK app written in TypeScript. cdk.json is the project config the cdk CLI reads on every command: its app key (npx ts-node bin/app.ts) tells CDK how to synthesize the app straight from the TypeScript source — no separate compile step needed for cdk synth / diff / deploy — and its context block carries the AWS CDK feature flags.

Path Role
cdk.json CDK config: app synth command, watch includes/excludes, context feature flags
bin/app.ts App entry point — instantiates every stack with an explicit kebab-case stackName and its target env (five accounts, per-account/per-region)
lib/*-stack.ts Stack definitions (one class per stack; larger stacks compose the constructs in lib/*.ts)
tsconfig.json TypeScript compiler options (outDir: cdk.out)
package.json Pinned aws-cdk-lib, CDK CLI, and the build / synth / diff / deploy npm scripts

bin/app.ts synthesizes eleven stacks across three regions and five accounts:

Construct id Stack name Account Region Source
account-baseline seahaven-account-baseline 328440206208 us-east-1 lib/account-baseline-stack.ts
dynamodb-cmk seahaven-dynamodb-cmk 328440206208 us-east-1 lib/dynamodb-cmk-stack.ts
regional-baseline-us-west-2 seahaven-regional-baseline-us-west-2 328440206208 us-west-2 lib/regional-baseline-stack.ts
regional-baseline-us-east-2 seahaven-regional-baseline-us-east-2 328440206208 us-east-2 lib/regional-baseline-stack.ts
backup-offsite seahaven-backup-offsite 328440206208 us-west-2 lib/backup-offsite-stack.ts
backup seahaven-backup 328440206208 us-east-1 lib/backup-stack.ts
org-governance seahaven-org-governance 328440206208 us-east-1 lib/org-governance-stack.ts
external-dev-baseline seahaven-external-dev-baseline 396287094661 us-east-1 lib/member-baseline-stack.ts
security-baseline seahaven-security-baseline 001520130573 us-east-1 lib/member-baseline-stack.ts
dev-baseline seahaven-dev-baseline 710827005802 us-east-1 lib/member-baseline-stack.ts (orgManagedDetection)
prod-baseline seahaven-prod-baseline 011934824531 us-east-1 lib/member-baseline-stack.ts (orgManagedDetection)

Member-account stacks deploy with per-account credentials — the CD workflow runs one job per account, each assuming that account's OIDC deploy role. Local deploys/diffs assume OrganizationAccountAccessRole in the target account.

Account OIDC deploy role Repo secret
396287094661 (external-dev) githubdeploy-seahaven-external-dev-baseline AWS_DEPLOY_ROLE_ARN_EXTDEV
001520130573 (security) githubdeploy-seahaven-org-baseline AWS_DEPLOY_ROLE_ARN_SECURITY
710827005802 (dev) githubdeploy-seahaven-org-baseline AWS_DEPLOY_ROLE_ARN_DEV
011934824531 (prod) githubdeploy-seahaven-org-baseline AWS_DEPLOY_ROLE_ARN_PROD

Shared constructs (DetectiveControls, FlowLogs, GovernanceToggles) are prefix-parameterized — construct ids and physical names must stay byte-identical to the deployed stacks (logical IDs are path-derived). Accounts enrolled by the org delegated admin (post 2026-07-14) set orgManagedDetection: true: the GuardDuty detector + Security Hub hub come from the org, while STANDARDS (FSBP + CIS v3.0) and the account analyzer stay CFN-owned (org AutoEnableStandards is NONE — the DEFAULT setting enrolls legacy CIS v1.2.0). Enrollment (member Enabled in GuardDuty + Security Hub) is a hard precondition for such a stack's first deploy.

backup declares an explicit dependency on backup-offsite so the offsite copy vault exists before the primary plan that copies into it. Stack names are set explicitly to enforce kebab-case (CDK defaults to PascalCase). Synthesized CloudFormation templates land in cdk.out/ (git-ignored).

Common commands:

npm ci                         # install pinned deps
npm run build                  # tsc type-check (compiles to cdk.out/)
npx cdk synth                  # synthesize CloudFormation for all stacks
npx cdk diff                   # diff synthesized stacks against deployed state
npx cdk deploy --all           # deploy every stack
npx cdk deploy <stack-name>    # deploy a single stack

The --context <key>=<value> flag overrides cdk.json context at the command line (e.g. the encryptTrailLogGroup toggle under Monitoring + logging).

Documentation

The canonical map of Sea Haven's AWS infrastructure lives in Confluence. This project's seahaven-account-baseline, seahaven-backup, and seahaven-backup-offsite stacks are represented there as Mermaid subgraphs.

What it deploys

CloudTrail (audit finding C-1)

Resource Logical ID Notes
Multi-region trail Trail (seahaven-org-trail) Management events read+write, global service events, log-file validation on, CloudTrail Insights on (ApiCallRate + ApiErrorRate, §37)
Log bucket TrailLogBucket (seahaven-cloudtrail-logs-328440206208) Private (Block Public Access all), SSE-KMS, versioned, TLS-only, Object Lock GOVERNANCE 365d, lifecycle (Glacier @90d, expire @365d), server access logging → seahaven-s3-access-logs
KMS CMK TrailKey (alias/cloudtrail-logs) Encrypts log files; automatic rotation enabled
CloudWatch Logs group created by the L2 Trail 365-day retention; this is the group the CIS Section 4 metric filters (H-1) attach to

Data flow: API activity across all regions → CloudTrail → (a) KMS-encrypted, Object-Locked S3 bucket for durable/tamper-resistant storage and (b) CloudWatch Logs for real-time querying and metric-filter alarms.

Compliance impact: closes CIS 3.1 (multi-region trail), 3.2 (log-file validation), 3.4 (CloudWatch Logs integration), 3.6 (bucket access logging), 3.7 (KMS CMK encryption), and 3.8 (CMK rotation). Unblocks CIS Section 4 / finding H-1 (metric filters + alarms now have a log group to target).

Design decisions

  • Management events only. Object-level S3/Lambda data events (CIS 3.10/3.11) are deferred to control cost; revisit with targeted S3 write data events on sensitive buckets (payments / accounting / kb) if needed.
  • Object Lock GOVERNANCE, not COMPLIANCE. Tamper-resistant but still deletable by a principal holding s3:BypassGovernanceRetention — avoids the irreversibility of COMPLIANCE mode. Revisit if a stricter posture is required.
  • RETAIN on the bucket and KMS key so a stack teardown never destroys the audit trail.

AWS Backup (audit finding C-7)

Phase 1 ("critical data first") of fixing the account's complete lack of AWS Backup. Protects the data stores with no offsite leg today and copies each recovery point cross-region into a governance-locked vault.

Resource Logical ID Notes
Primary vault seahaven-primary (us-east-1) KMS-CMK encrypted, unlocked (working copy), RETAIN
Offsite vault seahaven-offsite (us-west-2) KMS-CMK encrypted, Vault Lock GOVERNANCE (min-retention 30d, no cooling-off window), RETAIN
Backup plan seahaven-critical-daily Daily 06:00 UTC, delete-after 35d, cross-region CopyAction → offsite (retain 90d)
Service role seahaven-backup-service-role Backup-only (Backup + S3-Backup managed policies); restore perms intentionally deferred

Phase-1 scope (selected by explicit ARN, not tags, to avoid drifting other stacks): RDS proposal-system-db, DynamoDB PaymentsDashboard, DynamoDB purchase-orders, S3 accounting.seahaven.com, seahaven-payments-csv-328440206208, google-workspace-seahavenind.com. (RDS database-1 was originally in this set but was retired 2026-06-03 — audit H-19, idle 0 conn/60d — and removed from the selection; its final encrypted recovery point is retained in seahaven-offsite for 7 years.)

Coexists with existing EBS DLM snapshots and DynamoDB PITR — it supplements them with the missing offsite + immutable leg; it does not replace them.

Design decisions:

  • Governance lock first, not compliance. Recovery points can't be silently deleted, but a principal with explicit permission can still intervene while we validate. Graduate to COMPLIANCE (irreversible) later by adding changeableFor to the offsite vault lock + redeploy.
  • Backup-only role. Restore policies and allowRestores are not granted; restores get a separate audited path once a restore-test process exists.

Pre-deploy gates (must clear before the first scheduled run):

  1. Enable S3 versioning on seahaven-payments-csv-328440206208 and google-workspace-seahavenind.com (accounting.seahaven.com already has it, audit C-9), or their jobs fail silently (folds in H-21).
  2. database-1 is unencrypted (H-19): smoke-test an on-demand backup + copy of it to us-west-2 first; if the copy fails, encrypt it or drop it from the copy.
  3. Enable DynamoDB PITR (H-7) on the two tables for between-window recovery.

AWS Backup phase 2 (audit Day 4)

Expands the same seahaven-critical-daily plan to every remaining data store, so all of DynamoDB + EBS get the offsite + immutable leg ("offsite for everything").

Resource Logical ID Notes
Phase-2 selection Plan/Phase2Resources (phase2-offsite-everything) Same plan, same seahaven-backup-service-role, same daily + cross-region copy rule

Phase-2 scope: the 15 remaining DynamoDB tables (all except the two phase-1 financial tables + the deleted ledgerflow tables) and all 9 in-use EBS volumes, again by explicit ARN — tag-based selection was deliberately avoided because the file-share volumes are standalone-managed and the tables are owned by other stacks, so tagging here would drift them.

No IAM change: AWSBackupServiceRolePolicyForBackup already grants the DynamoDB/RDS/EBS backup actions, so phase 2 reuses the phase-1 role unchanged (cross-reviewed, no BLOCK).

Known tradeoff (→ Jira INFRA-31): explicit-ARN EBS entries go stale if a volume is replaced (new volume id), silently dropping it from backup. Migrating the EBS portion to tag-based selection (with the tag codified in each owning stack) is the resilient follow-up; scheduled drift detection is the interim backstop.

Also enabled outside this stack (audit H-7, via CLI — codify per stack → INFRA-30): PITR + DeletionProtectionEnabled on 12 more DynamoDB tables (account-wide PITR now 19/21).

Detective controls + budget (audit Day 1)

Account-level detective layer, in lib/detective-controls.ts, plus the cost budget in lib/governance-toggles.ts. Scope is us-east-1 only (all workloads live here); multi-region coverage is a follow-up.

Resource Logical ID Finding Notes
Config delivery bucket seahaven-config-328440206208 H-2 Private (BPA all), SSE-S3, versioned, TLS-only, 365d lifecycle
Config recorder role seahaven-config-recorder-role H-2 AWS_ConfigRole + scoped S3 delivery; IAM cross-reviewed
Config recorder + channel DetectiveControls/ConfigPutRecorder, ConfigPutChannel, ConfigStartRecorder H-2 AwsCustomResource calls PutConfigurationRecorder → PutDeliveryChannel → StartConfigurationRecorder in sequence; idempotent upsert avoids the L1 CFN deadlock (INFRA-17). Custom-resource role cross-reviewed.
GuardDuty detector DetectiveControls/GuardDutyDetector H-3 Findings every 15 min
Security Hub DetectiveControls/SecurityHub H-4 FSBP v1.0.0 + CIS v3.0.0; controls evaluate once Config is recording
Access Analyzer seahaven-account-analyzer M-5 ACCOUNT external-access analyzer (free)
Monthly budget GovernanceToggles/MonthlyCostBudget (seahaven-monthly-cost) M-10 $1,200/mo, 80%/100% actual + 100% forecast → adam@seahavenind.com

Config recorder + delivery channel are managed by AwsCustomResource (INFRA-17). The L1 AWS::Config::ConfigurationRecorder deadlocks the stack (recorder never reaches CREATE_COMPLETE without a delivery channel; channel can't be created without a recorder — hit 2026-06-01). The custom resource sidesteps this by calling the Config SDK directly: Put* is an upsert, so the deploy adopts the existing CLI-created recorder and channel without destroying them. Active recording is never interrupted.

CLI-applied governance toggles (no CloudFormation resource)

These account toggles have no native CloudFormation resource, so they are applied via CLI and recorded here — per account (they are account-scoped; a new member account has NONE of them until applied). Applied: 328440206208 (2026-06-01); 001520130573, 710827005802, 011934824531 (2026-07-14 — EBS encryption-by-default + password policy, verified per account; Inspector2 via delegated admin; cost-allocation tags are org-level).

# M-3  EBS encryption-by-default (new volumes; existing 5 plaintext volumes are H-19-adjacent)
aws ec2 enable-ebs-encryption-by-default --region us-east-1

# M-6  Inspector2 (EC2 + Lambda + ECR)
aws inspector2 enable --resource-types EC2 LAMBDA ECR --region us-east-1

# M-7  IAM password policy (CIS 1.8/1.9): >=14 chars, full complexity, no reuse of last 24
aws iam update-account-password-policy \
  --minimum-password-length 14 \
  --require-symbols --require-numbers \
  --require-uppercase-characters --require-lowercase-characters \
  --allow-users-to-change-password --password-reuse-prevention 24

# M-11  Activate cost-allocation tags (only activates keys already seen on resources)
aws ce update-cost-allocation-tags-status --cost-allocation-tags-status \
  'TagKey=Project,Status=Active' 'TagKey=Owner,Status=Active' 'TagKey=Environment,Status=Active'

Centralized root access management (org-level, no CloudFormation resource)

STATUS: ENABLED 2026-07-14, all member root credentials DELETED (evidence: ~/Documents/repositories/_audits/centralized-root-access-evidence-2026-07-14.md). Member accounts have NO root credentials; the only root path is a privileged session from the management account. The management account's own root is NOT centrally manageable and stays password+MFA hardened.

# Enable (mgmt account). ORDER MATTERS: trusted access must be enabled
# explicitly first — enable-organizations-root-credentials-management does
# NOT auto-enable it (fails ServiceAccessNotEnabledException).
aws organizations enable-aws-service-access --service-principal iam.amazonaws.com
aws iam enable-organizations-root-credentials-management
aws iam enable-organizations-root-sessions

# Periodic verification (add to governance checks): expect BOTH features
aws iam list-organizations-features

Audit / delete member root credentials (task-scoped root sessions, 15-min):

aws sts assume-root --target-principal <acct> \
  --task-policy-arn arn=arn:aws:iam::aws:policy/root-task/IAMAuditRootUserCredentials
# then, with the session creds (no --user-name; root has none):
#   get-login-profile / list-mfa-devices / list-access-keys / list-signing-certificates
aws sts assume-root --target-principal <acct> \
  --task-policy-arn arn=arn:aws:iam::aws:policy/root-task/IAMDeleteRootUserCredentials
#   delete-login-profile; deactivate-mfa-device --serial-number <arn>
# GOTCHA: the delete task policy explicitly DENIES iam:DeleteVirtualMFADevice —
# remove the orphaned virtual-device OBJECT via OrganizationAccountAccessRole.
# DONE = four surfaces clear: login profile NoSuchEntity, MFA/keys/certs all empty.

Root recovery runbook (proven by drill on prod 2026-07-14):

  1. deny-root-user (p-2idoxozz) DENIES root sessions in every covered OU (SCPs evaluate sts:AssumeRoot sessions — the principal is the member root ARN). Recovery therefore starts with a manual, temporary detach (aws organizations detach-policy — NOT a CDK deploy), timeboxed minutes.
  2. GOTCHA (inheritance): p-2idoxozz is attached to workloads AND its child OUs — for an account under workloads/, detach from BOTH the child OU and workloads, or the inherited deny still applies. Allow ~10s propagation.
  3. Freeze deploys of seahaven-org-governance for the window (a concurrent deploy would re-attach mid-recovery); verify no CD run in flight first.
  4. aws sts assume-root --target-principal <acct> --task-policy-arn arn=arn:aws:iam::aws:policy/root-task/IAMCreateRootUserPassword → create-login-profile (no args) restores a login profile.
  5. Do the root-only task, DELETE the credentials again (four-surface verify), reattach the SCP(s), confirm list-targets-for-policy matches the pre-detach capture and stack drift is IN_SYNC.
  6. extdev extra: external-dev-iam-guardrails also denies iam:CreateLoginProfile — recovery there needs that SCP temporarily detached too. The extdev OU sits at the 5-SCP hard quota: any new guardrail for extdev must attach at the ACCOUNT (396287094661) or consolidate into an existing policy.

New-account flow (supersedes root-harden-before-OU-move): create the account at the org ROOT → it has no root credentials from birth (verify with the audit session) → bootstrap + deploy role + baseline → move-account into the target OU → verify SCP inheritance + region-lock canary. No mailbox or MFA enrollment step. Root-usage monitoring: GuardDuty Policy:IAMUser/RootCredentialUsage + CIS 4.3 alarm remain active.

Delegated security administration (Phase 3, no CloudFormation resource)

Account seahaven-security (001520130573) is the org's delegated administrator for the detective services. STATUS: APPLIED 2026-07-14, verified (see evidence below). The hard preconditions were enforced before the first delegation call (security review SEC-BASE-B/D — never delegate to an account with unhardened root or before its baseline stack exists):

  • Baseline stack UPDATE_COMPLETE; root AccountMFAEnabled: 1; account parent ou-nbuj-v0s9630u with SCPs deny-root-user + protect-security-baseline + security-guardrails inherited.

Verification evidence (2026-07-14):

  • organizations list-delegated-administrators → 001520130573 (all five service principals registered).
  • GuardDuty: AutoEnableOrganizationMembers: ALL; members 328440206208 + 396287094661 both Enabled.
  • Security Hub: org auto-enable on; both members Enabled.
  • Org Access Analyzer seahaven-org-analyzer created; Config org aggregator seahaven-org-aggregator (AllAwsRegions) on the Config SLR; Inspector2 auto-enable ec2/ecr/lambda + both members associated.
  • Findings flow verified end-to-end: GuardDuty sample findings created in member 396287094661 were listed and fully readable from the admin detector in 001520130573 (Recon:EC2/PortProbeUnprotectedPort, AccountId 396287094661, sample=true), then archived.

The delegation runbook (all calls idempotent, run from the management account):

# GuardDuty: delegate + auto-enable all org members (adopts existing detectors)
aws guardduty enable-organization-admin-account --admin-account-id 001520130573
# then AS 001520130573: update-organization-configuration --auto-enable-organization-members ALL
#   + create-members for pre-existing accounts (mgmt, external-dev)

# Security Hub: delegate + auto-enable new members
aws securityhub enable-organization-admin-account --admin-account-id 001520130573
# then AS 001520130573: update-organization-configuration --auto-enable

# IAM Access Analyzer: delegate + ORGANIZATION-scoped analyzer
aws organizations register-delegated-administrator \
  --account-id 001520130573 --service-principal access-analyzer.amazonaws.com
# then AS 001520130573: create-analyzer --type ORGANIZATION

# Config: delegate the aggregator (recorders stay per-account in the baselines;
# the aggregator's recorder-status view is the drift detector)
aws organizations register-delegated-administrator \
  --account-id 001520130573 --service-principal config.amazonaws.com
# then AS 001520130573: put-configuration-aggregator --organization-aggregation-source

# Inspector2: delegate + associate members
aws inspector2 enable-delegated-admin-account --delegated-admin-account-id 001520130573

ONLY once delegation is live AND auto-enrollment is verified (a new member shows enrolled in the security account's GuardDuty/Security Hub consoles): new member accounts are then detected/enrolled automatically, and future member baselines can drop per-account GuardDuty/SecurityHub resources. Until then, every member baseline MUST keep them (slimming the existing member stacks is a separate, verification-gated change; note the DA account's own CFN-owned detector/hub become co-managed after delegation — never rename/remove them via CFN while the account is delegated admin).

Accepted read-surface note (SEC-BASE-I): the org Config aggregator + ORGANIZATION Access Analyzer give principals in 001520130573 org-wide READ of resource configurations (including recorded Lambda env vars) and IAM policies. Main-branch write access to this repo therefore implies that read surface — verify no prod Lambda keeps secrets in env vars before creating the aggregator, and keep branch protection tight.

L-8 (billing-metrics preference) is OUTSTANDING — console only. Enabling the CloudWatch EstimatedCharges metric in us-east-1 requires turning on Receive Billing Alerts under Billing → Billing preferences; there is no public API/CLI. The M-10 budget (seahaven-monthly-cost, 80%/100% actual + 100% forecast) provides cost alerting independent of that metric.

The legacy, manually-created AWS-MonthlyBilling CloudWatch alarm ($50 threshold on EstimatedCharges, routed to site-alerts) was deleted 2026-07-07 as unmanaged drift: it was fully redundant with the M-10 budget, sat permanently in ALARM (spend has far exceeded $50/mo), and was never in IaC. Billing alerting is now solely the managed M-10 budget. To restore the old alarm if ever needed: aws cloudwatch put-metric-alarm --alarm-name AWS-MonthlyBilling --namespace AWS/Billing --metric-name EstimatedCharges --dimensions Name=Currency,Value=USD --statistic Maximum --period 86400 --evaluation-periods 1 --threshold 50 --comparison-operator GreaterThanThreshold --alarm-actions arn:aws:sns:us-east-1:328440206208:site-alerts.

Monitoring + logging (audit Day 2)

Resource Logical ID Finding Notes
CIS metric filters + alarms CisMonitoring/* H-1 15 filters (CIS 4.1–4.15) on the CloudTrail log group, each with an alarm → seahaven-cis-alarms. ALARM-only actions (no OK). 4.16 = Security Hub (Day 1)
CIS alarm topic seahaven-cis-alarms H-1 SNS, SSE (alias/aws/sns), email sub to adam@seahavenind.com
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
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:

# 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 CloudWatch Logs group by name (seahaven-account-baseline-TrailLogGroup4CBE3AF5-…), imported read-only so the live audit trail is never replaced. Stable unless the Trail is recreated.

H-14 bucket policy note: the flow-logs delivery policy keeps s3:x-amz-acl=bucket-owner-full-control and the arn:aws:logs:…:* source-ARN wildcard — both are required by AWS's documented flow-logs-to-S3 policy (flow-logs-s3-permissions.html). A cross-review suggested dropping them; that was rejected as it would break delivery. s3:ListBucket was dropped (not needed).

M-13 follow-up: associate seahaven-email-events as the default config set on the live sending identities to capture events from existing senders:

aws sesv2 put-email-identity-configuration-set-attributes \
  --email-identity int.seahaven.com --configuration-set-name seahaven-email-events

Log-group retention + alarm wiring (audit L-4, L-5)

Applied via CLI (auto-created groups spread across stacks; one alarm in another stack). Applied 2026-06-02.

# L-4  90-day retention on the 13 never-expire log groups (CodeBuild + CDK helpers)
for lg in <the 13 groups>; do aws logs put-retention-policy --log-group-name "$lg" --retention-in-days 90; done

# L-5  wire the actionless forgejo backup-verification alarm to site-alerts
aws cloudwatch put-metric-alarm --alarm-name forgejo-backup-verification-errors \
  --alarm-actions arn:aws:sns:us-east-1:328440206208:site-alerts  # (preserve existing alarm config)

Roadmap (same stack)

Detective layer multi-region expansion (GuardDuty/Config/Security Hub beyond us-east-1). Backup: phase 2 is deployed (see above); remaining is migrating the phase-2 EBS entries to tag-based selection (INFRA-31) and graduating the offsite vault to compliance mode.

Deploy

CI/CD via the org reusable workflows (ci-typescript-cdk.yaml, cd-cdk.yaml); pushes to main deploy through the OIDC role in secrets.AWS_DEPLOY_ROLE_ARN. Local: npm ci && npm run build && npx cdk diff.

npx cdk deploy seahaven-account-baseline

Verify

aws cloudtrail get-trail-status --name seahaven-org-trail        # IsLogging: true
aws cloudtrail describe-trails --trail-name-list seahaven-org-trail
aws cloudtrail validate-logs --trail-arn <arn> --start-time <t>  # digest integrity

AWS Backup (C-7):

aws backup list-backup-vaults                                    # seahaven-primary
aws backup list-backup-vaults --region us-west-2                 # seahaven-offsite
aws backup describe-backup-vault --backup-vault-name seahaven-offsite --region us-west-2  # Locked, MinRetentionDays
aws backup get-backup-plan --backup-plan-id <id>                 # daily rule + CopyAction
# Smoke test: on-demand backup of one resource, then confirm the cross-region copy lands
aws backup start-backup-job --backup-vault-name seahaven-primary \
  --resource-arn arn:aws:rds:us-east-1:328440206208:db:proposal-system-db \
  --iam-role-arn arn:aws:iam::328440206208:role/seahaven-backup-service-role
aws backup list-copy-jobs --region us-west-2                     # copy to offsite present + COMPLETED
# Phase-2 selections live on the plan:
aws backup list-backup-selections --backup-plan-id <id> --query 'BackupSelectionsList[].SelectionName'  # critical-data + phase2-offsite-everything

Detective layer + governance (Day 1):

aws configservice describe-configuration-recorder-status         # recording: true
aws guardduty list-detectors                                     # one detector id
aws securityhub get-enabled-standards                            # FSBP + CIS v3.0.0
aws accessanalyzer list-analyzers                                # seahaven-account-analyzer ACTIVE
aws inspector2 batch-get-account-status --region us-east-1       # ec2/ecr/lambda ENABLED
aws iam get-account-password-policy                              # length 14, reuse 24
aws ec2 get-ebs-encryption-by-default --region us-east-1         # EbsEncryptionByDefault: true
aws budgets describe-budgets --account-id 328440206208           # seahaven-monthly-cost $1,200
aws ce list-cost-allocation-tags --status Active                 # Project/Owner/Environment Active