seahaven-dev account baseline with org-managed detection (Phase 4) (#49)

* Add seahaven-dev member baseline with org-managed detection

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

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

SH-DEV-001: org AutoEnableStandards DEFAULT gave dev legacy CIS v1.2.0
and nothing owned CIS v3.0 — org config set to NONE, standards are now
unconditional in DetectiveControls (attach fine to an org-enabled hub),
legacy ruleset disabled in dev. SH-DEVBASE-002: the ORGANIZATION
analyzer treats the whole org as trusted so it cannot flag intra-org
exposure — account analyzer restored unconditionally (coexistence
verified live). Enrollment comments corrected: manual create-members,
the automatic sweep is still unexercised. Zero diff re-verified on all
three deployed baseline stacks.
This commit is contained in:
Adam Moussa 2026-07-14 16:41:36 -04:00 • committed by GitHub
parent 2d3ba94140
commit 0a7c1bc450
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 109 additions and 20 deletions

View file

@ -41,3 +41,12 @@ jobs:
stack-name: "seahaven-security-baseline" stack-name: "seahaven-security-baseline"
secrets: secrets:
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_SECURITY }} 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 }}

View file

@ -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-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-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-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 ## 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`) | | `tsconfig.json` | TypeScript compiler options (`outDir: cdk.out`) |
| `package.json` | Pinned `aws-cdk-lib`, CDK CLI, and the `build` / `synth` / `diff` / `deploy` npm scripts | | `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 | | 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` | | `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` | | `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` | | `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 Member-account stacks deploy with per-account credentials — the CD workflow
**396287094661** — the CD workflow runs it as a separate job assuming that runs one job per account, each assuming that account's OIDC deploy role. Local
account's OIDC deploy role (`githubdeploy-seahaven-external-dev-baseline`, deploys/diffs assume `OrganizationAccountAccessRole` in the target account.
repo secret `AWS_DEPLOY_ROLE_ARN_EXTDEV`). Local deploys/diffs of that stack
assume `OrganizationAccountAccessRole` in 396287094661. Its shared constructs | Account | OIDC deploy role | Repo secret |
(`DetectiveControls`, `FlowLogs`, `GovernanceToggles`) are prefix-parameterized |---|---|---|
(`seahaven` vs `seahaven-extdev`) — construct ids and physical names must stay | 396287094661 (external-dev) | `githubdeploy-seahaven-external-dev-baseline` | `AWS_DEPLOY_ROLE_ARN_EXTDEV` |
byte-identical to the deployed stack (logical IDs are path-derived). | 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 `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 vault exists before the primary plan that copies into it. Stack names are set

View file

@ -12,6 +12,7 @@ import { OrgGovernanceStack } from "../lib/org-governance-stack";
const ACCOUNT = "328440206208"; const ACCOUNT = "328440206208";
const EXTERNAL_DEV_ACCOUNT = "396287094661"; const EXTERNAL_DEV_ACCOUNT = "396287094661";
const SECURITY_ACCOUNT = "001520130573"; const SECURITY_ACCOUNT = "001520130573";
const DEV_ACCOUNT = "710827005802";
// All 5 VPCs in 328440206208 / us-east-1 (4 custom + default), audit Agent 7. // All 5 VPCs in 328440206208 / us-east-1 (4 custom + default), audit Agent 7.
// Index-derived logical IDs — append only, never reorder. // Index-derived logical IDs — append only, never reorder.
@ -100,6 +101,33 @@ new MemberBaselineStack(app, "security-baseline", {
managedByTag: "seahaven-org-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) ───────────────────────────────────── // ── Shared DynamoDB CMK (INFRA-95 / M-3) ─────────────────────────────────────
// Dedicated, standalone stack so the customer-managed key for sensitive // Dedicated, standalone stack so the customer-managed key for sensitive
// finance/PII DynamoDB tables is an independent shared dependency for the owning // finance/PII DynamoDB tables is an independent shared dependency for the owning

View file

@ -31,6 +31,21 @@ export interface DetectiveControlsProps {
* custom resources; keep it stable per account. * custom resources; keep it stable per account.
*/ */
readonly namePrefix: string; 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 { 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", { const localDetection = props.localDetectiveServices ?? true;
enable: true, let hub: securityhub.CfnHub | undefined;
findingPublishingFrequency: "FIFTEEN_MINUTES", if (localDetection) {
}); new guardduty.CfnDetector(this, "GuardDutyDetector", {
enable: true,
findingPublishingFrequency: "FIFTEEN_MINUTES",
});
// ────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────
// H-4 Security Hub (FSBP + CIS v3.0) // 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 // CIS/FSBP controls evaluate against the Config recording managed by the
// custom resource above; no CFN dependency needed (findings populate once // custom resource above; no CFN dependency needed (findings populate once
// recording is active). // recording is active).
const hub = new securityhub.CfnHub(this, "SecurityHub", { hub = new securityhub.CfnHub(this, "SecurityHub", {
enableDefaultStandards: false, enableDefaultStandards: false,
controlFindingGenerator: "SECURITY_CONTROL", controlFindingGenerator: "SECURITY_CONTROL",
autoEnableControls: true, autoEnableControls: true,
}); });
}
const fsbpArn = cdk.Arn.format( const fsbpArn = cdk.Arn.format(
{ {
@ -337,18 +358,27 @@ export class DetectiveControls extends Construct {
stack 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", { const fsbp = new securityhub.CfnStandard(this, "StandardFSBP", {
standardsArn: fsbpArn, standardsArn: fsbpArn,
}); });
fsbp.node.addDependency(hub); if (hub) {
fsbp.node.addDependency(hub);
}
const cis = new securityhub.CfnStandard(this, "StandardCIS", { const cis = new securityhub.CfnStandard(this, "StandardCIS", {
standardsArn: cisArn, 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", { new accessanalyzer.CfnAnalyzer(this, "AccountAnalyzer", {
analyzerName: `${prefix}-account-analyzer`, analyzerName: `${prefix}-account-analyzer`,

View file

@ -21,6 +21,12 @@ export interface MemberBaselineStackProps extends cdk.StackProps {
readonly flowLogVpcIds: string[]; readonly flowLogVpcIds: string[];
/** Value for the ManagedBy tag on every resource in the stack. */ /** Value for the ManagedBy tag on every resource in the stack. */
readonly managedByTag: string; 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 — * - No WAF, SES monitoring, Bedrock logging, DynamoDB CMK, or AWS Backup —
* all prod-only concerns. * 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 * 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 { export class MemberBaselineStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: MemberBaselineStackProps) { constructor(scope: Construct, id: string, props: MemberBaselineStackProps) {
@ -47,6 +55,7 @@ export class MemberBaselineStack extends cdk.Stack {
new DetectiveControls(this, "DetectiveControls", { new DetectiveControls(this, "DetectiveControls", {
namePrefix: props.namePrefix, namePrefix: props.namePrefix,
localDetectiveServices: !(props.orgManagedDetection ?? false),
}); });
new FlowLogs(this, "FlowLogs", { new FlowLogs(this, "FlowLogs", {