mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-10-03 06:53:13 +00:00
Nightly backup jobs fail on resources that have left the management account. The CloudFront WebACL is already gone while CloudFormation still owns it, so the deletion policy has to be Retain before a later change can remove it.
345 lines
17 KiB
JavaScript
345 lines
17 KiB
JavaScript
#!/usr/bin/env node
|
|
import "source-map-support/register";
|
|
import * as cdk from "aws-cdk-lib";
|
|
import { AccountBaselineStack } from "../lib/account-baseline-stack";
|
|
import { AlarmTopicStack } from "../lib/alarm-topic-stack";
|
|
import { BackupOffsiteStack } from "../lib/backup-offsite-stack";
|
|
import { BackupStack } from "../lib/backup-stack";
|
|
import { RegionalBaselineStack } from "../lib/regional-baseline-stack";
|
|
import { DeploySubstrateStack } from "../lib/deploy-substrate-stack";
|
|
import { TerraformSubstrateStack } from "../lib/terraform-substrate-stack";
|
|
import { DynamoDbCmkStack } from "../lib/dynamodb-cmk-stack";
|
|
import { AppWebAclStack } from "../lib/app-web-acl-stack";
|
|
import { SeahavenSiteHcptfStack } from "../lib/seahaven-site-hcptf-stack";
|
|
import { MemberBaselineStack } from "../lib/member-baseline-stack";
|
|
import { OrgGovernanceStack } from "../lib/org-governance-stack";
|
|
import { PlatformAccessStack } from "../lib/platform-access-stack";
|
|
import { EngineeringAccessStack } from "../lib/engineering-access-stack";
|
|
|
|
const ACCOUNT = "328440206208";
|
|
const EXTERNAL_DEV_ACCOUNT = "396287094661";
|
|
const SECURITY_ACCOUNT = "001520130573";
|
|
const DEV_ACCOUNT = "710827005802";
|
|
const PROD_ACCOUNT = "011934824531";
|
|
|
|
// Index-derived logical IDs — append only, never reorder, never close a hole.
|
|
// Slots 0-2 are retired VPCs (FlowLog0, FlowLog1, FlowLog2). Clearing them
|
|
// deletes those flow logs. Slots 3 and 4 stay: seahaven-vpc and the default VPC.
|
|
const PROD_VPC_IDS: readonly (string | undefined)[] = [
|
|
undefined, // vpc-061d66990b6a4d1fb
|
|
undefined, // vpc-0542a9e934b417d23
|
|
undefined, // vpc-062d200c68bd4ca0e (flow log was still ACTIVE; VPC is gone)
|
|
"vpc-0d3d4b67bd0cf8a68",
|
|
"vpc-02c10a89d66f6f9b8",
|
|
];
|
|
|
|
const app = new cdk.App();
|
|
|
|
const contextBoolean = (key: string): boolean => {
|
|
const value = app.node.tryGetContext(key);
|
|
if (value === true || value === "true") return true;
|
|
if (value === false || value === "false" || value === undefined) return false;
|
|
throw new Error(`${key} must be true or false`);
|
|
};
|
|
|
|
const contextString = (key: string): string => {
|
|
const value = app.node.tryGetContext(key);
|
|
if (value === undefined) return "";
|
|
if (typeof value === "string") return value;
|
|
throw new Error(`${key} must be a string`);
|
|
};
|
|
|
|
new AccountBaselineStack(app, "account-baseline", {
|
|
stackName: "seahaven-account-baseline",
|
|
env: { account: ACCOUNT, region: "us-east-1" },
|
|
monthlyBudgetUsd: 1200,
|
|
// Dedicated AWS-notifications mailbox (Adam, 2026-07-14). Also feeds the CIS
|
|
// alarm SNS subscription — a changed endpoint must CONFIRM via the email
|
|
// link before alarm notifications flow again.
|
|
budgetAlertEmail: "aws@seahaven.com",
|
|
flowLogVpcIds: PROD_VPC_IDS,
|
|
});
|
|
|
|
// ── Member-account baseline: seahaven-external-dev ───────────────────────────
|
|
// Absorbed from the retired seahaven-external-dev-baseline repo. Stack name and
|
|
// every construct id preserved byte-identically (logical IDs are path-derived —
|
|
// renaming anything here replaces live resources). Deploys to the isolated
|
|
// external-dev member account via its own OIDC deploy role, NOT the mgmt role.
|
|
//
|
|
// Flow-log VPC ids are COMMITTED here, not passed via -c context. The old
|
|
// repo's `-c flowLogVpcIds=...` pattern was a confirmed security-review trap
|
|
// (SH-ORG-004): once flow logs were attached via context, any context-less
|
|
// deploy (including CI) would silently REMOVE them all. Append ids via PR;
|
|
// never reorder (index-derived logical IDs). Empty = hardened bucket only,
|
|
// matching the currently deployed stack.
|
|
const EXTDEV_FLOW_LOG_VPC_IDS: string[] = [];
|
|
|
|
// ── Org structure: OUs + generalized SCPs (management account only) ─────────
|
|
// Existing external-dev OU + its 3 imported SCPs are adopted into this stack via
|
|
// `cdk import` post-deploy — see lib/org-governance-stack.ts header + README.
|
|
new OrgGovernanceStack(app, "org-governance", {
|
|
stackName: "seahaven-org-governance",
|
|
env: { account: ACCOUNT, region: "us-east-1" },
|
|
});
|
|
|
|
new PlatformAccessStack(app, "platform-access", {
|
|
stackName: "seahaven-platform-access",
|
|
// Same management-account region as org-governance above.
|
|
env: { account: ACCOUNT, region: "us-east-1" }, // pragma: allowlist secret
|
|
});
|
|
|
|
new EngineeringAccessStack(app, "engineering-access", {
|
|
stackName: "seahaven-engineering-access",
|
|
env: { account: ACCOUNT, region: "us-east-1" }, // pragma: allowlist secret
|
|
devAccountId: DEV_ACCOUNT,
|
|
prodAccountId: PROD_ACCOUNT,
|
|
});
|
|
|
|
new MemberBaselineStack(app, "external-dev-baseline", {
|
|
stackName: "seahaven-external-dev-baseline",
|
|
env: { account: EXTERNAL_DEV_ACCOUNT, region: "us-east-1" },
|
|
namePrefix: "seahaven-extdev",
|
|
monthlyBudgetUsd: 200,
|
|
// Account-dedicated AWS-notifications mailbox (Adam, 2026-07-14 — resolves
|
|
// security-review flag SH-ORG-007).
|
|
budgetAlertEmail: "aws-external-dev@seahaven.com",
|
|
ownerEmail: "adam@seahaven.com",
|
|
flowLogVpcIds: EXTDEV_FLOW_LOG_VPC_IDS,
|
|
// Keeps the tag value the stack was deployed with (zero-diff merge). Update
|
|
// to the current repo name in a deliberate follow-up change if desired.
|
|
managedByTag: "seahaven-external-dev-baseline",
|
|
});
|
|
|
|
// ── Member-account baseline: seahaven-security (Phase 3) ────────────────────
|
|
// The org's delegated security administrator-to-be (GuardDuty / Security Hub /
|
|
// IAM Access Analyzer / Config aggregator / Inspector2 — delegation is CLI +
|
|
// README runbook with HARD preconditions, no CFN types). Created 2026-07-14 at
|
|
// org ROOT; moves into the security OU only after manual root hardening
|
|
// (deny-root-user invariant, see lib/org-governance-stack.ts). Delegation runs
|
|
// ONLY after the OU move (SEC-BASE-B).
|
|
// LIFECYCLE (SEC-BASE-E): once delegation is live, this stack's GuardDuty
|
|
// detector + Security Hub hub are co-managed by the org admin config — never
|
|
// rename/remove those constructs via CFN while the account is delegated admin.
|
|
new MemberBaselineStack(app, "security-baseline", {
|
|
stackName: "seahaven-security-baseline",
|
|
env: { account: SECURITY_ACCOUNT, region: "us-east-1" },
|
|
namePrefix: "seahaven-security",
|
|
monthlyBudgetUsd: 50,
|
|
// aws@ (not a per-account mailbox) is deliberate: Adam's 2026-07-14
|
|
// direction routes all AWS notifications to aws@seahaven.com; extdev's
|
|
// dedicated mailbox predates that direction.
|
|
budgetAlertEmail: "aws@seahaven.com",
|
|
ownerEmail: "adam@seahaven.com",
|
|
// Empty is deliberate: the account's default VPC is DELETED (a delegated
|
|
// security-admin account runs no workloads — SEC-BASE-F; also clears the
|
|
// default-VPC CIS/FSBP controls). Any future VPC id gets appended via PR.
|
|
flowLogVpcIds: [],
|
|
managedByTag: "seahaven-org-baseline",
|
|
});
|
|
|
|
// ── Member-account baseline: seahaven-dev (Phase 4) ─────────────────────────
|
|
// Internal dev/staging workloads (NOT the external-dev engagement account).
|
|
// Created 2026-07-14 AFTER org delegation went live: GuardDuty detector +
|
|
// Security Hub hub are org-managed — enrolled via delegated-admin
|
|
// create-members and verified Enabled (the AUTOMATIC sweep was later proven
|
|
// on seahaven-prod, ~2min; still verify enrollment before any org-managed
|
|
// stack's first deploy). Standards and
|
|
// the account analyzer stay CFN-owned (SH-DEV-001/SH-DEVBASE-002). Same
|
|
// lifecycle rule as the other new accounts: root-harden at org ROOT, then
|
|
// move-account into the nonprod OU (ou-nbuj-zpt5ka98) — NO WORKLOADS until
|
|
// the account is inside the OU (SH-DEV-002: until then no region lock, no
|
|
// baseline-tamper SCP, usable root).
|
|
new MemberBaselineStack(app, "dev-baseline", {
|
|
stackName: "seahaven-dev-baseline",
|
|
env: { account: DEV_ACCOUNT, region: "us-east-1" },
|
|
namePrefix: "seahaven-dev",
|
|
monthlyBudgetUsd: 150,
|
|
budgetAlertEmail: "aws@seahaven.com",
|
|
ownerEmail: "adam@seahaven.com",
|
|
// Default VPC kept (dev runs real workloads); flow-logged from this stack's
|
|
// first deploy. Index-derived logical IDs — append only, never reorder
|
|
// (replacing the default VPC later = append the new id, keep this entry
|
|
// until its flow log is deliberately retired).
|
|
flowLogVpcIds: ["vpc-08f07dc5edeea621f"],
|
|
managedByTag: "seahaven-org-baseline",
|
|
orgManagedDetection: true,
|
|
});
|
|
|
|
// ── Member-account baseline: seahaven-prod (Phase 5) ────────────────────────
|
|
// Target for ALL new production stacks (mgmt 328440206208 is frozen for new
|
|
// workloads). First tenant: proposal-system redeploy. Detection is org-managed
|
|
// (AUTO-enrolled by the sweep in 124s — first proven exercise, 2026-07-14 —
|
|
// and verified Enabled before this stack's first deploy); standards + account
|
|
// analyzer are CFN-owned per the Phase-4 review. Default VPC DELETED (prod
|
|
// workloads use purpose-built VPCs). Same lifecycle rule: root-harden at org
|
|
// ROOT, then move-account into the prod OU (ou-nbuj-5lc2wp6h) — NO WORKLOADS
|
|
// until the account is inside the OU. Budget starts at $100 and is resized as
|
|
// tenants land; AWS Backup vaults are added with the first stateful tenant
|
|
// (cross-account restore test = definition of done for that change).
|
|
new MemberBaselineStack(app, "prod-baseline", {
|
|
stackName: "seahaven-prod-baseline",
|
|
env: { account: PROD_ACCOUNT, region: "us-east-1" },
|
|
namePrefix: "seahaven-prod",
|
|
monthlyBudgetUsd: 100,
|
|
budgetAlertEmail: "aws@seahaven.com",
|
|
ownerEmail: "adam@seahaven.com",
|
|
// Append-only, never reorder (index-derived logical IDs). Empty: no VPCs
|
|
// exist yet; append ids via PR as purpose-built VPCs land.
|
|
flowLogVpcIds: [],
|
|
managedByTag: "seahaven-org-baseline",
|
|
orgManagedDetection: true,
|
|
});
|
|
|
|
// ── Per-account GitHub Actions deploy substrate ──────────────────────────────
|
|
// The shared account-level deploy plumbing for SAM pipelines: permissions
|
|
// boundary + github-cfn-execution-role (+ optional OIDC provider). mgmt's
|
|
// copy lives in Sea-Haven-Industries/.github/oidc-deploy-roles.yaml and stays
|
|
// there until its stacks finish migrating out; these stacks are what let SAM
|
|
// repos (payments-dashboard, front-integrations, sh-openswe-traces, ...)
|
|
// target prod/dev at all. Per-repo githubdeploy-* roles are provisioned at
|
|
// each repo's migration time, never here. createOidcProvider stays false for
|
|
// both accounts (provider verified present in each, 2026-07-27); a FUTURE
|
|
// member account without one sets it true on its own instance. First-create
|
|
// precondition verified 2026-07-27: github-cfn-execution-role and the
|
|
// seahaven-lambda-execution-boundary policy both returned NoSuchEntity in
|
|
// 011934824531 AND 710827005802, so the named creates cannot collide with
|
|
// out-of-band copies.
|
|
new DeploySubstrateStack(app, "deploy-substrate-prod", {
|
|
stackName: "seahaven-deploy-substrate",
|
|
env: { account: PROD_ACCOUNT, region: "us-east-1" },
|
|
createOidcProvider: false,
|
|
});
|
|
|
|
new DeploySubstrateStack(app, "deploy-substrate-dev", {
|
|
stackName: "seahaven-deploy-substrate",
|
|
env: { account: DEV_ACCOUNT, region: "us-east-1" },
|
|
createOidcProvider: false,
|
|
});
|
|
|
|
// Prod/dev seahaven-terraform-substrate is out of this app and out of CD
|
|
// (PLAT-147). The live stacks stay until scripts/delete-terraform-substrate-prod-dev.sh.
|
|
// Do not add them back. Do not add a CDK stack for hcptf-bootstrap (CLI-owned,
|
|
// PLAT-145). External-dev stays: SHOC IAM is not moving (PLAT-148).
|
|
// deploy-substrate stays for remaining SAM (PLAT-150).
|
|
|
|
// Shared CloudFront WAF for seahaven-prod (PLAT-92). Same AppWebAcl construct
|
|
// as mgmt account-baseline; thin stack so prod does not inherit the full
|
|
// mgmt baseline. Publishes /seahaven/waf/app-web-acl-arn for in-account
|
|
// CloudFront associations (same-account only).
|
|
new AppWebAclStack(app, "app-web-acl-prod", {
|
|
stackName: "seahaven-app-web-acl",
|
|
env: { account: PROD_ACCOUNT, region: "us-east-1" },
|
|
});
|
|
|
|
// seahaven-site exec roles (PLAT-225). Not part of terraform-substrate.
|
|
// Imported. On the prod deploy job. A create fails because the roles already exist.
|
|
new SeahavenSiteHcptfStack(app, "seahaven-site-hcptf", {
|
|
stackName: "seahaven-site-hcptf",
|
|
env: { account: PROD_ACCOUNT, region: "us-east-1" },
|
|
});
|
|
|
|
// External-dev already has app.terraform.io federation. Both role gates start
|
|
// false in cdk.json: POC is enabled by a normal update; dev/staging only by
|
|
// CloudFormation import after Terraform relinquishes those four live roles.
|
|
const terraformSubstrateExternalDev = new TerraformSubstrateStack(
|
|
app,
|
|
"terraform-substrate-external-dev",
|
|
{
|
|
stackName: "seahaven-terraform-substrate",
|
|
env: { account: EXTERNAL_DEV_ACCOUNT, region: "us-east-1" },
|
|
createOidcProvider: false,
|
|
enableShocBackendPocRoles: contextBoolean("enableShocBackendPocRoles"),
|
|
enableShocBackendLiveRoles: contextBoolean("enableShocBackendLiveRoles"),
|
|
enableShocFrontendPocRoles: contextBoolean("enableShocFrontendPocRoles"),
|
|
enableShocFrontendLiveRoles: contextBoolean("enableShocFrontendLiveRoles"),
|
|
shocFrontendPocDistributionId: contextString(
|
|
"shocFrontendPocDistributionId",
|
|
),
|
|
shocFrontendPocOriginAccessControlId: contextString(
|
|
"shocFrontendPocOriginAccessControlId",
|
|
),
|
|
shocFrontendPocFunctionName: contextString(
|
|
"shocFrontendPocFunctionName",
|
|
),
|
|
shocFrontendPocHostedZoneId: contextString(
|
|
"shocFrontendPocHostedZoneId",
|
|
),
|
|
shocFrontendPocCertificateArn: contextString(
|
|
"shocFrontendPocCertificateArn",
|
|
),
|
|
},
|
|
);
|
|
|
|
// ── Shared DynamoDB CMK (INFRA-95 / M-3) ─────────────────────────────────────
|
|
// Dedicated, standalone stack so the customer-managed key for sensitive
|
|
// finance/PII DynamoDB tables is an independent shared dependency for the owning
|
|
// app repos (payments-dashboard, procurement-ingest, exec-aide). Its ARN is
|
|
// published to SSM (/seahaven/dynamodb/cmk-arn) for those stacks to consume.
|
|
new DynamoDbCmkStack(app, "dynamodb-cmk", {
|
|
stackName: "seahaven-dynamodb-cmk",
|
|
env: { account: ACCOUNT, region: "us-east-1" },
|
|
});
|
|
|
|
// ── seahaven-prod copies for the procurement-ingest migration ────────────────
|
|
// procurement-ingest is moving from mgmt to seahaven-prod; its stacks resolve
|
|
// the DynamoDB CMK via SSM /seahaven/dynamodb/cmk-arn and import the SNS topic
|
|
// `site-alerts` by constructed in-account ARN, so both must exist in prod
|
|
// BEFORE that app's first prod deploy. Same stack names as mgmt (unique
|
|
// per-account); distinct CDK ids.
|
|
// No cross-account key-policy statement: the only cross-account reader of the
|
|
// CMK-encrypted purchase-orders table (seahaven-slack-bot) was decommissioned
|
|
// 2026-07-23, and its successor sh-mcp is undeployed and uses same-account
|
|
// DynamoDB access. When/if a cross-account consumer materializes, add a
|
|
// correctly-scoped grant then (target its real roles + account).
|
|
//
|
|
// Recovery note (failed FIRST create): the key is RETAIN, its alias/SSM param
|
|
// are not — a CREATE_FAILED rollback orphans an unaliased rotation-enabled
|
|
// key. Before re-running the deploy, list unaliased CMKs in prod and schedule
|
|
// deletion of the orphan.
|
|
new DynamoDbCmkStack(app, "dynamodb-cmk-prod", {
|
|
stackName: "seahaven-dynamodb-cmk",
|
|
env: { account: PROD_ACCOUNT, region: "us-east-1" },
|
|
});
|
|
|
|
new AlarmTopicStack(app, "alarm-topic-prod", {
|
|
stackName: "seahaven-alarm-topic",
|
|
env: { account: PROD_ACCOUNT, region: "us-east-1" },
|
|
});
|
|
|
|
// ── Secondary-region baselines (INFRA-16, INFRA-91) ──────────────────────────
|
|
// The us-east-1 baseline above is region-pinned by design. These stacks extend
|
|
// a minimal detective/logging footprint into the secondary regions, codifying
|
|
// state applied out-of-band this week so it lives in IaC.
|
|
|
|
// us-west-2: Bedrock invocation logging only (INFRA-91). Shares the region with
|
|
// the offsite backup vault but is an independent concern (separate stack).
|
|
new RegionalBaselineStack(app, "regional-baseline-us-west-2", {
|
|
stackName: "seahaven-regional-baseline-us-west-2",
|
|
env: { account: ACCOUNT, region: "us-west-2" },
|
|
bedrockLogging: true,
|
|
});
|
|
|
|
// us-east-2: Bedrock invocation logging (INFRA-91) + the still-missing AWS
|
|
// Config recorder and Security Hub (INFRA-16). GuardDuty + flow logs already
|
|
// live here (adopted as a follow-up, see lib/regional-baseline-stack.ts).
|
|
new RegionalBaselineStack(app, "regional-baseline-us-east-2", {
|
|
stackName: "seahaven-regional-baseline-us-east-2",
|
|
env: { account: ACCOUNT, region: "us-east-2" },
|
|
bedrockLogging: true,
|
|
configRecorder: true,
|
|
securityHub: true,
|
|
});
|
|
|
|
// AWS Backup (audit C-7). Offsite vault (us-west-2) must exist before the
|
|
// primary plan that copies to it, hence the explicit dependency.
|
|
const backupOffsite = new BackupOffsiteStack(app, "backup-offsite", {
|
|
stackName: "seahaven-backup-offsite",
|
|
env: { account: "328440206208", region: "us-west-2" },
|
|
});
|
|
|
|
const backupPrimary = new BackupStack(app, "backup", {
|
|
stackName: "seahaven-backup",
|
|
env: { account: "328440206208", region: "us-east-1" },
|
|
});
|
|
|
|
backupPrimary.addStackDependency(backupOffsite);
|