From 0a7c1bc4502ed69c3192bdc577336aa6381b88d1 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Tue, 14 Jul 2026 16:41:36 -0400 Subject: [PATCH] seahaven-dev account baseline with org-managed detection (Phase 4) (#49) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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. --- .github/workflows/deploy.yaml | 9 +++++++ README.md | 31 +++++++++++++++------- bin/app.ts | 28 ++++++++++++++++++++ lib/detective-controls.ts | 48 ++++++++++++++++++++++++++++------- lib/member-baseline-stack.ts | 13 ++++++++-- 5 files changed, 109 insertions(+), 20 deletions(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index cdb57a9..e70cfd5 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -41,3 +41,12 @@ jobs: stack-name: "seahaven-security-baseline" secrets: deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_SECURITY }} + + deploy-dev: + uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@3cde673b9d05c0e68aac4d997d582f2543853d20 # main + with: + node-version: "24" + stacks: "dev-baseline" + stack-name: "seahaven-dev-baseline" + secrets: + deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_DEV }} diff --git a/README.md b/README.md index 78ce524..8c406c4 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Stacks (deployed by the CD workflow — one job per target account): | `seahaven-backup-offsite` | 328440206208 | us-west-2 | Governance-locked offsite copy vault (C-7) | | `seahaven-external-dev-baseline` | 396287094661 | us-east-1 | Member-account baseline: Config, GuardDuty, Security Hub (FSBP + CIS v3.0), Access Analyzer, flow logs, budget | | `seahaven-security-baseline` | 001520130573 | us-east-1 | Member-account baseline for the delegated security-admin account (same construct set) | +| `seahaven-dev-baseline` | 710827005802 | us-east-1 | Member-account baseline for internal dev/staging (org-managed detection — no local GuardDuty/SecurityHub) | ## CDK app @@ -47,7 +48,7 @@ the TypeScript source — no separate compile step needed for `cdk synth` / | `tsconfig.json` | TypeScript compiler options (`outDir: cdk.out`) | | `package.json` | Pinned `aws-cdk-lib`, CDK CLI, and the `build` / `synth` / `diff` / `deploy` npm scripts | -`bin/app.ts` synthesizes seven stacks across three regions and two accounts: +`bin/app.ts` synthesizes nine stacks across three regions and four accounts: | Construct id | Stack name | Account | Region | Source | |---|---|---|---|---| @@ -59,15 +60,27 @@ the TypeScript source — no separate compile step needed for `cdk synth` / | `backup` | `seahaven-backup` | 328440206208 | us-east-1 | `lib/backup-stack.ts` | | `external-dev-baseline` | `seahaven-external-dev-baseline` | 396287094661 | us-east-1 | `lib/member-baseline-stack.ts` | | `security-baseline` | `seahaven-security-baseline` | 001520130573 | us-east-1 | `lib/member-baseline-stack.ts` | +| `dev-baseline` | `seahaven-dev-baseline` | 710827005802 | us-east-1 | `lib/member-baseline-stack.ts` (orgManagedDetection) | -The member-account stack (`external-dev-baseline`) deploys with credentials for -**396287094661** — the CD workflow runs it as a separate job assuming that -account's OIDC deploy role (`githubdeploy-seahaven-external-dev-baseline`, -repo secret `AWS_DEPLOY_ROLE_ARN_EXTDEV`). Local deploys/diffs of that stack -assume `OrganizationAccountAccessRole` in 396287094661. Its shared constructs -(`DetectiveControls`, `FlowLogs`, `GovernanceToggles`) are prefix-parameterized -(`seahaven` vs `seahaven-extdev`) — construct ids and physical names must stay -byte-identical to the deployed stack (logical IDs are path-derived). +Member-account stacks deploy with per-account credentials — the CD workflow +runs one job per account, each assuming that account's OIDC deploy role. Local +deploys/diffs assume `OrganizationAccountAccessRole` in the target account. + +| Account | OIDC deploy role | Repo secret | +|---|---|---| +| 396287094661 (external-dev) | `githubdeploy-seahaven-external-dev-baseline` | `AWS_DEPLOY_ROLE_ARN_EXTDEV` | +| 001520130573 (security) | `githubdeploy-seahaven-org-baseline` | `AWS_DEPLOY_ROLE_ARN_SECURITY` | +| 710827005802 (dev) | `githubdeploy-seahaven-org-baseline` | `AWS_DEPLOY_ROLE_ARN_DEV` | + +Shared constructs (`DetectiveControls`, `FlowLogs`, `GovernanceToggles`) are +prefix-parameterized — construct ids and physical names must stay +byte-identical to the deployed stacks (logical IDs are path-derived). +Accounts enrolled by the org delegated admin (post 2026-07-14) set +`orgManagedDetection: true`: the GuardDuty detector + Security Hub hub come +from the org, while STANDARDS (FSBP + CIS v3.0) and the account analyzer stay +CFN-owned (org `AutoEnableStandards` is `NONE` — the DEFAULT setting enrolls +legacy CIS v1.2.0). Enrollment (member `Enabled` in GuardDuty + Security Hub) +is a hard precondition for such a stack's first deploy. `backup` declares an explicit dependency on `backup-offsite` so the offsite copy vault exists before the primary plan that copies into it. Stack names are set diff --git a/bin/app.ts b/bin/app.ts index 813ee3f..014f969 100644 --- a/bin/app.ts +++ b/bin/app.ts @@ -12,6 +12,7 @@ import { OrgGovernanceStack } from "../lib/org-governance-stack"; const ACCOUNT = "328440206208"; const EXTERNAL_DEV_ACCOUNT = "396287094661"; const SECURITY_ACCOUNT = "001520130573"; +const DEV_ACCOUNT = "710827005802"; // All 5 VPCs in 328440206208 / us-east-1 (4 custom + default), audit Agent 7. // Index-derived logical IDs — append only, never reorder. @@ -100,6 +101,33 @@ new MemberBaselineStack(app, "security-baseline", { 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 new-account sweep +// remains unexercised; do not rely on it without verifying). 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, +}); + // ── 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 diff --git a/lib/detective-controls.ts b/lib/detective-controls.ts index a3fa3bd..be6aa2e 100644 --- a/lib/detective-controls.ts +++ b/lib/detective-controls.ts @@ -31,6 +31,21 @@ export interface DetectiveControlsProps { * custom resources; keep it stable per account. */ readonly namePrefix: string; + /** + * Create the account-local GuardDuty detector + Security Hub hub. Default + * TRUE (pre-delegation accounts own these). Set FALSE for accounts enrolled + * by the org delegated admin (post 2026-07-14): the org creates the + * detector/hub itself and a CFN-owned duplicate fails (one per account). + * ALWAYS created regardless of this flag (security review SH-DEV-001 / + * SH-DEVBASE-002): Security Hub STANDARDS (org AutoEnableStandards is NONE — + * DEFAULT would enroll legacy CIS v1.2.0, so IaC owns FSBP + CIS v3.0; + * CfnStandard attaches fine to an org-enabled hub), the account Access + * Analyzer (the ORGANIZATION analyzer treats the whole org as trusted and + * cannot flag intra-org exposure — e.g. to the isolated contractor account; + * both analyzer types coexist, verified live), and the Config recorder + * (recorders stay per-account, delegation never makes one). + */ + readonly localDetectiveServices?: boolean; } export class DetectiveControls extends Construct { @@ -297,12 +312,17 @@ export class DetectiveControls extends Construct { }); // ────────────────────────────────────────────────────────────────────── - // H-3 GuardDuty + // H-3 GuardDuty detector + H-4 Security Hub hub — skipped when the org + // delegated admin owns them (localDetectiveServices: false). Standards and + // the analyzer below are created UNCONDITIONALLY (see props doc). // ────────────────────────────────────────────────────────────────────── - new guardduty.CfnDetector(this, "GuardDutyDetector", { - enable: true, - findingPublishingFrequency: "FIFTEEN_MINUTES", - }); + const localDetection = props.localDetectiveServices ?? true; + let hub: securityhub.CfnHub | undefined; + if (localDetection) { + new guardduty.CfnDetector(this, "GuardDutyDetector", { + enable: true, + findingPublishingFrequency: "FIFTEEN_MINUTES", + }); // ────────────────────────────────────────────────────────────────────── // H-4 Security Hub (FSBP + CIS v3.0) @@ -310,11 +330,12 @@ export class DetectiveControls extends Construct { // CIS/FSBP controls evaluate against the Config recording managed by the // custom resource above; no CFN dependency needed (findings populate once // recording is active). - const hub = new securityhub.CfnHub(this, "SecurityHub", { + hub = new securityhub.CfnHub(this, "SecurityHub", { enableDefaultStandards: false, controlFindingGenerator: "SECURITY_CONTROL", autoEnableControls: true, }); + } const fsbpArn = cdk.Arn.format( { @@ -337,18 +358,27 @@ export class DetectiveControls extends Construct { stack ); + // When the hub is org-managed (localDetection false) it already exists + // before this stack deploys (enrollment is a deploy precondition), so the + // standards attach without a CFN dependency. const fsbp = new securityhub.CfnStandard(this, "StandardFSBP", { standardsArn: fsbpArn, }); - fsbp.node.addDependency(hub); + if (hub) { + fsbp.node.addDependency(hub); + } const cis = new securityhub.CfnStandard(this, "StandardCIS", { standardsArn: cisArn, }); - cis.node.addDependency(hub); + if (hub) { + cis.node.addDependency(hub); + } // ────────────────────────────────────────────────────────────────────── - // M-5 IAM Access Analyzer (free, account-scoped external-access) + // M-5 IAM Access Analyzer (free, account-scoped external-access) — + // always created: the ORGANIZATION analyzer cannot flag intra-org + // exposure (SH-DEVBASE-002). // ────────────────────────────────────────────────────────────────────── new accessanalyzer.CfnAnalyzer(this, "AccountAnalyzer", { analyzerName: `${prefix}-account-analyzer`, diff --git a/lib/member-baseline-stack.ts b/lib/member-baseline-stack.ts index f6677a8..0ef7608 100644 --- a/lib/member-baseline-stack.ts +++ b/lib/member-baseline-stack.ts @@ -21,6 +21,12 @@ export interface MemberBaselineStackProps extends cdk.StackProps { readonly flowLogVpcIds: string[]; /** Value for the ManagedBy tag on every resource in the stack. */ readonly managedByTag: string; + /** + * TRUE for accounts created after org delegation (2026-07-14): GuardDuty / + * Security Hub / analyzer are org-managed (auto-enrolled), so the stack must + * not create local duplicates. Default FALSE (pre-delegation accounts). + */ + readonly orgManagedDetection?: boolean; } /** @@ -37,9 +43,11 @@ export interface MemberBaselineStackProps extends cdk.StackProps { * - No WAF, SES monitoring, Bedrock logging, DynamoDB CMK, or AWS Backup — * all prod-only concerns. * - * Contains: AWS Config, GuardDuty, Security Hub (FSBP + CIS v3.0), IAM Access + * Contains: AWS Config, Security Hub standards (FSBP + CIS v3.0), IAM Access * Analyzer, Inspector2 (post-deploy CLI, see README), VPC flow logs, and a - * monthly cost Budget. + * monthly cost Budget — plus the GuardDuty detector + Security Hub hub only + * when the account predates org delegation (orgManagedDetection false); newer + * accounts get those from the delegated admin. */ export class MemberBaselineStack extends cdk.Stack { constructor(scope: Construct, id: string, props: MemberBaselineStackProps) { @@ -47,6 +55,7 @@ export class MemberBaselineStack extends cdk.Stack { new DetectiveControls(this, "DetectiveControls", { namePrefix: props.namePrefix, + localDetectiveServices: !(props.orgManagedDetection ?? false), }); new FlowLogs(this, "FlowLogs", {