seahaven-org-baseline/bin/app.ts

404 lines
19 KiB
TypeScript
Raw Normal View History

#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { AccountBaselineStack } from "../lib/account-baseline-stack";
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
import { AlarmTopicStack } from "../lib/alarm-topic-stack";
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
import { BackupOffsiteStack } from "../lib/backup-offsite-stack";
import { BackupStack } from "../lib/backup-stack";
[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
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 { SeahavenHcptfStack } from "../lib/seahaven-hcptf-stack";
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
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";
import { ViewAccessStack } from "../lib/view-access-stack";
[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
const ACCOUNT = "328440206208";
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
const EXTERNAL_DEV_ACCOUNT = "396287094661";
const SECURITY_ACCOUNT = "001520130573";
const DEV_ACCOUNT = "710827005802";
const PROD_ACCOUNT = "011934824531";
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
// 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)
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
"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",
[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
env: { account: ACCOUNT, region: "us-east-1" },
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
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",
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
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 ViewAccessStack(app, "view-access", {
stackName: "seahaven-view-access",
env: { account: ACCOUNT, region: "us-east-1" }, // pragma: allowlist secret
managementAccountId: ACCOUNT,
securityAccountId: SECURITY_ACCOUNT,
externalDevAccountId: EXTERNAL_DEV_ACCOUNT,
devAccountId: DEV_ACCOUNT,
prodAccountId: PROD_ACCOUNT,
});
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
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",
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
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",
});
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
// ── 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). The management account
// no longer publishes /seahaven/waf/app-web-acl-arn. This stack publishes that
// parameter 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" },
});
// payments-dashboard HCP roles, scoped policies, and the Lambda boundary.
// Prod also includes the imported seahaven-site apply and plan roles
// (PLAT-225). A create fails. Import site with `-c hcptfSiteImport=true`.
feat(hcptf): add hcptf-mta-sts apply and plan roles to seahaven-hcptf (PLAT-243) (#178) * feat(hcptf): add hcptf-mta-sts apply and plan roles to seahaven-hcptf New MtaStsRoles construct nested in the prod seahaven-hcptf stack for the mta-sts-prod HCP workspace. Fresh roles, plain create, Retain on every resource. Trust is StringEquals on the exact workspace sub per run phase. Managed policies at /tf-managed/: - mta-sts-hcptf-iam: manage only githubdeploy-mta-sts and its boundary; CreateRole requires that boundary; DenySelfMutation on hcptf-*, githubdeploy-*, cdk, OrganizationAccountAccessRole, seahaven-* - mta-sts-hcptf-services: S3 on mta-sts-prod-*, CloudFront, ACM scoped to Project=mta-sts, SSM /mta-sts/deploy/* and the WAF ACL parameter, GitHub OIDC provider read - mta-sts-hcptf-plan: enumerated refresh reads beside ViewOnlyAccess Outputs MtaStsApplyRoleArn and MtaStsPlanRoleArn. README lists mta-sts with the other prod exec roles that live in this stack. * fix(hcptf): scope mta-sts CreatePolicy and plan policy reads to the boundary ARN CreateDeployBoundary now names the boundary ARN as its Resource instead of "*", keeping the BoundaryFor request-tag condition as a second gate. The plan sidecar's GetPolicy, GetPolicyVersion, ListPolicyVersions, and ListPolicyTags are merged into one RefreshDeployBoundary statement on the boundary ARN. The boundary is the only managed policy in Terraform state, and ViewOnlyAccess does not carry GetPolicy or GetPolicyVersion. * fix(hcptf): replace cloudfront:* in mta-sts services policy with tag-gated grants CloudFrontManage granted cloudfront:* on every CloudFront resource in the account. Split into: - CloudFrontRead: the Get and ListTagsForResource calls Terraform makes - CloudFrontCreateTagged: CreateDistribution and TagResource on the distribution ARN type, gated on request tag Project=mta-sts - CloudFrontManageTagged: Update, Delete, Tag, Untag, and CreateInvalidation gated on resource tag Project=mta-sts - CloudFrontOac: Create, Update, Delete on the origin-access-control ARN type; OACs do not support tags A distribution another workspace owns cannot be mutated by this role. The workspace provider must set Project=mta-sts in default_tags. * fix(hcptf): close mta-sts TagResource bypass and trim ACM and plan reads CloudFrontCreateTagged keeps cloudfront:TagResource, which CreateDistributionWithTags requires before the distribution has tags, but adds Null aws:ResourceTag/Project so it applies only to a distribution with no Project tag yet. An existing distribution owned by another workspace can no longer be re-tagged into CloudFrontManageTagged's scope. ACM is trimmed to what aws_acm_certificate calls: RequestCertificate, DescribeCertificate, ListTagsForCertificate, AddTagsToCertificate, RemoveTagsFromCertificate, DeleteCertificate. GetCertificate, RenewCertificate, and ListCertificates are dropped from both roles. RefreshDeployRole reads only githubdeploy-mta-sts; the two CFN-owned exec roles are not in Terraform state. * fix(hcptf): give CloudFront create actions Resource "*" in mta-sts services policy cloudfront:CreateDistribution and cloudfront:CreateOriginAccessControl have no resource type in the service authorization reference and only match Resource "*". Scoping them to the distribution and OAC ARN types would have implicitly denied the first apply. TagResource at create time stays on the distribution ARN type with the RequestTag and Null ResourceTag conditions, and OAC update and delete stay on the OAC ARN type. Verified with iam simulate-custom-policy: CreateDistribution with request tag Project=mta-sts allowed; TagResource, UpdateDistribution, and DeleteDistribution on a distribution tagged Project=seahaven-site denied.
2026-10-05 18:42:33 +00:00
// Prod also creates the mta-sts apply and plan roles (PLAT-243).
// Trust is pinned per workspace.
const hcptfPaymentsImport = contextBoolean("hcptfPaymentsImport");
const hcptfSiteImport = contextBoolean("hcptfSiteImport");
const paymentsSecrets = (
account: string,
suffixes: readonly string[],
): string[] =>
suffixes.map(
(suffix) =>
`arn:aws:secretsmanager:us-east-1:${account}:secret:payments-dashboard/${suffix}`,
);
new SeahavenHcptfStack(app, "seahaven-hcptf", {
stackName: "seahaven-hcptf",
env: { account: PROD_ACCOUNT, region: "us-east-1" },
hcpProject: "seahaven-prod",
hcpWorkspace: "payments-dashboard-prod",
dynamodbCmkArn:
"arn:aws:kms:us-east-1:011934824531:key/be5fa4cb-c546-40fe-a13d-c7bec79f5d12",
secretArns: paymentsSecrets(PROD_ACCOUNT, [
"slack-bot-token-0pAM3S",
"slack-signing-secret-u0T6h8",
"boa-check-mgmt-LEbC65",
"boa-reporting-JoR9lq",
"expense-slack-token-SeMg3s",
"expense-slack-signing-secret-lbb78J",
]),
importExisting: hcptfPaymentsImport,
includeSeahavenSite: true,
siteImportExisting: hcptfSiteImport,
feat(hcptf): add hcptf-mta-sts apply and plan roles to seahaven-hcptf (PLAT-243) (#178) * feat(hcptf): add hcptf-mta-sts apply and plan roles to seahaven-hcptf New MtaStsRoles construct nested in the prod seahaven-hcptf stack for the mta-sts-prod HCP workspace. Fresh roles, plain create, Retain on every resource. Trust is StringEquals on the exact workspace sub per run phase. Managed policies at /tf-managed/: - mta-sts-hcptf-iam: manage only githubdeploy-mta-sts and its boundary; CreateRole requires that boundary; DenySelfMutation on hcptf-*, githubdeploy-*, cdk, OrganizationAccountAccessRole, seahaven-* - mta-sts-hcptf-services: S3 on mta-sts-prod-*, CloudFront, ACM scoped to Project=mta-sts, SSM /mta-sts/deploy/* and the WAF ACL parameter, GitHub OIDC provider read - mta-sts-hcptf-plan: enumerated refresh reads beside ViewOnlyAccess Outputs MtaStsApplyRoleArn and MtaStsPlanRoleArn. README lists mta-sts with the other prod exec roles that live in this stack. * fix(hcptf): scope mta-sts CreatePolicy and plan policy reads to the boundary ARN CreateDeployBoundary now names the boundary ARN as its Resource instead of "*", keeping the BoundaryFor request-tag condition as a second gate. The plan sidecar's GetPolicy, GetPolicyVersion, ListPolicyVersions, and ListPolicyTags are merged into one RefreshDeployBoundary statement on the boundary ARN. The boundary is the only managed policy in Terraform state, and ViewOnlyAccess does not carry GetPolicy or GetPolicyVersion. * fix(hcptf): replace cloudfront:* in mta-sts services policy with tag-gated grants CloudFrontManage granted cloudfront:* on every CloudFront resource in the account. Split into: - CloudFrontRead: the Get and ListTagsForResource calls Terraform makes - CloudFrontCreateTagged: CreateDistribution and TagResource on the distribution ARN type, gated on request tag Project=mta-sts - CloudFrontManageTagged: Update, Delete, Tag, Untag, and CreateInvalidation gated on resource tag Project=mta-sts - CloudFrontOac: Create, Update, Delete on the origin-access-control ARN type; OACs do not support tags A distribution another workspace owns cannot be mutated by this role. The workspace provider must set Project=mta-sts in default_tags. * fix(hcptf): close mta-sts TagResource bypass and trim ACM and plan reads CloudFrontCreateTagged keeps cloudfront:TagResource, which CreateDistributionWithTags requires before the distribution has tags, but adds Null aws:ResourceTag/Project so it applies only to a distribution with no Project tag yet. An existing distribution owned by another workspace can no longer be re-tagged into CloudFrontManageTagged's scope. ACM is trimmed to what aws_acm_certificate calls: RequestCertificate, DescribeCertificate, ListTagsForCertificate, AddTagsToCertificate, RemoveTagsFromCertificate, DeleteCertificate. GetCertificate, RenewCertificate, and ListCertificates are dropped from both roles. RefreshDeployRole reads only githubdeploy-mta-sts; the two CFN-owned exec roles are not in Terraform state. * fix(hcptf): give CloudFront create actions Resource "*" in mta-sts services policy cloudfront:CreateDistribution and cloudfront:CreateOriginAccessControl have no resource type in the service authorization reference and only match Resource "*". Scoping them to the distribution and OAC ARN types would have implicitly denied the first apply. TagResource at create time stays on the distribution ARN type with the RequestTag and Null ResourceTag conditions, and OAC update and delete stay on the OAC ARN type. Verified with iam simulate-custom-policy: CreateDistribution with request tag Project=mta-sts allowed; TagResource, UpdateDistribution, and DeleteDistribution on a distribution tagged Project=seahaven-site denied.
2026-10-05 18:42:33 +00:00
includeMtaSts: true,
});
new SeahavenHcptfStack(app, "seahaven-hcptf-dev", {
stackName: "seahaven-hcptf",
env: { account: DEV_ACCOUNT, region: "us-east-1" },
hcpProject: "seahaven-dev",
hcpWorkspace: "payments-dashboard-dev",
dynamodbCmkArn:
"arn:aws:kms:us-east-1:710827005802:key/600997e6-418e-4b7f-9d63-cd42a7505a95",
secretArns: paymentsSecrets(DEV_ACCOUNT, [
"slack-bot-token-KhPaLp",
"slack-signing-secret-CDxsUK",
"boa-check-mgmt-kkEnCw",
"boa-reporting-uMHHVo",
"expense-slack-token-05OZg3",
"expense-slack-signing-secret-DQAoXS",
]),
importExisting: hcptfPaymentsImport,
});
// 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" },
});
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
// ── 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" },
});
[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
// ── 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,
});
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 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);