mirror of
https://github.com/Sea-Haven-Industries/seahaven-account-baseline.git
synced 2026-08-04 16:56:14 +00:00
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).
This commit is contained in:
parent
f3c37d5b20
commit
1041a157da
4 changed files with 68 additions and 32 deletions
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue