2026-06-02 15:16:24 -04:00
|
|
|
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";
|
|
|
|
|
|
|
|
|
|
/**
|
2026-07-14 13:53:07 -04:00
|
|
|
* 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.
|
2026-06-02 15:16:24 -04:00
|
|
|
*
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2026-07-14 13:53:07 -04:00
|
|
|
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[];
|
|
|
|
|
}
|
2026-06-02 15:16:24 -04:00
|
|
|
|
|
|
|
|
export class FlowLogs extends Construct {
|
2026-07-14 13:53:07 -04:00
|
|
|
constructor(scope: Construct, id: string, props: FlowLogsProps) {
|
2026-06-02 15:16:24 -04:00
|
|
|
super(scope, id);
|
|
|
|
|
|
|
|
|
|
const stack = cdk.Stack.of(this);
|
|
|
|
|
|
|
|
|
|
const bucket = new s3.Bucket(this, "FlowLogsBucket", {
|
2026-07-14 13:53:07 -04:00
|
|
|
bucketName: `${props.namePrefix}-vpc-flow-logs-${stack.account}`,
|
2026-06-02 15:16:24 -04:00
|
|
|
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}:*`,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
|
2026-07-14 13:53:07 -04:00
|
|
|
props.vpcIds.forEach((vpcId, i) => {
|
2026-06-02 15:16:24 -04:00
|
|
|
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 });
|
|
|
|
|
}
|
|
|
|
|
}
|