mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-10-07 16:08:55 +00:00
Compare commits
2 commits
2d3ba94140
...
2303a54ebc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2303a54ebc | ||
|
|
0a7c1bc450 |
5 changed files with 157 additions and 22 deletions
18
.github/workflows/deploy.yaml
vendored
18
.github/workflows/deploy.yaml
vendored
|
|
@ -41,3 +41,21 @@ 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 }}
|
||||
|
||||
deploy-prod:
|
||||
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@3cde673b9d05c0e68aac4d997d582f2543853d20 # main
|
||||
with:
|
||||
node-version: "24"
|
||||
stacks: "prod-baseline"
|
||||
stack-name: "seahaven-prod-baseline"
|
||||
secrets:
|
||||
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_PROD }}
|
||||
|
|
|
|||
44
README.md
44
README.md
|
|
@ -28,8 +28,11 @@ Stacks (deployed by the CD workflow — one job per target account):
|
|||
| `seahaven-regional-baseline-us-east-2` | 328440206208 | us-east-2 | Bedrock invocation logging + AWS Config recorder + Security Hub (INFRA-91 / INFRA-16) |
|
||||
| `seahaven-backup` | 328440206208 | us-east-1 | Primary AWS Backup vault + plan + role (C-7) |
|
||||
| `seahaven-backup-offsite` | 328440206208 | us-west-2 | Governance-locked offsite copy vault (C-7) |
|
||||
| `seahaven-org-governance` | 328440206208 | us-east-1 | AWS Organizations OU tree + SCPs (incl. the cdk-imported external-dev guardrails) |
|
||||
| `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) |
|
||||
| `seahaven-prod-baseline` | 011934824531 | us-east-1 | Member-account baseline for production workloads (org-managed detection; mgmt account frozen for new workloads) |
|
||||
|
||||
## CDK app
|
||||
|
||||
|
|
@ -42,12 +45,12 @@ the TypeScript source — no separate compile step needed for `cdk synth` /
|
|||
| Path | Role |
|
||||
|---|---|
|
||||
| `cdk.json` | CDK config: `app` synth command, `watch` includes/excludes, `context` feature flags |
|
||||
| `bin/app.ts` | App entry point — instantiates every stack with an explicit kebab-case `stackName` and its target `env` (account `328440206208`, per-region) |
|
||||
| `bin/app.ts` | App entry point — instantiates every stack with an explicit kebab-case `stackName` and its target `env` (five accounts, per-account/per-region) |
|
||||
| `lib/*-stack.ts` | Stack definitions (one class per stack; larger stacks compose the constructs in `lib/*.ts`) |
|
||||
| `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 eleven stacks across three regions and five accounts:
|
||||
|
||||
| Construct id | Stack name | Account | Region | Source |
|
||||
|---|---|---|---|---|
|
||||
|
|
@ -57,17 +60,32 @@ the TypeScript source — no separate compile step needed for `cdk synth` /
|
|||
| `regional-baseline-us-east-2` | `seahaven-regional-baseline-us-east-2` | 328440206208 | us-east-2 | `lib/regional-baseline-stack.ts` |
|
||||
| `backup-offsite` | `seahaven-backup-offsite` | 328440206208 | us-west-2 | `lib/backup-offsite-stack.ts` |
|
||||
| `backup` | `seahaven-backup` | 328440206208 | us-east-1 | `lib/backup-stack.ts` |
|
||||
| `org-governance` | `seahaven-org-governance` | 328440206208 | us-east-1 | `lib/org-governance-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) |
|
||||
| `prod-baseline` | `seahaven-prod-baseline` | 011934824531 | 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` |
|
||||
| 011934824531 (prod) | `githubdeploy-seahaven-org-baseline` | `AWS_DEPLOY_ROLE_ARN_PROD` |
|
||||
|
||||
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
|
||||
|
|
@ -223,7 +241,11 @@ recording is never interrupted.
|
|||
### CLI-applied governance toggles (no CloudFormation resource)
|
||||
|
||||
These account toggles have no native CloudFormation resource, so they are applied
|
||||
via CLI and recorded here. Applied 2026-06-01.
|
||||
via CLI and recorded here — **per account** (they are account-scoped; a new
|
||||
member account has NONE of them until applied). Applied: 328440206208
|
||||
(2026-06-01); 001520130573, 710827005802, 011934824531 (2026-07-14 — EBS
|
||||
encryption-by-default + password policy, verified per account; Inspector2 via
|
||||
delegated admin; cost-allocation tags are org-level).
|
||||
|
||||
```bash
|
||||
# M-3 EBS encryption-by-default (new volumes; existing 5 plaintext volumes are H-19-adjacent)
|
||||
|
|
|
|||
55
bin/app.ts
55
bin/app.ts
|
|
@ -12,6 +12,8 @@ import { OrgGovernanceStack } from "../lib/org-governance-stack";
|
|||
const ACCOUNT = "328440206208";
|
||||
const EXTERNAL_DEV_ACCOUNT = "396287094661";
|
||||
const SECURITY_ACCOUNT = "001520130573";
|
||||
const DEV_ACCOUNT = "710827005802";
|
||||
const PROD_ACCOUNT = "011934824531";
|
||||
|
||||
// All 5 VPCs in 328440206208 / us-east-1 (4 custom + default), audit Agent 7.
|
||||
// Index-derived logical IDs — append only, never reorder.
|
||||
|
|
@ -100,6 +102,59 @@ 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 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,
|
||||
});
|
||||
|
||||
// ── 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
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,13 @@ 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): the GuardDuty
|
||||
* detector and Security Hub hub are org-managed (auto-enrolled), so the
|
||||
* stack must not create local duplicates. Standards and the account analyzer
|
||||
* remain CFN-owned either way. Default FALSE (pre-delegation accounts).
|
||||
*/
|
||||
readonly orgManagedDetection?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -37,9 +44,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 +56,7 @@ export class MemberBaselineStack extends cdk.Stack {
|
|||
|
||||
new DetectiveControls(this, "DetectiveControls", {
|
||||
namePrefix: props.namePrefix,
|
||||
localDetectiveServices: !(props.orgManagedDetection ?? false),
|
||||
});
|
||||
|
||||
new FlowLogs(this, "FlowLogs", {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue