mirror of
https://github.com/Sea-Haven-Industries/seahaven-org-baseline.git
synced 2026-09-30 20:53:17 +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). * 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.
115 lines
4.2 KiB
TypeScript
115 lines
4.2 KiB
TypeScript
import * as cdk from "aws-cdk-lib";
|
|
import * as s3 from "aws-cdk-lib/aws-s3";
|
|
import * as iam from "aws-cdk-lib/aws-iam";
|
|
import * as ec2 from "aws-cdk-lib/aws-ec2";
|
|
import { Construct } from "constructs";
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
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, props: FlowLogsProps) {
|
|
super(scope, id);
|
|
|
|
const stack = cdk.Stack.of(this);
|
|
|
|
const bucket = new s3.Bucket(this, "FlowLogsBucket", {
|
|
bucketName: `${props.namePrefix}-vpc-flow-logs-${stack.account}`,
|
|
encryption: s3.BucketEncryption.S3_MANAGED,
|
|
blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
|
|
enforceSSL: true,
|
|
versioned: false,
|
|
lifecycleRules: [
|
|
{
|
|
id: "transition-and-expire",
|
|
transitions: [
|
|
{
|
|
storageClass: s3.StorageClass.GLACIER,
|
|
transitionAfter: cdk.Duration.days(90),
|
|
},
|
|
],
|
|
expiration: cdk.Duration.days(365),
|
|
abortIncompleteMultipartUploadAfter: cdk.Duration.days(7),
|
|
},
|
|
],
|
|
removalPolicy: cdk.RemovalPolicy.RETAIN,
|
|
});
|
|
|
|
// Log-delivery service permissions (scoped to this account) — the standard
|
|
// VPC-flow-logs-to-S3 bucket policy.
|
|
bucket.addToResourcePolicy(
|
|
new iam.PolicyStatement({
|
|
sid: "AWSLogDeliveryWrite",
|
|
effect: iam.Effect.ALLOW,
|
|
principals: [new iam.ServicePrincipal("delivery.logs.amazonaws.com")],
|
|
actions: ["s3:PutObject"],
|
|
resources: [bucket.arnForObjects(`AWSLogs/${stack.account}/*`)],
|
|
conditions: {
|
|
StringEquals: {
|
|
"s3:x-amz-acl": "bucket-owner-full-control",
|
|
"aws:SourceAccount": stack.account,
|
|
},
|
|
ArnLike: {
|
|
"aws:SourceArn": `arn:${stack.partition}:logs:${stack.region}:${stack.account}:*`,
|
|
},
|
|
},
|
|
})
|
|
);
|
|
bucket.addToResourcePolicy(
|
|
new iam.PolicyStatement({
|
|
sid: "AWSLogDeliveryAclCheck",
|
|
effect: iam.Effect.ALLOW,
|
|
principals: [new iam.ServicePrincipal("delivery.logs.amazonaws.com")],
|
|
// AWS's documented flow-logs-to-S3 policy uses GetBucketAcl only
|
|
// (verified against flow-logs-s3-permissions.html); ListBucket is not
|
|
// needed and would be over-permissioned.
|
|
actions: ["s3:GetBucketAcl"],
|
|
resources: [bucket.bucketArn],
|
|
conditions: {
|
|
StringEquals: { "aws:SourceAccount": stack.account },
|
|
ArnLike: {
|
|
"aws:SourceArn": `arn:${stack.partition}:logs:${stack.region}:${stack.account}:*`,
|
|
},
|
|
},
|
|
})
|
|
);
|
|
|
|
props.vpcIds.forEach((vpcId, i) => {
|
|
const flowLog = new ec2.CfnFlowLog(this, `FlowLog${i}`, {
|
|
resourceId: vpcId,
|
|
resourceType: "VPC",
|
|
trafficType: "ALL",
|
|
logDestinationType: "s3",
|
|
logDestination: bucket.bucketArn,
|
|
maxAggregationInterval: 600,
|
|
tags: [{ key: "Name", value: `flow-log-${vpcId}` }],
|
|
});
|
|
flowLog.node.addDependency(bucket.policy!);
|
|
});
|
|
|
|
new cdk.CfnOutput(this, "FlowLogsBucketName", { value: bucket.bucketName });
|
|
}
|
|
}
|