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.
This commit is contained in:
Adam Moussa 2026-07-14 13:53:07 -04:00 • committed by GitHub
parent f3c37d5b20
commit 3ab3bc773a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 234 additions and 61 deletions

View file

@ -11,10 +11,24 @@ concurrency:
group: deploy group: deploy
cancel-in-progress: false 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: jobs:
deploy: deploy-management:
uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@fd60e4c9041784f666ac0fdefb9bec3c7fbf5143 # main uses: Sea-Haven-Industries/.github/.github/workflows/cd-cdk.yaml@3cde673b9d05c0e68aac4d997d582f2543853d20 # main
with: with:
node-version: "24" node-version: "24"
stacks: "account-baseline dynamodb-cmk regional-baseline-us-west-2 regional-baseline-us-east-2 backup-offsite backup"
secrets: secrets:
deploy-role-arn: ${{ secrets.AWS_DEPLOY_ROLE_ARN }} 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 }}

View file

@ -1,26 +1,34 @@
# seahaven-account-baseline # seahaven-org-baseline
![TypeScript](https://img.shields.io/badge/TypeScript-3178C6?logo=typescript&logoColor=white) ![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) ![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 Organization-wide security and governance baseline for Sea Haven Industries,
(AWS account **328440206208**), managed as a single CDK TypeScript app. The managed as a single CDK TypeScript app. Covers the management account
primary baseline is in **us-east-1**, with secondary-region baselines in (**328440206208**: primary baseline in **us-east-1**, secondary-region
**us-east-2** and **us-west-2** and the offsite backup vault in **us-west-2**. baselines in **us-east-2**/**us-west-2**, offsite backup vault in
This is where account-wide detective and recovery controls live, so they are **us-west-2**) and org **member accounts** (first tenant:
versioned, reviewed, and drift-checked like any other stack. `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 | Stacks (deployed by the CD workflow — one job per target account):
|---|---|---|
| `seahaven-account-baseline` | us-east-1 | CloudTrail + future detective controls (C-1) | | Stack | Account | Region | Purpose |
| `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-account-baseline` | 328440206208 | us-east-1 | CloudTrail + detective controls (C-1) |
| `seahaven-regional-baseline-us-east-2` | us-east-2 | Bedrock invocation logging + AWS Config recorder + Security Hub (INFRA-91 / INFRA-16) | | `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-backup` | us-east-1 | Primary AWS Backup vault + plan + role (C-7) | | `seahaven-regional-baseline-us-west-2` | 328440206208 | us-west-2 | Bedrock invocation logging (INFRA-91) |
| `seahaven-backup-offsite` | us-west-2 | Governance-locked offsite copy vault (C-7) | | `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 ## 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`) | | `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 six stacks across three regions: `bin/app.ts` synthesizes seven stacks across three regions and two accounts:
| Construct id | Stack name | Region | Source | | Construct id | Stack name | Account | Region | Source |
|---|---|---|---| |---|---|---|---|---|
| `account-baseline` | `seahaven-account-baseline` | us-east-1 | `lib/account-baseline-stack.ts` | | `account-baseline` | `seahaven-account-baseline` | 328440206208 | us-east-1 | `lib/account-baseline-stack.ts` |
| `dynamodb-cmk` | `seahaven-dynamodb-cmk` | us-east-1 | `lib/dynamodb-cmk-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` | us-west-2 | `lib/regional-baseline-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` | us-east-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` | us-west-2 | `lib/backup-offsite-stack.ts` | | `backup-offsite` | `seahaven-backup-offsite` | 328440206208 | us-west-2 | `lib/backup-offsite-stack.ts` |
| `backup` | `seahaven-backup` | 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` |
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 `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

@ -6,8 +6,20 @@ import { BackupOffsiteStack } from "../lib/backup-offsite-stack";
import { BackupStack } from "../lib/backup-stack"; import { BackupStack } from "../lib/backup-stack";
import { RegionalBaselineStack } from "../lib/regional-baseline-stack"; import { RegionalBaselineStack } from "../lib/regional-baseline-stack";
import { DynamoDbCmkStack } from "../lib/dynamodb-cmk-stack"; import { DynamoDbCmkStack } from "../lib/dynamodb-cmk-stack";
import { MemberBaselineStack } from "../lib/member-baseline-stack";
const ACCOUNT = "328440206208"; 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(); const app = new cdk.App();
@ -16,6 +28,35 @@ new AccountBaselineStack(app, "account-baseline", {
env: { account: ACCOUNT, region: "us-east-1" }, env: { account: ACCOUNT, region: "us-east-1" },
monthlyBudgetUsd: 1200, monthlyBudgetUsd: 1200,
budgetAlertEmail: "adam@seahavenind.com", 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) ───────────────────────────────────── // ── Shared DynamoDB CMK (INFRA-95 / M-3) ─────────────────────────────────────

View file

@ -31,6 +31,11 @@ export interface AccountBaselineStackProps extends cdk.StackProps {
readonly monthlyBudgetUsd: number; readonly monthlyBudgetUsd: number;
/** Email for budget threshold alerts (M-10). */ /** Email for budget threshold alerts (M-10). */
readonly budgetAlertEmail: string; 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 { export class AccountBaselineStack extends cdk.Stack {
@ -225,9 +230,12 @@ export class AccountBaselineStack extends cdk.Stack {
// ── Day 1 detective layer + governance toggles ── // ── Day 1 detective layer + governance toggles ──
// Config (H-2), GuardDuty (H-3), Security Hub (H-4), Access Analyzer (M-5). // 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. // Monthly cost budget (M-10). Other governance toggles are CLI + documented.
new GovernanceToggles(this, "GovernanceToggles", { new GovernanceToggles(this, "GovernanceToggles", {
budgetName: "seahaven-monthly-cost",
monthlyLimitUsd: props.monthlyBudgetUsd, monthlyLimitUsd: props.monthlyBudgetUsd,
alertEmail: props.budgetAlertEmail, alertEmail: props.budgetAlertEmail,
}); });
@ -239,7 +247,10 @@ export class AccountBaselineStack extends cdk.Stack {
alarmEmail: props.budgetAlertEmail, alarmEmail: props.budgetAlertEmail,
trailLogGroup, trailLogGroup,
}); });
new FlowLogs(this, "FlowLogs"); new FlowLogs(this, "FlowLogs", {
namePrefix: "seahaven",
vpcIds: props.flowLogVpcIds,
});
new SesMonitoring(this, "SesMonitoring"); new SesMonitoring(this, "SesMonitoring");
// Shared CloudFront WAF WebACL (M-17); ARN published to SSM for app stacks. // Shared CloudFront WAF WebACL (M-17); ARN published to SSM for app stacks.
new AppWebAcl(this, "AppWebAcl"); new AppWebAcl(this, "AppWebAcl");

View file

@ -16,14 +16,29 @@ import { Construct } from "constructs";
* H-4 Security Hub with AWS FSBP + CIS v3.0 standards * H-4 Security Hub with AWS FSBP + CIS v3.0 standards
* M-5 IAM Access Analyzer (account-scoped external-access analyzer) * 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). * Scope is us-east-1 only — all workloads live here (Adam's call, Day 1).
* Multi-region coverage is a documented follow-up. * 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 { export class DetectiveControls extends Construct {
constructor(scope: Construct, id: string) { constructor(scope: Construct, id: string, props: DetectiveControlsProps) {
super(scope, id); super(scope, id);
const stack = cdk.Stack.of(this); const stack = cdk.Stack.of(this);
const prefix = props.namePrefix;
// ────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────
// H-2 AWS Config // 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 // 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). // failure mode and is sufficient — CIS does not require a CMK here).
const configBucket = new s3.Bucket(this, "ConfigBucket", { const configBucket = new s3.Bucket(this, "ConfigBucket", {
bucketName: `seahaven-config-${stack.account}`, bucketName: `${prefix}-config-${stack.account}`,
encryption: s3.BucketEncryption.S3_MANAGED, encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
enforceSSL: true, 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 // grants delivery to the bucket above. **This role is the Day 1 cross-review
// item (IAM change per CLAUDE.md).** // item (IAM change per CLAUDE.md).**
const recorderRole = new iam.Role(this, "ConfigRecorderRole", { const recorderRole = new iam.Role(this, "ConfigRecorderRole", {
roleName: "seahaven-config-recorder-role", roleName: `${prefix}-config-recorder-role`,
assumedBy: new iam.ServicePrincipal("config.amazonaws.com"), assumedBy: new iam.ServicePrincipal("config.amazonaws.com"),
managedPolicies: [ managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWS_ConfigRole"), iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AWS_ConfigRole"),
@ -143,7 +158,7 @@ export class DetectiveControls extends Construct {
this, this,
"ConfigCustomResourceRole", "ConfigCustomResourceRole",
{ {
roleName: "seahaven-config-custom-resource-role", roleName: `${prefix}-config-custom-resource-role`,
assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"), assumedBy: new iam.ServicePrincipal("lambda.amazonaws.com"),
managedPolicies: [ managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName( iam.ManagedPolicy.fromAwsManagedPolicyName(
@ -191,7 +206,7 @@ export class DetectiveControls extends Construct {
action: "putConfigurationRecorder", action: "putConfigurationRecorder",
parameters: { parameters: {
ConfigurationRecorder: { ConfigurationRecorder: {
name: "seahaven-config-recorder", name: `${prefix}-config-recorder`,
roleARN: recorderRole.roleArn, roleARN: recorderRole.roleArn,
recordingGroup: { recordingGroup: {
allSupported: true, allSupported: true,
@ -200,7 +215,7 @@ export class DetectiveControls extends Construct {
}, },
}, },
// No meaningful response data to extract. // No meaningful response data to extract.
physicalResourceId: cr.PhysicalResourceId.of("seahaven-config-recorder"), physicalResourceId: cr.PhysicalResourceId.of(`${prefix}-config-recorder`),
}; };
const putChannelCall: cr.AwsSdkCall = { const putChannelCall: cr.AwsSdkCall = {
@ -208,7 +223,7 @@ export class DetectiveControls extends Construct {
action: "putDeliveryChannel", action: "putDeliveryChannel",
parameters: { parameters: {
DeliveryChannel: { DeliveryChannel: {
name: "seahaven-config-delivery", name: `${prefix}-config-delivery`,
s3BucketName: configBucket.bucketName, s3BucketName: configBucket.bucketName,
configSnapshotDeliveryProperties: { configSnapshotDeliveryProperties: {
deliveryFrequency: "TwentyFour_Hours", deliveryFrequency: "TwentyFour_Hours",
@ -216,7 +231,7 @@ export class DetectiveControls extends Construct {
}, },
}, },
physicalResourceId: cr.PhysicalResourceId.of( physicalResourceId: cr.PhysicalResourceId.of(
"seahaven-config-delivery" `${prefix}-config-delivery`
), ),
}; };
@ -224,10 +239,10 @@ export class DetectiveControls extends Construct {
service: "ConfigService", service: "ConfigService",
action: "startConfigurationRecorder", action: "startConfigurationRecorder",
parameters: { parameters: {
ConfigurationRecorderName: "seahaven-config-recorder", ConfigurationRecorderName: `${prefix}-config-recorder`,
}, },
physicalResourceId: cr.PhysicalResourceId.of( physicalResourceId: cr.PhysicalResourceId.of(
"seahaven-config-recorder-start" `${prefix}-config-recorder-start`
), ),
}; };
@ -265,10 +280,10 @@ export class DetectiveControls extends Construct {
service: "ConfigService", service: "ConfigService",
action: "stopConfigurationRecorder", action: "stopConfigurationRecorder",
parameters: { parameters: {
ConfigurationRecorderName: "seahaven-config-recorder", ConfigurationRecorderName: `${prefix}-config-recorder`,
}, },
physicalResourceId: cr.PhysicalResourceId.of( physicalResourceId: cr.PhysicalResourceId.of(
"seahaven-config-recorder-stop" `${prefix}-config-recorder-stop`
), ),
}, },
role: configCustomResourceRole, role: configCustomResourceRole,
@ -336,7 +351,7 @@ export class DetectiveControls extends Construct {
// M-5 IAM Access Analyzer (free, account-scoped external-access) // M-5 IAM Access Analyzer (free, account-scoped external-access)
// ────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────
new accessanalyzer.CfnAnalyzer(this, "AccountAnalyzer", { new accessanalyzer.CfnAnalyzer(this, "AccountAnalyzer", {
analyzerName: "seahaven-account-analyzer", analyzerName: `${prefix}-account-analyzer`,
type: "ACCOUNT", type: "ACCOUNT",
}); });

View file

@ -5,32 +5,39 @@ import * as ec2 from "aws-cdk-lib/aws-ec2";
import { Construct } from "constructs"; import { Construct } from "constructs";
/** /**
* VPC flow logs for every VPC, delivered to a hardened S3 bucket (audit H-14, * VPC flow logs delivered to a hardened S3 bucket (audit H-14, CIS 3.9/5.6).
* CIS 3.9/5.6). S3 destination (not CloudWatch Logs) for cost — query * S3 destination (not CloudWatch Logs) for cost — query forensically via
* forensically via Athena. ALL traffic (accept + reject). * 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 * S3 delivery needs no IAM role; instead the bucket policy grants the
* `delivery.logs.amazonaws.com` service principal write access, scoped to this * `delivery.logs.amazonaws.com` service principal write access, scoped to this
* account. That bucket policy is the Day 2 cross-review item. * account. That bucket policy is the Day 2 cross-review item.
*/ */
export interface FlowLogsProps {
// All 5 VPCs in 328440206208 / us-east-1 (4 custom + default), audit Agent 7. /** Physical-name prefix for the destination bucket (e.g. "seahaven" or "seahaven-extdev"). */
const VPC_IDS = [ readonly namePrefix: string;
"vpc-061d66990b6a4d1fb", /**
"vpc-0542a9e934b417d23", * VPC ids to attach ALL-traffic flow logs to. May be empty. Logical IDs are
"vpc-062d200c68bd4ca0e", * index-derived (FlowLog0, FlowLog1, ...) — REORDERING this list replaces
"vpc-0d3d4b67bd0cf8a68", * deployed flow logs; only append.
"vpc-02c10a89d66f6f9b8", */
]; readonly vpcIds: string[];
}
export class FlowLogs extends Construct { export class FlowLogs extends Construct {
constructor(scope: Construct, id: string) { constructor(scope: Construct, id: string, props: FlowLogsProps) {
super(scope, id); super(scope, id);
const stack = cdk.Stack.of(this); const stack = cdk.Stack.of(this);
const bucket = new s3.Bucket(this, "FlowLogsBucket", { 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, encryption: s3.BucketEncryption.S3_MANAGED,
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
enforceSSL: true, 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}`, { const flowLog = new ec2.CfnFlowLog(this, `FlowLog${i}`, {
resourceId: vpcId, resourceId: vpcId,
resourceType: "VPC", resourceType: "VPC",

View file

@ -3,6 +3,8 @@ import * as budgets from "aws-cdk-lib/aws-budgets";
import { Construct } from "constructs"; import { Construct } from "constructs";
export interface GovernanceTogglesProps { 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. */ /** Monthly cost budget ceiling in USD. */
readonly monthlyLimitUsd: number; readonly monthlyLimitUsd: number;
/** Email that receives the budget threshold alerts. */ /** Email that receives the budget threshold alerts. */
@ -11,7 +13,8 @@ export interface GovernanceTogglesProps {
/** /**
* Account-level governance toggles that *are* expressible as CloudFormation * 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: * Closes:
* M-10 Monthly AWS Budget with 80% / 100% actual + 100% forecast alerts * 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", { new budgets.CfnBudget(this, "MonthlyCostBudget", {
budget: { budget: {
budgetName: "seahaven-monthly-cost", budgetName: props.budgetName,
budgetType: "COST", budgetType: "COST",
timeUnit: "MONTHLY", timeUnit: "MONTHLY",
budgetLimit: { budgetLimit: {

View file

@ -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 (`<namePrefix>-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);
}
}

View file

@ -1,5 +1,5 @@
{ {
"name": "seahaven-account-baseline", "name": "seahaven-org-baseline",
"version": "1.0.0", "version": "1.0.0",
"bin": { "bin": {
"app": "bin/app.js" "app": "bin/app.js"