Commit graph

20 commits

Author SHA1 Message Date
Adam Moussa
db9465deda
fix(iam): allow shoc-backend HCP apply to write deploy SSM and matching githubdeploy trust (PLAT-148) (#148)
* fix(iam): allow shoc-backend HCP apply to write deploy SSM and matching githubdeploy trust (PLAT-148)

* fix(iam): grant shoc-backend staging plan named inventory reads (PLAT-148)

* fix(iam): allow staging githubdeploy to GetObject release zips (PLAT-148)

* fix(iam): allow staging githubdeploy to write EB processed extensions (PLAT-148)

* fix(iam): allow staging githubdeploy GetObjectAcl on release zips (PLAT-148)

* fix(iam): grant staging githubdeploy named S3 reads on EB resources prefix (PLAT-148)

* fix(iam): allow staging githubdeploy to delete EB version cache objects (PLAT-148)

* fix(iam): scope staging githubdeploy S3 object access to the EB bucket (PLAT-148)

* fix(iam): allow staging githubdeploy PutObjectVersionAcl on EB artifacts (PLAT-148)

* fix(iam): allow staging githubdeploy GetBucketPolicy on the EB bucket (PLAT-148)

* fix(iam): scope staging githubdeploy S3 objects to SHOC and staging EB prefixes (PLAT-148)
2026-09-18 18:13:01 +00:00
Adam Moussa
b02f52b805
feat(iam): lock app-owned HCP IAM and add bootstrap SCP (PLAT-143) (#137)
* feat(iam): lock app-owned HCP IAM and add bootstrap SCP (PLAT-143)

* fix(iam): pin HCP boundary ARNs and bootstrap trust window (PLAT-143)

Null on iam:PermissionsBoundary accepted any ceiling, including AdministratorAccess. Import apply cannot self-mutate hcptf-* while bootstrap trust is iam-bootstrap only; add a time-boxed exact StringEquals workspace grant instead of StringLike.
2026-09-02 15:22:48 +00:00
Adam Moussa
6a0713f49d
feat(iam): add frontend Terraform substrate (#133)
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
* feat(iam): add frontend Terraform substrate

* fix(iam): align frontend Terraform substrate

* feat(iam): enable frontend live Terraform roles
2026-08-31 02:25:47 +00:00
Adam Moussa
dba0871587
feat(iam): add external-dev backend Terraform substrate (#131)
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
* feat(iam): add external-dev backend terraform substrate

* fix(iam): require boundaries for SHOC policy writes
2026-08-29 21:04:40 +00:00
Adam Moussa
a6f22880db
feat(waf): add seahaven-prod shared CloudFront WebACL (PLAT-92) (#96)
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
* feat(waf): add seahaven-prod shared CloudFront WebACL stack

Stand up AppWebAcl in a thin prod stack and widen seahaven-site HCP
roles to read the SSM ARN so CloudFront can associate the ACL in-account.

* fix(deploy): add app-web-acl-prod to deploy.yaml
2026-08-07 17:07:04 -04:00
ea27635ef2
feat(iac): add per-account HCP Terraform deploy substrate for prod and dev
New stack seahaven-terraform-substrate (instances terraform-substrate-prod +
terraform-substrate-dev): app.terraform.io OIDC provider and the shared
boundary-gated guardrail policy seahaven-hcptf-iam-management that
per-workspace Terraform apply roles attach at migration time. No roles are
pre-provisioned (accumulator pattern, parallel to githubdeploy-*).

Guardrail statements mirror seahaven-cfn-exec-iam-management byte-identically
except DenySelfMutation, whose scope extends to hcptf-* alongside the
GitHub-substrate principals. Explicit stack dependency on the same-account
deploy-substrate stack (boundary ARN appears only in Condition strings, so
CFN infers no edge).
2026-07-30 16:31:34 -04:00
d6bea33436
feat(deploy-substrate): per-account GitHub Actions deploy substrate for prod/dev
SAM repos migrating off the frozen management account need the shared
deploy plumbing (permissions boundary + github-cfn-execution-role) in
their target account; none of it existed outside mgmt, so there was no
OIDC SAM deploy path into seahaven-prod or seahaven-dev at all.

Adds a templated, per-account substrate stack so onboarding a future
account is one bin/app.ts instance plus one CD job, not a hand-rolled
copy. Per-repo githubdeploy-* roles stay out by design: they are
provisioned per repo at migration time so an account never accumulates
trust for repos that do not deploy to it.

The template is a verbatim extraction of the reviewed mgmt substrate,
with deliberate, documented divergences — notably the removal of
iam:DeleteRolePermissionsBoundary plus explicit Deny backstops, which
closes a confirmed privilege-escalation path (see PR body).
2026-07-27 16:24:09 -04:00
Adam Moussa
4d3c846b88
feat(prod): seahaven-prod DynamoDB CMK + site-alerts alarm topic (procurement-ingest migration Phase 0a) (#57)
* feat(prod): add seahaven-prod DynamoDB CMK and site-alerts alarm-topic stacks

Provisions the two shared dependencies procurement-ingest imports by name,
ahead of its migration from mgmt to seahaven-prod:

- dynamodb-cmk-prod: second DynamoDbCmkStack instance (same stack name,
  prod account) creating alias/seahaven-dynamodb + the
  /seahaven/dynamodb/cmk-arn SSM param. Adds a cross-account key-policy
  statement so the mgmt seahaven-slack-bot roles can keep reading the
  CMK-encrypted purchase-orders table after it moves (ViaService +
  PrincipalArn-wildcard scoped; identity-policy half lands in the
  slack-bot repo's cutover PR).
- alarm-topic-prod: codified site-alerts SNS topic + seahaven-alarm-topics
  CMK with the cloudwatch.amazonaws.com publish grant (mirrors the working
  mgmt pattern; mgmt's topic remains CLI-managed debt).
- deploy.yaml: both appended to the deploy-prod job's explicit stack list
  (SH-ORG-005 rule: unlisted stacks silently never deploy).

* fix(scripts): account-id assertion in cfn-stack-decommission; complete the aws-cdk-lib 2.262.0 bump (patched brace-expansion); document CMK cutover trap

- cfn-stack-decommission.sh: --account-id is now REQUIRED and asserted
  against sts get-caller-identity before anything runs. Stack names are no
  longer org-unique (seahaven-dynamodb-cmk now exists in mgmt AND prod), so
  a name-only lookup under the wrong ambient profile could report or delete
  the wrong account's stack (security-review LOGIC-001).
- package.json/lock: PR #56's bump-for-patched-brace-expansion landed the
  commit title but not the pin; package.json still said 2.261.0 and the
  lockfile still resolved brace-expansion 5.0.6 (GHSA-3jxr-9vmj-r5cp HIGH,
  blocking the pre-commit scanner). Pin 2.262.0 and regenerate; npm audit
  now clean.
- bin/app.ts comments: slack-bot cutover MUST grant the PROD key ARN, never
  the account-local mgmt SSM param (LOGIC-005); failed-first-create orphan
  CMK recovery note (LOGIC-004).

* refactor(prod): drop cross-account CMK grant (slack-bot decommissioned 2026-07-23)

The AllowMgmtSlackBotReadViaDynamoDb key-policy statement targeted the
seahaven-slack-bot roles, which were decommissioned 2026-07-23. Its successor
sh-mcp is undeployed and uses same-account DynamoDB access, so no cross-account
reader of the CMK-encrypted purchase-orders table exists. The prod CMK + SSM
param + alarm-topic stacks remain (procurement-ingest still imports them). Add
a scoped cross-account grant if/when a real cross-account consumer deploys.
2026-07-23 15:29:55 -04:00
Adam Moussa
cc54b1e28b
chore(security): add explicit workflow permissions and bump aws-cdk-lib to 2.262.0 (#56)
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
* docs: update aws profile specified in script (local renaming)

* ci: add least-privilege permissions blocks to workflow callers

Resolves code scanning alerts #3 and #4 (actions/missing-workflow-permissions). Both callable workflows only need contents: read; the dependency-review callable already declares it internally, this caps the caller token to match."

* chore(deps): bump aws-cdk-lib to 2.262.0 for patched brace-expansion

Resolves Dependabot alert #4 (CVE-2026-13149, exponential-time DoS in brace-expansion expand()). The vulnerable 5.0.6 is a bundled dependency inside the aws-cdk-lib tarball, so it cannot be updated independently; 2.262.0 bundles the patched 5.0.7.

Also migrates Stack#addDependency to addStackDependency (deprecated in this release) in bin/app.ts.
2026-07-23 17:38:17 +00:00
Adam Moussa
2303a54ebc
seahaven-prod account baseline (Phase 5) (#50)
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
* Add seahaven-prod member baseline (Phase 5)

Account 011934824531 is the target for all new production stacks; the
management account is frozen for new workloads. First proven exercise
of the automatic enrollment sweep (Enabled in 124s, no manual
create-members) and of AutoEnableStandards=NONE (no pre-enabled
standards, so CFN owns FSBP + CIS v3.0 cleanly). Default VPC deleted;
budget starts at $100 and resizes as tenants land.

* Apply Phase-5 review findings

Fleet gap closed: EBS encryption-by-default + IAM password policy were
management-account-only (the runbook's unscoped 'applied' claim hid
it); now applied and verified in all three member accounts, runbook
scoped per account. README stack inventory corrected (eleven stacks,
org-governance rows restored). Sweep comments reconciled: the
automatic enrollment sweep is proven (seahaven-prod, ~2min).
2026-07-14 17:17:55 -04:00
Adam Moussa
0a7c1bc450
seahaven-dev account baseline with org-managed detection (Phase 4) (#49)
* Add seahaven-dev member baseline with org-managed detection

Account 710827005802 (internal dev/staging) is the first account born
after delegation: GuardDuty/Security Hub enroll it via the org admin,
so DetectiveControls gains a localDetectiveServices flag (default true
— zero diff on the three deployed consumers, verified) and the dev
instance sets orgManagedDetection to skip the colliding local
detector/hub/analyzer. Default VPC kept and flow-logged (dev runs real
workloads). Enrollment verified Enabled in both services before this
commit.

* Fix Phase-4 review findings: standards + analyzer stay CFN-owned

SH-DEV-001: org AutoEnableStandards DEFAULT gave dev legacy CIS v1.2.0
and nothing owned CIS v3.0 — org config set to NONE, standards are now
unconditional in DetectiveControls (attach fine to an org-enabled hub),
legacy ruleset disabled in dev. SH-DEVBASE-002: the ORGANIZATION
analyzer treats the whole org as trusted so it cannot flag intra-org
exposure — account analyzer restored unconditionally (coexistence
verified live). Enrollment comments corrected: manual create-members,
the automatic sweep is still unexercised. Zero diff re-verified on all
three deployed baseline stacks.
2026-07-14 16:41:36 -04:00
Adam Moussa
18f0f40e74
seahaven-security account baseline + security-OU guardrails (Phase 3) (#47)
* Add seahaven-security member baseline (Phase 3)

Account 001520130573 is the org's delegated security administrator.
Same member-baseline construct set as external-dev; own CD job under
its own OIDC role. Created at org root pending manual root hardening
before the OU move (deny-root-user invariant).

* Document delegated security administration runbook

Delegation to seahaven-security has no CloudFormation types; the CLI
sequence is the record, same pattern as the other account toggles.

* Apply Phase-3 security-review findings

Delegation runbook marked PENDING with hard preconditions (baseline
deployed, root MFA verified, account inside the security OU) — it had
read as applied before execution, the org's known claimed-done-but-NOT
failure mode (SEC-BASE-A/B). New security-guardrails SCP on the
security OU: region lock, IAM user/key lockout, privileged-role
protection, delegated-admin membership protection (SEC-BASE-C,
cross-reviewed APPROVE). deploy-security gains stack-name pre-flight
(SEC-BASE-D). Default VPC in 001520130573 deleted; empty flow-log list
and aws@ alert routing documented as deliberate (SEC-BASE-F/H).
2026-07-14 15:32:50 -04:00
Adam Moussa
57fd67324e
Route AWS notifications to dedicated mailboxes (#46)
Some checks are pending
Deploy / deploy-management (push) Waiting to run
Deploy / deploy-external-dev (push) Waiting to run
Budget alerts and CIS alarm subscriptions now go to aws@seahaven.com
(management) and aws-external-dev@seahaven.com (external-dev) instead
of personal addresses (Adam, 2026-07-14; resolves security-review flag
SH-ORG-007). Owner tags are informational and stay decoupled.
2026-07-14 14:41:42 -04:00
Adam Moussa
22b04c4e75
Org governance: OU skeleton + generalized SCPs (Phase 2) (#44)
Some checks are pending
Deploy / deploy-management (push) Waiting to run
Deploy / deploy-external-dev (push) Waiting to run
* Add org-governance stack: OU skeleton + generalized SCPs

Phase 2 of the multi-account segregation plan: codifies the OU tree
(workloads/prod/nonprod, security, sandbox, graveyard) and three
org-wide SCPs (workloads-region-lock, protect-security-baseline,
deny-root-user) generalized from the proven external-dev guardrails.
All resources Retain — CFN must never detach a live guardrail. New SCPs
attach only to the new empty OUs; extending to external-dev is a
separate gated targetIds change after live verification.

* Record SCP cross-review dispositions in org-governance

Root hardening must precede the OU move (deny-root-user blocks root MFA
enrollment), delegated-admin flows ride service-linked roles that SCPs
never evaluate, and the cdk exec-role exemption is accepted risk
mirroring the external-dev guardrails.

* Add org-governance to the management deploy job

Explicit stack selectors require every new stack to join exactly one
CD job (SH-ORG-005 discipline documented in this file).
2026-07-14 14:02:30 -04:00
Adam Moussa
3ab3bc773a
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
Adam Moussa
ec620e4e79
[INFRA-95] Shared DynamoDB CMK for sensitive finance/PII tables (M-3) (#21) 2026-06-08 19:04:42 -04:00
Adam Moussa
e9a184cf96
[INFRA-91/89/16/88/73] Reconcile out-of-band baseline changes + add missing detective controls (#18)
* Codify primary vault lock + add backups (INFRA-89, INFRA-88)

INFRA-89: codify the GOVERNANCE Vault Lock applied out-of-band on the
seahaven-primary vault (MinRetention 1d, MaxRetention 2555d, no
changeableFor = admin-removable) so it lives in IaC. Values match the
live lock exactly, so the deploy is a no-op adoption.

Add a scoped vault access policy that denies manual recovery-point
deletion and lock/policy tampering to all principals except the AWS
Backup service role and the break-glass SSO AdministratorAccess role,
so automatic lifecycle expiry still works but humans cannot prune
recovery points by hand.

Cross-review (GPT-4.1) BLOCK: NotPrincipal does not support wildcard
ARN matching, so the SSO exemption is expressed as Effect DENY with
Principal * and a StringNotLike condition on aws:PrincipalArn, which
does support wildcards. This avoids an unrecoverable vault lockout.

INFRA-88: add 6 S3 buckets (kb-docs, payroll-emails [PII], amazon-po,
extracted-amazon-po, proposal-system uploads + generated) to the
phase2-offsite-everything selection. Versioning verified enabled on
all 6 against the live account (S3 backup requires versioning).

Refs: INFRA-89, INFRA-88

* Promote account trail to organization trail (INFRA-73)

INFRA-73: set isOrganizationTrail on seahaven-org-trail and pass orgId
(o-9kufuzz6b4) so the L2 Trail attaches the AWSLogs/<org-id>/* bucket
PutObject statement for member-account delivery. CloudTrail org
trusted-access is already enabled on the management account.

Broaden the KMS key policy with an org-scoped GenerateDataKey/DescribeKey
statement for member-account trail delivery, guarded by
aws:PrincipalOrgID. The existing single-account statements are
preserved so management-account delivery is unaffected.

Cross-review (GPT-4.1) BLOCK: the member KMS SourceArn and encryption
context must be wildcarded across accounts (org-trail shadow trails
present the member account id), not pinned to the management account,
or member delivery silently fails. Fixed before checkpoint.

CHECKPOINT: delicate org-trail KMS/bucket-policy change — code +
diff captured for review, NOT deployed.

Refs: INFRA-73

* Add secondary-region baseline stacks (INFRA-91, INFRA-16)

INFRA-91: codify the Bedrock model-invocation logging applied
out-of-band in us-west-2 and us-east-2 (per-region delivery role
seahaven-bedrock-invocation-logging-<region> + log group
/aws/bedrock/model-invocations 90d, CloudWatch-only). The account-level
logging config itself has no CFN resource type and is applied via CLI
(already live), same as us-east-1.

INFRA-16: add the still-missing us-east-2 detective controls — AWS
Config recorder role + delivery bucket (recorder/channel via CLI to
avoid the CFN stabilization deadlock seen in us-east-1) and Security
Hub with FSBP + CIS v3.0. GuardDuty + flow logs already live in
us-east-2 and are left for a follow-up adoption to keep this change
non-destructive.

The us-east-1 baseline stays region-pinned; these are separate
RegionalBaselineStack instances composed opt-in per region.

CHECKPOINT: new multi-region stacks. The live Bedrock role + log group
already exist (CLI-created), so a plain deploy would collide — these
need cdk import / changeset adoption, not cdk deploy. Code + diff
captured for review, NOT deployed.

Refs: INFRA-91, INFRA-16

* Drop vault access policy from this deploy; tracked in INFRA-94 (kept governance lock codify + selection)
2026-06-08 17:03:18 -04:00
Adam Moussa
38d4a5753a
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
Adam Moussa
64ef25dc5b
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
Adam Moussa
dc079edb93 Initial account-baseline stack with CloudTrail (audit C-1)
Multi-region CloudTrail with log-file validation, a rotating KMS CMK, an
Object-Lock'd S3 log bucket, and CloudWatch Logs delivery. First resident of
the account-level security baseline; AWS Backup / 3-2-1 (C-7) lands alongside.

IAM/KMS/S3 policies cross-reviewed; review caught a missing CloudTrail KMS
grant, now added (SourceArn + encryption-context scoped).
2026-05-29 17:44:55 -04:00