seahaven-org-baseline/lib/flow-logs.ts
Adam Moussa 42781f743e
fix(baseline): drop departed mgmt resources and retain drifted web acl (#171)
Nightly backup jobs fail on resources that have left the management account.
The CloudFront WebACL is already gone while CloudFormation still owns it, so
the deletion policy has to be Retain before a later change can remove it.
2026-10-01 23:57:42 +00:00

117 lines
4.4 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 a stable-index
* list in bin/app.ts; member accounts pass a dense list). Pass an empty list
* to create the hardened destination bucket without any flow logs attached
* yet. An empty slot keeps its index so later logical ids do not shift.
*
* 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. A hole
* (`undefined`) reserves that index and creates no flow log. Logical IDs are
* index-derived (FlowLog0, FlowLog1, ...) — REORDERING this list replaces
* deployed flow logs; only append, and never close a hole.
*/
readonly vpcIds: readonly (string | undefined)[];
}
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) => {
if (!vpcId) return;
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 });
}
}