|
Some checks failed
Bumps the minor-and-patch group with 3 updates: [aws-cdk-lib](https://github.com/aws/aws-cdk/tree/HEAD/packages/aws-cdk-lib), [constructs](https://github.com/aws/constructs) and [aws-cdk](https://github.com/aws/aws-cdk-cli/tree/HEAD/packages/aws-cdk). Updates `aws-cdk-lib` from 2.262.0 to 2.262.1 - [Release notes](https://github.com/aws/aws-cdk/releases) - [Changelog](https://github.com/aws/aws-cdk/blob/main/CHANGELOG.v2.alpha.md) - [Commits](https://github.com/aws/aws-cdk/commits/v2.262.1/packages/aws-cdk-lib) Updates `constructs` from 10.7.0 to 10.7.1 - [Release notes](https://github.com/aws/constructs/releases) - [Commits](https://github.com/aws/constructs/compare/v10.7.0...v10.7.1) Updates `aws-cdk` from 2.1132.0 to 2.1133.0 - [Release notes](https://github.com/aws/aws-cdk-cli/releases) - [Commits](https://github.com/aws/aws-cdk-cli/commits/aws-cdk@v2.1133.0/packages/aws-cdk) --- updated-dependencies: - dependency-name: aws-cdk-lib dependency-version: 2.262.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-and-patch - dependency-name: constructs dependency-version: 10.7.1 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: minor-and-patch - dependency-name: aws-cdk dependency-version: 2.1133.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: minor-and-patch ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> |
||
|---|---|---|
| .github | ||
| bin | ||
| lib | ||
| scripts | ||
| .gitignore | ||
| cdk.json | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
seahaven-org-baseline
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.
- AWS Architecture Map (Confluence, IT space, page 1540098)
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
changeableForto the offsite vault lock + redeploy. - Backup-only role. Restore policies and
allowRestoresare not granted; restores get a separate audited path once a restore-test process exists.
Pre-deploy gates (must clear before the first scheduled run):
- Enable S3 versioning on
seahaven-payments-csv-328440206208andgoogle-workspace-seahavenind.com(accounting.seahaven.comalready has it, audit C-9), or their jobs fail silently (folds in H-21). database-1is 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.- 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):
deny-root-user(p-2idoxozz) DENIES root sessions in every covered OU (SCPs evaluatests:AssumeRootsessions — 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.- GOTCHA (inheritance): p-2idoxozz is attached to
workloadsAND 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. - Freeze deploys of
seahaven-org-governancefor the window (a concurrent deploy would re-attach mid-recovery); verify no CD run in flight first. 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.- Do the root-only task, DELETE the credentials again (four-surface verify),
reattach the SCP(s), confirm
list-targets-for-policymatches the pre-detach capture and stack drift is IN_SYNC. - extdev extra:
external-dev-iam-guardrailsalso deniesiam: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; rootAccountMFAEnabled: 1; account parentou-nbuj-v0s9630uwith 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 bothEnabled. - Security Hub: org auto-enable on; both members
Enabled. - Org Access Analyzer
seahaven-org-analyzercreated; Config org aggregatorseahaven-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-MonthlyBillingCloudWatch alarm ($50 threshold onEstimatedCharges, routed tosite-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