mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-09-30 05:43:17 +00:00
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:
parent
f3c37d5b20
commit
3ab3bc773a
9 changed files with 234 additions and 61 deletions
18
.github/workflows/deploy.yaml
vendored
18
.github/workflows/deploy.yaml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
70
README.md
70
README.md
|
|
@ -1,26 +1,34 @@
|
|||
# seahaven-account-baseline
|
||||
# seahaven-org-baseline
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
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
|
||||
|
|
|
|||
41
bin/app.ts
41
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) ─────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
64
lib/member-baseline-stack.ts
Normal file
64
lib/member-baseline-stack.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "seahaven-account-baseline",
|
||||
"name": "seahaven-org-baseline",
|
||||
"version": "1.0.0",
|
||||
"bin": {
|
||||
"app": "bin/app.js"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue