From 3ab3bc773a0c60a192d1eb063a167d02ceff2ec7 Mon Sep 17 00:00:00 2001 From: Adam Moussa <166072409+amoussa1229@users.noreply.github.com> Date: Tue, 14 Jul 2026 13:53:07 -0400 Subject: [PATCH] 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. --- .github/workflows/deploy.yaml | 18 ++++++++- README.md | 70 ++++++++++++++++++++++------------- bin/app.ts | 41 ++++++++++++++++++++ lib/account-baseline-stack.ts | 15 +++++++- lib/detective-controls.ts | 41 +++++++++++++------- lib/flow-logs.ts | 37 ++++++++++-------- lib/governance-toggles.ts | 7 +++- lib/member-baseline-stack.ts | 64 ++++++++++++++++++++++++++++++++ package.json | 2 +- 9 files changed, 234 insertions(+), 61 deletions(-) create mode 100644 lib/member-baseline-stack.ts diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 4d0aaa7..7d2717f 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -11,10 +11,24 @@ concurrency: group: deploy cancel-in-progress: false +# One job per target AWS account: cdk deploy with explicit stack selectors so +# each OIDC role only ever deploys its own account's stacks. A new stack added +# to bin/app.ts MUST be appended to exactly one job's `stacks` list — explicit +# selectors mean an unlisted stack is silently never deployed (security review +# SH-ORG-005). jobs: - deploy: - uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@fd60e4c9041784f666ac0fdefb9bec3c7fbf5143 # main + deploy-management: + uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@3cde673b9d05c0e68aac4d997d582f2543853d20 # main with: node-version: "24" + stacks: "account-baseline dynamodb-cmk regional-baseline-us-west-2 regional-baseline-us-east-2 backup-offsite backup" secrets: deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} + + deploy-external-dev: + uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@3cde673b9d05c0e68aac4d997d582f2543853d20 # main + with: + node-version: "24" + stacks: "external-dev-baseline" + secrets: + deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN_EXTDEV }} diff --git a/README.md b/README.md index b5c7e7f..21cd8ea 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,34 @@ -# seahaven-account-baseline +# seahaven-org-baseline ![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white) ![AWS CDK](https://img.shields.io/badge/AWS-CDK-FF9900?logo=amazonaws&logoColor=white) -![CI](https://github.com/Sea-Haven-Industries/seahaven-account-baseline/actions/workflows/ci.yaml/badge.svg) +![CI](https://github.com/Sea-Haven-Industries/seahaven-org-baseline/actions/workflows/ci.yaml/badge.svg) -Account-level security and governance baseline for Sea Haven Industries -(AWS account **328440206208**), managed as a single CDK TypeScript app. The -primary baseline is in **us-east-1**, with secondary-region baselines in -**us-east-2** and **us-west-2** and the offsite backup vault in **us-west-2**. -This is where account-wide detective and recovery controls live, so they are -versioned, reviewed, and drift-checked like any other stack. +Organization-wide security and governance baseline for Sea Haven Industries, +managed as a single CDK TypeScript app. Covers the management account +(**328440206208**: primary baseline in **us-east-1**, secondary-region +baselines in **us-east-2**/**us-west-2**, offsite backup vault in +**us-west-2**) and org **member accounts** (first tenant: +`seahaven-external-dev` **396287094661**, absorbed from the retired +`seahaven-external-dev-baseline` repo). This is where account-wide detective +and recovery controls live, so they are versioned, reviewed, and drift-checked +like any other stack. -Stacks (all deployed by `cdk deploy --all` / the CD workflow): +> **History:** this repo was `seahaven-account-baseline` (management account +> only) until 2026-07-14, when the external-dev member baseline was merged in +> and the repo renamed. Deployed CloudFormation stack names are unchanged. -| Stack | Region | Purpose | -|---|---|---| -| `seahaven-account-baseline` | us-east-1 | CloudTrail + future detective controls (C-1) | -| `seahaven-dynamodb-cmk` | us-east-1 | Shared customer-managed KMS key for finance/PII DynamoDB tables; ARN published to SSM `/seahaven/dynamodb/cmk-arn` (INFRA-95 / M-3) | -| `seahaven-regional-baseline-us-west-2` | us-west-2 | Bedrock invocation logging (INFRA-91) | -| `seahaven-regional-baseline-us-east-2` | us-east-2 | Bedrock invocation logging + AWS Config recorder + Security Hub (INFRA-91 / INFRA-16) | -| `seahaven-backup` | us-east-1 | Primary AWS Backup vault + plan + role (C-7) | -| `seahaven-backup-offsite` | us-west-2 | Governance-locked offsite copy vault (C-7) | +Stacks (deployed by the CD workflow — one job per target account): + +| Stack | Account | Region | Purpose | +|---|---|---|---| +| `seahaven-account-baseline` | 328440206208 | us-east-1 | CloudTrail + detective controls (C-1) | +| `seahaven-dynamodb-cmk` | 328440206208 | us-east-1 | Shared customer-managed KMS key for finance/PII DynamoDB tables; ARN published to SSM `/seahaven/dynamodb/cmk-arn` (INFRA-95 / M-3) | +| `seahaven-regional-baseline-us-west-2` | 328440206208 | us-west-2 | Bedrock invocation logging (INFRA-91) | +| `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-external-dev-baseline` | 396287094661 | us-east-1 | Member-account baseline: Config, GuardDuty, Security Hub (FSBP + CIS v3.0), Access Analyzer, flow logs, budget | ## CDK app @@ -38,16 +46,26 @@ 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 six stacks across three regions: +`bin/app.ts` synthesizes seven stacks across three regions and two accounts: -| Construct id | Stack name | Region | Source | -|---|---|---|---| -| `account-baseline` | `seahaven-account-baseline` | us-east-1 | `lib/account-baseline-stack.ts` | -| `dynamodb-cmk` | `seahaven-dynamodb-cmk` | us-east-1 | `lib/dynamodb-cmk-stack.ts` | -| `regional-baseline-us-west-2` | `seahaven-regional-baseline-us-west-2` | us-west-2 | `lib/regional-baseline-stack.ts` | -| `regional-baseline-us-east-2` | `seahaven-regional-baseline-us-east-2` | us-east-2 | `lib/regional-baseline-stack.ts` | -| `backup-offsite` | `seahaven-backup-offsite` | us-west-2 | `lib/backup-offsite-stack.ts` | -| `backup` | `seahaven-backup` | us-east-1 | `lib/backup-stack.ts` | +| Construct id | Stack name | Account | Region | Source | +|---|---|---|---|---| +| `account-baseline` | `seahaven-account-baseline` | 328440206208 | us-east-1 | `lib/account-baseline-stack.ts` | +| `dynamodb-cmk` | `seahaven-dynamodb-cmk` | 328440206208 | us-east-1 | `lib/dynamodb-cmk-stack.ts` | +| `regional-baseline-us-west-2` | `seahaven-regional-baseline-us-west-2` | 328440206208 | us-west-2 | `lib/regional-baseline-stack.ts` | +| `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` | +| `external-dev-baseline` | `seahaven-external-dev-baseline` | 396287094661 | us-east-1 | `lib/member-baseline-stack.ts` | + +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). `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 06564a7..1630a51 100644 --- a/bin/app.ts +++ b/bin/app.ts @@ -6,8 +6,20 @@ import { BackupOffsiteStack } from "../lib/backup-offsite-stack"; import { BackupStack } from "../lib/backup-stack"; import { RegionalBaselineStack } from "../lib/regional-baseline-stack"; import { DynamoDbCmkStack } from "../lib/dynamodb-cmk-stack"; +import { MemberBaselineStack } from "../lib/member-baseline-stack"; const ACCOUNT = "328440206208"; +const EXTERNAL_DEV_ACCOUNT = "396287094661"; + +// All 5 VPCs in 328440206208 / us-east-1 (4 custom + default), audit Agent 7. +// Index-derived logical IDs — append only, never reorder. +const PROD_VPC_IDS = [ + "vpc-061d66990b6a4d1fb", + "vpc-0542a9e934b417d23", + "vpc-062d200c68bd4ca0e", + "vpc-0d3d4b67bd0cf8a68", + "vpc-02c10a89d66f6f9b8", +]; const app = new cdk.App(); @@ -16,6 +28,35 @@ new AccountBaselineStack(app, "account-baseline", { env: { account: ACCOUNT, region: "us-east-1" }, monthlyBudgetUsd: 1200, budgetAlertEmail: "adam@seahavenind.com", + flowLogVpcIds: PROD_VPC_IDS, +}); + +// ── Member-account baseline: seahaven-external-dev ─────────────────────────── +// Absorbed from the retired seahaven-external-dev-baseline repo. Stack name and +// every construct id preserved byte-identically (logical IDs are path-derived — +// renaming anything here replaces live resources). Deploys to the isolated +// external-dev member account via its own OIDC deploy role, NOT the mgmt role. +// +// Flow-log VPC ids are COMMITTED here, not passed via -c context. The old +// repo's `-c flowLogVpcIds=...` pattern was a confirmed security-review trap +// (SH-ORG-004): once flow logs were attached via context, any context-less +// deploy (including CI) would silently REMOVE them all. Append ids via PR; +// never reorder (index-derived logical IDs). Empty = hardened bucket only, +// matching the currently deployed stack. +const EXTDEV_FLOW_LOG_VPC_IDS: string[] = []; + +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, + // NOTE: seahaven.com (not seahavenind.com) is deliberate-as-deployed; flagged + // in the 2026-07-14 security review (SH-ORG-007) for mailbox verification. + budgetAlertEmail: "adam@seahaven.com", + flowLogVpcIds: EXTDEV_FLOW_LOG_VPC_IDS, + // Keeps the tag value the stack was deployed with (zero-diff merge). Update + // to the current repo name in a deliberate follow-up change if desired. + managedByTag: "seahaven-external-dev-baseline", }); // ── Shared DynamoDB CMK (INFRA-95 / M-3) ───────────────────────────────────── diff --git a/lib/account-baseline-stack.ts b/lib/account-baseline-stack.ts index 606ed48..5126d06 100644 --- a/lib/account-baseline-stack.ts +++ b/lib/account-baseline-stack.ts @@ -31,6 +31,11 @@ export interface AccountBaselineStackProps extends cdk.StackProps { readonly monthlyBudgetUsd: number; /** Email for budget threshold alerts (M-10). */ readonly budgetAlertEmail: string; + /** + * VPC ids to attach ALL-traffic flow logs to (H-14). Logical IDs are + * index-derived — only append, never reorder (see lib/flow-logs.ts). + */ + readonly flowLogVpcIds: string[]; } export class AccountBaselineStack extends cdk.Stack { @@ -225,9 +230,12 @@ export class AccountBaselineStack extends cdk.Stack { // ── Day 1 detective layer + governance toggles ── // Config (H-2), GuardDuty (H-3), Security Hub (H-4), Access Analyzer (M-5). - new DetectiveControls(this, "DetectiveControls"); + new DetectiveControls(this, "DetectiveControls", { + namePrefix: "seahaven", + }); // Monthly cost budget (M-10). Other governance toggles are CLI + documented. new GovernanceToggles(this, "GovernanceToggles", { + budgetName: "seahaven-monthly-cost", monthlyLimitUsd: props.monthlyBudgetUsd, alertEmail: props.budgetAlertEmail, }); @@ -239,7 +247,10 @@ export class AccountBaselineStack extends cdk.Stack { alarmEmail: props.budgetAlertEmail, trailLogGroup, }); - new FlowLogs(this, "FlowLogs"); + new FlowLogs(this, "FlowLogs", { + namePrefix: "seahaven", + vpcIds: props.flowLogVpcIds, + }); new SesMonitoring(this, "SesMonitoring"); // Shared CloudFront WAF WebACL (M-17); ARN published to SSM for app stacks. new AppWebAcl(this, "AppWebAcl"); diff --git a/lib/detective-controls.ts b/lib/detective-controls.ts index 8bc164f..a3fa3bd 100644 --- a/lib/detective-controls.ts +++ b/lib/detective-controls.ts @@ -16,14 +16,29 @@ import { Construct } from "constructs"; * H-4 Security Hub with AWS FSBP + CIS v3.0 standards * M-5 IAM Access Analyzer (account-scoped external-access analyzer) * + * Serves both the management-account baseline (namePrefix "seahaven") and + * member-account baselines (e.g. "seahaven-extdev") — physical resource names + * are prefix-parameterized, structure is identical. + * * Scope is us-east-1 only — all workloads live here (Adam's call, Day 1). * Multi-region coverage is a documented follow-up. */ +export interface DetectiveControlsProps { + /** + * Physical-name prefix for account-scoped resources (bucket, roles, recorder, + * delivery channel, analyzer). Also baked into the custom resources' + * PhysicalResourceId strings — changing it on a deployed stack REPLACES the + * custom resources; keep it stable per account. + */ + readonly namePrefix: string; +} + export class DetectiveControls extends Construct { - constructor(scope: Construct, id: string) { + constructor(scope: Construct, id: string, props: DetectiveControlsProps) { super(scope, id); const stack = cdk.Stack.of(this); + const prefix = props.namePrefix; // ────────────────────────────────────────────────────────────────────── // H-2 AWS Config @@ -33,7 +48,7 @@ export class DetectiveControls extends Construct { // versioned, SSE-S3 (Config writes here; SSE-S3 avoids a second KMS-grant // failure mode and is sufficient — CIS does not require a CMK here). const configBucket = new s3.Bucket(this, "ConfigBucket", { - bucketName: `seahaven-config-${stack.account}`, + bucketName: `${prefix}-config-${stack.account}`, encryption: s3.BucketEncryption.S3_MANAGED, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, enforceSSL: true, @@ -85,7 +100,7 @@ export class DetectiveControls extends Construct { // grants delivery to the bucket above. **This role is the Day 1 cross-review // item (IAM change per CLAUDE.md).** const recorderRole = new iam.Role(this, "ConfigRecorderRole", { - roleName: "seahaven-config-recorder-role", + roleName: `${prefix}-config-recorder-role`, assumedBy: new iam.ServicePrincipal("config.amazonaws.com"), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWS_ConfigRole"), @@ -143,7 +158,7 @@ export class DetectiveControls extends Construct { this, "ConfigCustomResourceRole", { - roleName: "seahaven-config-custom-resource-role", + roleName: `${prefix}-config-custom-resource-role`, assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), managedPolicies: [ iam.ManagedPolicy.fromAwsManagedPolicyName( @@ -191,7 +206,7 @@ export class DetectiveControls extends Construct { action: "putConfigurationRecorder", parameters: { ConfigurationRecorder: { - name: "seahaven-config-recorder", + name: `${prefix}-config-recorder`, roleARN: recorderRole.roleArn, recordingGroup: { allSupported: true, @@ -200,7 +215,7 @@ export class DetectiveControls extends Construct { }, }, // No meaningful response data to extract. - physicalResourceId: cr.PhysicalResourceId.of("seahaven-config-recorder"), + physicalResourceId: cr.PhysicalResourceId.of(`${prefix}-config-recorder`), }; const putChannelCall: cr.AwsSdkCall = { @@ -208,7 +223,7 @@ export class DetectiveControls extends Construct { action: "putDeliveryChannel", parameters: { DeliveryChannel: { - name: "seahaven-config-delivery", + name: `${prefix}-config-delivery`, s3BucketName: configBucket.bucketName, configSnapshotDeliveryProperties: { deliveryFrequency: "TwentyFour_Hours", @@ -216,7 +231,7 @@ export class DetectiveControls extends Construct { }, }, physicalResourceId: cr.PhysicalResourceId.of( - "seahaven-config-delivery" + `${prefix}-config-delivery` ), }; @@ -224,10 +239,10 @@ export class DetectiveControls extends Construct { service: "ConfigService", action: "startConfigurationRecorder", parameters: { - ConfigurationRecorderName: "seahaven-config-recorder", + ConfigurationRecorderName: `${prefix}-config-recorder`, }, physicalResourceId: cr.PhysicalResourceId.of( - "seahaven-config-recorder-start" + `${prefix}-config-recorder-start` ), }; @@ -265,10 +280,10 @@ export class DetectiveControls extends Construct { service: "ConfigService", action: "stopConfigurationRecorder", parameters: { - ConfigurationRecorderName: "seahaven-config-recorder", + ConfigurationRecorderName: `${prefix}-config-recorder`, }, physicalResourceId: cr.PhysicalResourceId.of( - "seahaven-config-recorder-stop" + `${prefix}-config-recorder-stop` ), }, role: configCustomResourceRole, @@ -336,7 +351,7 @@ export class DetectiveControls extends Construct { // M-5 IAM Access Analyzer (free, account-scoped external-access) // ────────────────────────────────────────────────────────────────────── new accessanalyzer.CfnAnalyzer(this, "AccountAnalyzer", { - analyzerName: "seahaven-account-analyzer", + analyzerName: `${prefix}-account-analyzer`, type: "ACCOUNT", }); diff --git a/lib/flow-logs.ts b/lib/flow-logs.ts index 75205d6..151dd84 100644 --- a/lib/flow-logs.ts +++ b/lib/flow-logs.ts @@ -5,32 +5,39 @@ import * as ec2 from "aws-cdk-lib/aws-ec2"; import { Construct } from "constructs"; /** - * VPC flow logs for every VPC, delivered to a hardened S3 bucket (audit H-14, - * CIS 3.9/5.6). S3 destination (not CloudWatch Logs) for cost — query - * forensically via Athena. ALL traffic (accept + reject). + * VPC flow logs delivered to a hardened S3 bucket (audit H-14, CIS 3.9/5.6). + * S3 destination (not CloudWatch Logs) for cost — query forensically via + * Athena. ALL traffic (accept + reject). + * + * Serves both the management-account baseline and member-account baselines: + * VPC ids are passed via props (the management account pins its 5 audited + * VPCs in bin/app.ts; member accounts source theirs from cdk context because + * their VPCs change over time). Pass an empty list to create the hardened + * destination bucket without any flow logs attached yet. * * S3 delivery needs no IAM role; instead the bucket policy grants the * `delivery.logs.amazonaws.com` service principal write access, scoped to this * account. That bucket policy is the Day 2 cross-review item. */ - -// All 5 VPCs in 328440206208 / us-east-1 (4 custom + default), audit Agent 7. -const VPC_IDS = [ - "vpc-061d66990b6a4d1fb", - "vpc-0542a9e934b417d23", - "vpc-062d200c68bd4ca0e", - "vpc-0d3d4b67bd0cf8a68", - "vpc-02c10a89d66f6f9b8", -]; +export interface FlowLogsProps { + /** Physical-name prefix for the destination bucket (e.g. "seahaven" or "seahaven-extdev"). */ + readonly namePrefix: string; + /** + * VPC ids to attach ALL-traffic flow logs to. May be empty. Logical IDs are + * index-derived (FlowLog0, FlowLog1, ...) — REORDERING this list replaces + * deployed flow logs; only append. + */ + readonly vpcIds: string[]; +} export class FlowLogs extends Construct { - constructor(scope: Construct, id: string) { + constructor(scope: Construct, id: string, props: FlowLogsProps) { super(scope, id); const stack = cdk.Stack.of(this); const bucket = new s3.Bucket(this, "FlowLogsBucket", { - bucketName: `seahaven-vpc-flow-logs-${stack.account}`, + bucketName: `${props.namePrefix}-vpc-flow-logs-${stack.account}`, encryption: s3.BucketEncryption.S3_MANAGED, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, enforceSSL: true, @@ -90,7 +97,7 @@ export class FlowLogs extends Construct { }) ); - VPC_IDS.forEach((vpcId, i) => { + props.vpcIds.forEach((vpcId, i) => { const flowLog = new ec2.CfnFlowLog(this, `FlowLog${i}`, { resourceId: vpcId, resourceType: "VPC", diff --git a/lib/governance-toggles.ts b/lib/governance-toggles.ts index e1544e6..d198726 100644 --- a/lib/governance-toggles.ts +++ b/lib/governance-toggles.ts @@ -3,6 +3,8 @@ import * as budgets from "aws-cdk-lib/aws-budgets"; import { Construct } from "constructs"; export interface GovernanceTogglesProps { + /** Physical name of the AWS Budget (account-scoped, e.g. "seahaven-monthly-cost"). */ + readonly budgetName: string; /** Monthly cost budget ceiling in USD. */ readonly monthlyLimitUsd: number; /** Email that receives the budget threshold alerts. */ @@ -11,7 +13,8 @@ export interface GovernanceTogglesProps { /** * Account-level governance toggles that *are* expressible as CloudFormation - * (audit Day 1). + * (audit Day 1). Serves both the management-account baseline and member-account + * baselines (budget name parameterized per account). * * Closes: * M-10 Monthly AWS Budget with 80% / 100% actual + 100% forecast alerts @@ -37,7 +40,7 @@ export class GovernanceToggles extends Construct { new budgets.CfnBudget(this, "MonthlyCostBudget", { budget: { - budgetName: "seahaven-monthly-cost", + budgetName: props.budgetName, budgetType: "COST", timeUnit: "MONTHLY", budgetLimit: { diff --git a/lib/member-baseline-stack.ts b/lib/member-baseline-stack.ts new file mode 100644 index 0000000..bec50ab --- /dev/null +++ b/lib/member-baseline-stack.ts @@ -0,0 +1,64 @@ +import * as cdk from "aws-cdk-lib"; +import { Construct } from "constructs"; +import { DetectiveControls } from "./detective-controls"; +import { FlowLogs } from "./flow-logs"; +import { GovernanceToggles } from "./governance-toggles"; + +export interface MemberBaselineStackProps extends cdk.StackProps { + /** + * Physical-name prefix for account-scoped resources (e.g. "seahaven-extdev"). + * Also names the monthly budget (`-monthly-cost`). Stable per + * account — changing it on a deployed stack replaces live resources. + */ + readonly namePrefix: string; + /** Monthly cost budget ceiling in USD. */ + readonly monthlyBudgetUsd: number; + /** Sea Haven ops address that receives budget alerts (not the account's tenants). */ + readonly budgetAlertEmail: string; + /** VPC ids to attach flow logs to (from cdk context; may be empty). */ + readonly flowLogVpcIds: string[]; + /** Value for the ManagedBy tag on every resource in the stack. */ + readonly managedByTag: string; +} + +/** + * Account-local security baseline for org MEMBER accounts (first tenant: + * seahaven-external-dev). Absorbed from the retired seahaven-external-dev-baseline + * repo — a stripped fork of the management-account baseline, now sharing its + * constructs (prefix-parameterized) instead of forking them. + * + * Deliberately excludes everything that is org-level or prod-specific: + * - No local CloudTrail — the management-account org trail (seahaven-org-trail) + * already captures every member account's events centrally. + * - No CIS Section-4 metric-filter alarms — the Security Hub CIS standard + * evaluates those controls against Config without a local trail log group. + * - 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 + * Analyzer, Inspector2 (post-deploy CLI, see README), VPC flow logs, and a + * monthly cost Budget. + */ +export class MemberBaselineStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: MemberBaselineStackProps) { + super(scope, id, props); + + new DetectiveControls(this, "DetectiveControls", { + namePrefix: props.namePrefix, + }); + + new FlowLogs(this, "FlowLogs", { + namePrefix: props.namePrefix, + vpcIds: props.flowLogVpcIds, + }); + + new GovernanceToggles(this, "GovernanceToggles", { + budgetName: `${props.namePrefix}-monthly-cost`, + monthlyLimitUsd: props.monthlyBudgetUsd, + alertEmail: props.budgetAlertEmail, + }); + + cdk.Tags.of(this).add("Owner", props.budgetAlertEmail); + cdk.Tags.of(this).add("ManagedBy", props.managedByTag); + } +} diff --git a/package.json b/package.json index 07f95e6..7ec5b7e 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "seahaven-account-baseline", + "name": "seahaven-org-baseline", "version": "1.0.0", "bin": { "app": "bin/app.js"