seahaven-org-baseline/README.md

377 lines
22 KiB
Markdown
Raw Normal View History

Merge external-dev member baseline; rename to seahaven-org-baseline (#43) * Parameterize baseline constructs for multi-account reuse DetectiveControls, FlowLogs, and GovernanceToggles were forked into seahaven-external-dev-baseline with only physical-name and VPC-sourcing differences. Prefix/name props let one implementation serve both accounts; synthesized templates are unchanged (verified: empty cdk diff against all deployed stacks). * Absorb external-dev member baseline stack Moves seahaven-external-dev-baseline's stack in as MemberBaselineStack, construct ids and physical names byte-identical to the deployed stack (logical IDs are path-derived; empty cdk diff verified via change set against 396287094661). Retires the forked repo so member-account baselines share one drift surface and one dependency pin. * Rename package to seahaven-org-baseline Prepares the repo rename: the app now spans the management account and org member accounts, so 'account-baseline' undersells the scope. README documents the two-account deploy topology and logical-ID constraints. * Commit extdev flow-log VPC ids in code, not -c context Security review SH-ORG-004 (confirmed high): with the ids sourced from ephemeral cdk context, any context-less deploy silently removes every flow log in the isolated account. A committed list makes the attachment set reviewable and immune to a forgotten -c flag. Empty list matches the deployed stack (zero diff). * Split CD into per-account deploy jobs The app now spans two AWS accounts; cdk deploy --all under one role fails on the other account's stacks (security review IAC-01). Each job passes explicit stack selectors and its own account's OIDC role via the new cd-cdk stacks input.
2026-07-14 13:53:07 -04:00
# seahaven-org-baseline
![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white)
![AWS CDK](https://img.shields.io/badge/AWS-CDK-FF9900?logo=amazonaws&logoColor=white)
Merge external-dev member baseline; rename to seahaven-org-baseline (#43) * Parameterize baseline constructs for multi-account reuse DetectiveControls, FlowLogs, and GovernanceToggles were forked into seahaven-external-dev-baseline with only physical-name and VPC-sourcing differences. Prefix/name props let one implementation serve both accounts; synthesized templates are unchanged (verified: empty cdk diff against all deployed stacks). * Absorb external-dev member baseline stack Moves seahaven-external-dev-baseline's stack in as MemberBaselineStack, construct ids and physical names byte-identical to the deployed stack (logical IDs are path-derived; empty cdk diff verified via change set against 396287094661). Retires the forked repo so member-account baselines share one drift surface and one dependency pin. * Rename package to seahaven-org-baseline Prepares the repo rename: the app now spans the management account and org member accounts, so 'account-baseline' undersells the scope. README documents the two-account deploy topology and logical-ID constraints. * Commit extdev flow-log VPC ids in code, not -c context Security review SH-ORG-004 (confirmed high): with the ids sourced from ephemeral cdk context, any context-less deploy silently removes every flow log in the isolated account. A committed list makes the attachment set reviewable and immune to a forgotten -c flag. Empty list matches the deployed stack (zero diff). * Split CD into per-account deploy jobs The app now spans two AWS accounts; cdk deploy --all under one role fails on the other account's stacks (security review IAC-01). Each job passes explicit stack selectors and its own account's OIDC role via the new cd-cdk stacks input.
2026-07-14 13:53:07 -04:00
![CI](https://github.com/Sea-Haven-Industries/seahaven-org-baseline/actions/workflows/ci.yaml/badge.svg)
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-external-dev-baseline` | 396287094661 | us-east-1 | Member-account baseline: Config, GuardDuty, Security Hub (FSBP + CIS v3.0), Access Analyzer, flow logs, budget |
Add AWS Backup with offsite vault (audit C-7) (#3) * Add AWS Backup with offsite vault (audit C-7) The account had zero AWS Backup vaults/plans, so 22 of 23 data stores had no immutable, cross-region recovery path (audit finding C-7). One ransomware event or rogue delete would erase primary plus same-region snapshots/PITR. Phase 1 ("critical data first") protects the seven highest-risk stores with no offsite leg today (2 RDS, 2 DynamoDB, 3 S3) via a daily plan in a new us-east-1 vault, copied cross-region into a governance-locked us-west-2 vault. Governance (not compliance) mode first so the plan can be validated before committing to irreversible immutability. The backup service role is backup-only (no restore policies) to stay least-privilege; restores get a separate audited path later. Resources are selected by explicit ARN to avoid drifting the stacks that own them. Deploys via the shared cdk deploy --all alongside the C-1 CloudTrail stack. See the README pre-deploy gates (S3 versioning, database-1 unencrypted copy smoke-test, DynamoDB PITR) before the first run. * Grant AWS Backup service use of vault CMKs The L2 BackupVault does not grant the backup service principal use of a customer-managed key; the synthesized key policy only delegated to account IAM. Cross-region copy of encrypted RDS/EBS recovery points uses KMS grants on the destination key, so without an explicit grant those copy jobs fail — and silently, since the account has no CloudTrail yet. Add backup.amazonaws.com crypto + CreateGrant statements to both vault keys, scoped by aws:SourceAccount (cross-review BLOCK 2; mirrors the discipline used on the C-1 CloudTrail key). Same class of bug the C-1 cross-review caught on the CloudTrail CMK.
2026-05-29 18:06:17 -04:00
## 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` (account `328440206208`, 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 |
Merge external-dev member baseline; rename to seahaven-org-baseline (#43) * Parameterize baseline constructs for multi-account reuse DetectiveControls, FlowLogs, and GovernanceToggles were forked into seahaven-external-dev-baseline with only physical-name and VPC-sourcing differences. Prefix/name props let one implementation serve both accounts; synthesized templates are unchanged (verified: empty cdk diff against all deployed stacks). * Absorb external-dev member baseline stack Moves seahaven-external-dev-baseline's stack in as MemberBaselineStack, construct ids and physical names byte-identical to the deployed stack (logical IDs are path-derived; empty cdk diff verified via change set against 396287094661). Retires the forked repo so member-account baselines share one drift surface and one dependency pin. * Rename package to seahaven-org-baseline Prepares the repo rename: the app now spans the management account and org member accounts, so 'account-baseline' undersells the scope. README documents the two-account deploy topology and logical-ID constraints. * Commit extdev flow-log VPC ids in code, not -c context Security review SH-ORG-004 (confirmed high): with the ids sourced from ephemeral cdk context, any context-less deploy silently removes every flow log in the isolated account. A committed list makes the attachment set reviewable and immune to a forgotten -c flag. Empty list matches the deployed stack (zero diff). * Split CD into per-account deploy jobs The app now spans two AWS accounts; cdk deploy --all under one role fails on the other account's stacks (security review IAC-01). Each job passes explicit stack selectors and its own account's OIDC role via the new cd-cdk stacks input.
2026-07-14 13:53:07 -04:00
`bin/app.ts` synthesizes seven stacks across three regions and two accounts:
Merge external-dev member baseline; rename to seahaven-org-baseline (#43) * Parameterize baseline constructs for multi-account reuse DetectiveControls, FlowLogs, and GovernanceToggles were forked into seahaven-external-dev-baseline with only physical-name and VPC-sourcing differences. Prefix/name props let one implementation serve both accounts; synthesized templates are unchanged (verified: empty cdk diff against all deployed stacks). * Absorb external-dev member baseline stack Moves seahaven-external-dev-baseline's stack in as MemberBaselineStack, construct ids and physical names byte-identical to the deployed stack (logical IDs are path-derived; empty cdk diff verified via change set against 396287094661). Retires the forked repo so member-account baselines share one drift surface and one dependency pin. * Rename package to seahaven-org-baseline Prepares the repo rename: the app now spans the management account and org member accounts, so 'account-baseline' undersells the scope. README documents the two-account deploy topology and logical-ID constraints. * Commit extdev flow-log VPC ids in code, not -c context Security review SH-ORG-004 (confirmed high): with the ids sourced from ephemeral cdk context, any context-less deploy silently removes every flow log in the isolated account. A committed list makes the attachment set reviewable and immune to a forgotten -c flag. Empty list matches the deployed stack (zero diff). * Split CD into per-account deploy jobs The app now spans two AWS accounts; cdk deploy --all under one role fails on the other account's stacks (security review IAC-01). Each job passes explicit stack selectors and its own account's OIDC role via the new cd-cdk stacks input.
2026-07-14 13:53:07 -04:00
| 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` |
| `external-dev-baseline` | `seahaven-external-dev-baseline` | 396287094661 | us-east-1 | `lib/member-baseline-stack.ts` |
The member-account stack (`external-dev-baseline`) deploys with credentials for
**396287094661** — the CD workflow runs it as a separate job assuming that
account's OIDC deploy role (`githubdeploy-seahaven-external-dev-baseline`,
repo secret `AWS_DEPLOY_ROLE_ARN_EXTDEV`). Local deploys/diffs of that stack
assume `OrganizationAccountAccessRole` in 396287094661. Its shared constructs
(`DetectiveControls`, `FlowLogs`, `GovernanceToggles`) are prefix-parameterized
(`seahaven` vs `seahaven-extdev`) — construct ids and physical names must stay
byte-identical to the deployed stack (logical IDs are path-derived).
`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](https://seahaven.atlassian.net/wiki/spaces/IT/pages/1540098)** (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.
Add AWS Backup with offsite vault (audit C-7) (#3) * Add AWS Backup with offsite vault (audit C-7) The account had zero AWS Backup vaults/plans, so 22 of 23 data stores had no immutable, cross-region recovery path (audit finding C-7). One ransomware event or rogue delete would erase primary plus same-region snapshots/PITR. Phase 1 ("critical data first") protects the seven highest-risk stores with no offsite leg today (2 RDS, 2 DynamoDB, 3 S3) via a daily plan in a new us-east-1 vault, copied cross-region into a governance-locked us-west-2 vault. Governance (not compliance) mode first so the plan can be validated before committing to irreversible immutability. The backup service role is backup-only (no restore policies) to stay least-privilege; restores get a separate audited path later. Resources are selected by explicit ARN to avoid drifting the stacks that own them. Deploys via the shared cdk deploy --all alongside the C-1 CloudTrail stack. See the README pre-deploy gates (S3 versioning, database-1 unencrypted copy smoke-test, DynamoDB PITR) before the first run. * Grant AWS Backup service use of vault CMKs The L2 BackupVault does not grant the backup service principal use of a customer-managed key; the synthesized key policy only delegated to account IAM. Cross-region copy of encrypted RDS/EBS recovery points uses KMS grants on the destination key, so without an explicit grant those copy jobs fail — and silently, since the account has no CloudTrail yet. Add backup.amazonaws.com crypto + CreateGrant statements to both vault keys, scoped by aws:SourceAccount (cross-review BLOCK 2; mirrors the discipline used on the C-1 CloudTrail key). Same class of bug the C-1 cross-review caught on the CloudTrail CMK.
2026-05-29 18:06:17 -04:00
### 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`,
Add AWS Backup with offsite vault (audit C-7) (#3) * Add AWS Backup with offsite vault (audit C-7) The account had zero AWS Backup vaults/plans, so 22 of 23 data stores had no immutable, cross-region recovery path (audit finding C-7). One ransomware event or rogue delete would erase primary plus same-region snapshots/PITR. Phase 1 ("critical data first") protects the seven highest-risk stores with no offsite leg today (2 RDS, 2 DynamoDB, 3 S3) via a daily plan in a new us-east-1 vault, copied cross-region into a governance-locked us-west-2 vault. Governance (not compliance) mode first so the plan can be validated before committing to irreversible immutability. The backup service role is backup-only (no restore policies) to stay least-privilege; restores get a separate audited path later. Resources are selected by explicit ARN to avoid drifting the stacks that own them. Deploys via the shared cdk deploy --all alongside the C-1 CloudTrail stack. See the README pre-deploy gates (S3 versioning, database-1 unencrypted copy smoke-test, DynamoDB PITR) before the first run. * Grant AWS Backup service use of vault CMKs The L2 BackupVault does not grant the backup service principal use of a customer-managed key; the synthesized key policy only delegated to account IAM. Cross-region copy of encrypted RDS/EBS recovery points uses KMS grants on the destination key, so without an explicit grant those copy jobs fail — and silently, since the account has no CloudTrail yet. Add backup.amazonaws.com crypto + CreateGrant statements to both vault keys, scoped by aws:SourceAccount (cross-review BLOCK 2; mirrors the discipline used on the C-1 CloudTrail key). Same class of bug the C-1 cross-review caught on the CloudTrail CMK.
2026-05-29 18:06:17 -04:00
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.)*
Add AWS Backup with offsite vault (audit C-7) (#3) * Add AWS Backup with offsite vault (audit C-7) The account had zero AWS Backup vaults/plans, so 22 of 23 data stores had no immutable, cross-region recovery path (audit finding C-7). One ransomware event or rogue delete would erase primary plus same-region snapshots/PITR. Phase 1 ("critical data first") protects the seven highest-risk stores with no offsite leg today (2 RDS, 2 DynamoDB, 3 S3) via a daily plan in a new us-east-1 vault, copied cross-region into a governance-locked us-west-2 vault. Governance (not compliance) mode first so the plan can be validated before committing to irreversible immutability. The backup service role is backup-only (no restore policies) to stay least-privilege; restores get a separate audited path later. Resources are selected by explicit ARN to avoid drifting the stacks that own them. Deploys via the shared cdk deploy --all alongside the C-1 CloudTrail stack. See the README pre-deploy gates (S3 versioning, database-1 unencrypted copy smoke-test, DynamoDB PITR) before the first run. * Grant AWS Backup service use of vault CMKs The L2 BackupVault does not grant the backup service principal use of a customer-managed key; the synthesized key policy only delegated to account IAM. Cross-region copy of encrypted RDS/EBS recovery points uses KMS grants on the destination key, so without an explicit grant those copy jobs fail — and silently, since the account has no CloudTrail yet. Add backup.amazonaws.com crypto + CreateGrant statements to both vault keys, scoped by aws:SourceAccount (cross-review BLOCK 2; mirrors the discipline used on the C-1 CloudTrail key). Same class of bug the C-1 cross-review caught on the CloudTrail CMK.
2026-05-29 18:06:17 -04:00
**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).
Account detective layer + budget (audit Day 1) (#5) * Add account detective layer + budget (audit Day 1: H-2/H-3/H-4/M-5/M-10) Adds to the seahaven-account-baseline stack: - AWS Config recorder (all + global resources) + delivery channel + role + hardened delivery bucket (H-2, CIS 3.3/3.5). Recorder role IAM cross-reviewed. - GuardDuty detector, us-east-1 (H-3) - Security Hub with AWS FSBP v1.0.0 + CIS v3.0.0 standards, depends on Config (H-4) - IAM Access Analyzer, account scope (M-5) - Monthly cost budget $1,200 with 80/100% actual + 100% forecast alerts to adam@seahavenind.com (M-10) Scope us-east-1 only (all workloads here); multi-region is a follow-up. The CLI-applied governance toggles (M-6/M-3/M-7/L-8/M-11) are documented separately in the README runbook. * Document Day 1 detective layer + CLI governance toggles in README * Move Config recorder+channel to CLI (L1 stabilization deadlock) The L1 AWS::Config::ConfigurationRecorder hangs the stack: it never reaches CREATE_COMPLETE until recording is active (needs a delivery channel), and the delivery channel cannot be created until the recorder completes — a deadlock that hung the deploy ~27 min before manual cancel (2026-06-01). Keep the cross-reviewed recorder role + delivery bucket in IaC; create the recorder, delivery channel, and start recording via CLI (documented in README). Security Hub no longer takes a CFN dependency on the recorder; CIS/FSBP controls evaluate once Config is recording. Verified live: recording=true, SUCCESS.
2026-06-01 17:56:12 -04:00
### 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. |
Account detective layer + budget (audit Day 1) (#5) * Add account detective layer + budget (audit Day 1: H-2/H-3/H-4/M-5/M-10) Adds to the seahaven-account-baseline stack: - AWS Config recorder (all + global resources) + delivery channel + role + hardened delivery bucket (H-2, CIS 3.3/3.5). Recorder role IAM cross-reviewed. - GuardDuty detector, us-east-1 (H-3) - Security Hub with AWS FSBP v1.0.0 + CIS v3.0.0 standards, depends on Config (H-4) - IAM Access Analyzer, account scope (M-5) - Monthly cost budget $1,200 with 80/100% actual + 100% forecast alerts to adam@seahavenind.com (M-10) Scope us-east-1 only (all workloads here); multi-region is a follow-up. The CLI-applied governance toggles (M-6/M-3/M-7/L-8/M-11) are documented separately in the README runbook. * Document Day 1 detective layer + CLI governance toggles in README * Move Config recorder+channel to CLI (L1 stabilization deadlock) The L1 AWS::Config::ConfigurationRecorder hangs the stack: it never reaches CREATE_COMPLETE until recording is active (needs a delivery channel), and the delivery channel cannot be created until the recorder completes — a deadlock that hung the deploy ~27 min before manual cancel (2026-06-01). Keep the cross-reviewed recorder role + delivery bucket in IaC; create the recorder, delivery channel, and start recording via CLI (documented in README). Security Hub no longer takes a CFN dependency on the recorder; CIS/FSBP controls evaluate once Config is recording. Verified live: recording=true, SUCCESS.
2026-06-01 17:56:12 -04:00
| 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.
Account detective layer + budget (audit Day 1) (#5) * Add account detective layer + budget (audit Day 1: H-2/H-3/H-4/M-5/M-10) Adds to the seahaven-account-baseline stack: - AWS Config recorder (all + global resources) + delivery channel + role + hardened delivery bucket (H-2, CIS 3.3/3.5). Recorder role IAM cross-reviewed. - GuardDuty detector, us-east-1 (H-3) - Security Hub with AWS FSBP v1.0.0 + CIS v3.0.0 standards, depends on Config (H-4) - IAM Access Analyzer, account scope (M-5) - Monthly cost budget $1,200 with 80/100% actual + 100% forecast alerts to adam@seahavenind.com (M-10) Scope us-east-1 only (all workloads here); multi-region is a follow-up. The CLI-applied governance toggles (M-6/M-3/M-7/L-8/M-11) are documented separately in the README runbook. * Document Day 1 detective layer + CLI governance toggles in README * Move Config recorder+channel to CLI (L1 stabilization deadlock) The L1 AWS::Config::ConfigurationRecorder hangs the stack: it never reaches CREATE_COMPLETE until recording is active (needs a delivery channel), and the delivery channel cannot be created until the recorder completes — a deadlock that hung the deploy ~27 min before manual cancel (2026-06-01). Keep the cross-reviewed recorder role + delivery bucket in IaC; create the recorder, delivery channel, and start recording via CLI (documented in README). Security Hub no longer takes a CFN dependency on the recorder; CIS/FSBP controls evaluate once Config is recording. Verified live: recording=true, SUCCESS.
2026-06-01 17:56:12 -04:00
### CLI-applied governance toggles (no CloudFormation resource)
These account toggles have no native CloudFormation resource, so they are applied
via CLI and recorded here. Applied 2026-06-01.
```bash
# 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'
```
**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.
fix: Fix noisy CIS 4.1 unauthorized-API alarm; drop redundant billing alarm (#36) * Fix noisy CIS 4.1 unauthorized-API alarm; drop redundant billing alarm CIS 4.1 (cis-UnauthorizedAPICalls) flapped OK<->ALARM 15 times in 30 days, all from benign AWS-service AccessDenied noise (CloudFormation deploy/drift describe-scans, AWS Config recorder). A single CFN run on 2026-07-07 emitted 100+ such denials in 15 min, tripping the alarm and burying the real CIS 4.1 security signal in email noise (alert fatigue). - Group both error codes so the exclusions apply to the whole filter (the old pattern leaked the UnauthorizedOperation branch past the exclusions due to && binding tighter than ||). - Exclude denials whose sourceIPAddress is an AWS service host (*.amazonaws.com) — AWS acting on our behalf, not a principal of concern. Real unauthorized calls from a console/CLI/attacker present a routable IP and are still counted. Validated against the trail log group: spike window 107 -> 4 matches, the 4 remaining all from a routable admin IP (genuine activity CIS should retain). - Keep the 3/3 evaluation as a backstop against one-off human fat-fingers. Billing: deleted the manually-created AWS-MonthlyBilling CloudWatch alarm ($50 threshold on EstimatedCharges, routed to site-alerts). It was unmanaged drift, permanently in ALARM, and fully redundant with the managed M-10 budget (seahaven-monthly-cost). README updated with rationale + restore command. * Address sh-security-review: scope CIS 4.1 exclusion to named benign sources The high-recall security review (detector fan-out + proof-or-kill verifier) confirmed a MEDIUM detection blind spot in the first revision: excluding all `*.amazonaws.com` source hosts would hide denials driven through ANY AWS service (SSM Automation, Step Functions, Lambda, etc.), which CloudTrail records with that service's host as sourceIPAddress — i.e. service-proxied privesc/recon attempts would evade CIS 4.1. Remediation: scope the exclusion to the specific benign sources that actually flap this account — `*cloudformation.amazonaws.com` (covers both cloudformation. and hooks.cloudformation.) and `config.amazonaws.com` — plus the pre-existing delivery.logs exclusion. Every other service-proxied denial is now retained. Residual (accepted, documented inline): CloudFormation/Config- proxied denials are still excluded — that path needs near-admin privilege (CreateStack + PassRole), successful changes still trip the other CIS 4.x alarms, and GuardDuty backstops. Validated on the live trail log group: spike window still 107 -> 4 matches (identical noise suppression), the 4 from a routable admin IP. tsc + synth clean.
2026-07-07 15:32:10 -04:00
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`.
Account detective layer + budget (audit Day 1) (#5) * Add account detective layer + budget (audit Day 1: H-2/H-3/H-4/M-5/M-10) Adds to the seahaven-account-baseline stack: - AWS Config recorder (all + global resources) + delivery channel + role + hardened delivery bucket (H-2, CIS 3.3/3.5). Recorder role IAM cross-reviewed. - GuardDuty detector, us-east-1 (H-3) - Security Hub with AWS FSBP v1.0.0 + CIS v3.0.0 standards, depends on Config (H-4) - IAM Access Analyzer, account scope (M-5) - Monthly cost budget $1,200 with 80/100% actual + 100% forecast alerts to adam@seahavenind.com (M-10) Scope us-east-1 only (all workloads here); multi-region is a follow-up. The CLI-applied governance toggles (M-6/M-3/M-7/L-8/M-11) are documented separately in the README runbook. * Document Day 1 detective layer + CLI governance toggles in README * Move Config recorder+channel to CLI (L1 stabilization deadlock) The L1 AWS::Config::ConfigurationRecorder hangs the stack: it never reaches CREATE_COMPLETE until recording is active (needs a delivery channel), and the delivery channel cannot be created until the recorder completes — a deadlock that hung the deploy ~27 min before manual cancel (2026-06-01). Keep the cross-reviewed recorder role + delivery bucket in IaC; create the recorder, delivery channel, and start recording via CLI (documented in README). Security Hub no longer takes a CFN dependency on the recorder; CIS/FSBP controls evaluate once Config is recording. Verified live: recording=true, SUCCESS.
2026-06-01 17:56:12 -04:00
### 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 |
2026-06-08 19:04:36 -04:00
| 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:
```bash
# 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:
```bash
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.
```bash
# 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)
Account detective layer + budget (audit Day 1) (#5) * Add account detective layer + budget (audit Day 1: H-2/H-3/H-4/M-5/M-10) Adds to the seahaven-account-baseline stack: - AWS Config recorder (all + global resources) + delivery channel + role + hardened delivery bucket (H-2, CIS 3.3/3.5). Recorder role IAM cross-reviewed. - GuardDuty detector, us-east-1 (H-3) - Security Hub with AWS FSBP v1.0.0 + CIS v3.0.0 standards, depends on Config (H-4) - IAM Access Analyzer, account scope (M-5) - Monthly cost budget $1,200 with 80/100% actual + 100% forecast alerts to adam@seahavenind.com (M-10) Scope us-east-1 only (all workloads here); multi-region is a follow-up. The CLI-applied governance toggles (M-6/M-3/M-7/L-8/M-11) are documented separately in the README runbook. * Document Day 1 detective layer + CLI governance toggles in README * Move Config recorder+channel to CLI (L1 stabilization deadlock) The L1 AWS::Config::ConfigurationRecorder hangs the stack: it never reaches CREATE_COMPLETE until recording is active (needs a delivery channel), and the delivery channel cannot be created until the recorder completes — a deadlock that hung the deploy ~27 min before manual cancel (2026-06-01). Keep the cross-reviewed recorder role + delivery bucket in IaC; create the recorder, delivery channel, and start recording via CLI (documented in README). Security Hub no longer takes a CFN dependency on the recorder; CIS/FSBP controls evaluate once Config is recording. Verified live: recording=true, SUCCESS.
2026-06-01 17:56:12 -04:00
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
```
Add AWS Backup with offsite vault (audit C-7) (#3) * Add AWS Backup with offsite vault (audit C-7) The account had zero AWS Backup vaults/plans, so 22 of 23 data stores had no immutable, cross-region recovery path (audit finding C-7). One ransomware event or rogue delete would erase primary plus same-region snapshots/PITR. Phase 1 ("critical data first") protects the seven highest-risk stores with no offsite leg today (2 RDS, 2 DynamoDB, 3 S3) via a daily plan in a new us-east-1 vault, copied cross-region into a governance-locked us-west-2 vault. Governance (not compliance) mode first so the plan can be validated before committing to irreversible immutability. The backup service role is backup-only (no restore policies) to stay least-privilege; restores get a separate audited path later. Resources are selected by explicit ARN to avoid drifting the stacks that own them. Deploys via the shared cdk deploy --all alongside the C-1 CloudTrail stack. See the README pre-deploy gates (S3 versioning, database-1 unencrypted copy smoke-test, DynamoDB PITR) before the first run. * Grant AWS Backup service use of vault CMKs The L2 BackupVault does not grant the backup service principal use of a customer-managed key; the synthesized key policy only delegated to account IAM. Cross-region copy of encrypted RDS/EBS recovery points uses KMS grants on the destination key, so without an explicit grant those copy jobs fail — and silently, since the account has no CloudTrail yet. Add backup.amazonaws.com crypto + CreateGrant statements to both vault keys, scoped by aws:SourceAccount (cross-review BLOCK 2; mirrors the discipline used on the C-1 CloudTrail key). Same class of bug the C-1 cross-review caught on the CloudTrail CMK.
2026-05-29 18:06:17 -04:00
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 \
Add AWS Backup with offsite vault (audit C-7) (#3) * Add AWS Backup with offsite vault (audit C-7) The account had zero AWS Backup vaults/plans, so 22 of 23 data stores had no immutable, cross-region recovery path (audit finding C-7). One ransomware event or rogue delete would erase primary plus same-region snapshots/PITR. Phase 1 ("critical data first") protects the seven highest-risk stores with no offsite leg today (2 RDS, 2 DynamoDB, 3 S3) via a daily plan in a new us-east-1 vault, copied cross-region into a governance-locked us-west-2 vault. Governance (not compliance) mode first so the plan can be validated before committing to irreversible immutability. The backup service role is backup-only (no restore policies) to stay least-privilege; restores get a separate audited path later. Resources are selected by explicit ARN to avoid drifting the stacks that own them. Deploys via the shared cdk deploy --all alongside the C-1 CloudTrail stack. See the README pre-deploy gates (S3 versioning, database-1 unencrypted copy smoke-test, DynamoDB PITR) before the first run. * Grant AWS Backup service use of vault CMKs The L2 BackupVault does not grant the backup service principal use of a customer-managed key; the synthesized key policy only delegated to account IAM. Cross-region copy of encrypted RDS/EBS recovery points uses KMS grants on the destination key, so without an explicit grant those copy jobs fail — and silently, since the account has no CloudTrail yet. Add backup.amazonaws.com crypto + CreateGrant statements to both vault keys, scoped by aws:SourceAccount (cross-review BLOCK 2; mirrors the discipline used on the C-1 CloudTrail key). Same class of bug the C-1 cross-review caught on the CloudTrail CMK.
2026-05-29 18:06:17 -04:00
--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
Add AWS Backup with offsite vault (audit C-7) (#3) * Add AWS Backup with offsite vault (audit C-7) The account had zero AWS Backup vaults/plans, so 22 of 23 data stores had no immutable, cross-region recovery path (audit finding C-7). One ransomware event or rogue delete would erase primary plus same-region snapshots/PITR. Phase 1 ("critical data first") protects the seven highest-risk stores with no offsite leg today (2 RDS, 2 DynamoDB, 3 S3) via a daily plan in a new us-east-1 vault, copied cross-region into a governance-locked us-west-2 vault. Governance (not compliance) mode first so the plan can be validated before committing to irreversible immutability. The backup service role is backup-only (no restore policies) to stay least-privilege; restores get a separate audited path later. Resources are selected by explicit ARN to avoid drifting the stacks that own them. Deploys via the shared cdk deploy --all alongside the C-1 CloudTrail stack. See the README pre-deploy gates (S3 versioning, database-1 unencrypted copy smoke-test, DynamoDB PITR) before the first run. * Grant AWS Backup service use of vault CMKs The L2 BackupVault does not grant the backup service principal use of a customer-managed key; the synthesized key policy only delegated to account IAM. Cross-region copy of encrypted RDS/EBS recovery points uses KMS grants on the destination key, so without an explicit grant those copy jobs fail — and silently, since the account has no CloudTrail yet. Add backup.amazonaws.com crypto + CreateGrant statements to both vault keys, scoped by aws:SourceAccount (cross-review BLOCK 2; mirrors the discipline used on the C-1 CloudTrail key). Same class of bug the C-1 cross-review caught on the CloudTrail CMK.
2026-05-29 18:06:17 -04:00
```
Account detective layer + budget (audit Day 1) (#5) * Add account detective layer + budget (audit Day 1: H-2/H-3/H-4/M-5/M-10) Adds to the seahaven-account-baseline stack: - AWS Config recorder (all + global resources) + delivery channel + role + hardened delivery bucket (H-2, CIS 3.3/3.5). Recorder role IAM cross-reviewed. - GuardDuty detector, us-east-1 (H-3) - Security Hub with AWS FSBP v1.0.0 + CIS v3.0.0 standards, depends on Config (H-4) - IAM Access Analyzer, account scope (M-5) - Monthly cost budget $1,200 with 80/100% actual + 100% forecast alerts to adam@seahavenind.com (M-10) Scope us-east-1 only (all workloads here); multi-region is a follow-up. The CLI-applied governance toggles (M-6/M-3/M-7/L-8/M-11) are documented separately in the README runbook. * Document Day 1 detective layer + CLI governance toggles in README * Move Config recorder+channel to CLI (L1 stabilization deadlock) The L1 AWS::Config::ConfigurationRecorder hangs the stack: it never reaches CREATE_COMPLETE until recording is active (needs a delivery channel), and the delivery channel cannot be created until the recorder completes — a deadlock that hung the deploy ~27 min before manual cancel (2026-06-01). Keep the cross-reviewed recorder role + delivery bucket in IaC; create the recorder, delivery channel, and start recording via CLI (documented in README). Security Hub no longer takes a CFN dependency on the recorder; CIS/FSBP controls evaluate once Config is recording. Verified live: recording=true, SUCCESS.
2026-06-01 17:56:12 -04:00
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
```