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:
|
2026-10-01 23:57:42 +00:00
|
|
|
* 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.
|
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;
|
|
|
|
|
/**
|
2026-10-01 23:57:42 +00:00
|
|
|
* 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
|
2026-07-14 13:53:07 -04:00
|
|
|
* index-derived (FlowLog0, FlowLog1, ...) — REORDERING this list replaces
|
2026-10-01 23:57:42 +00:00
|
|
|
* deployed flow logs; only append, and never close a hole.
|
2026-07-14 13:53:07 -04:00
|
|
|
*/
|
2026-10-01 23:57:42 +00:00
|
|
|
readonly vpcIds: readonly (string | undefined)[];
|
2026-07-14 13:53:07 -04:00
|
|
|
}
|
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-10-01 23:57:42 +00:00
|
|
|
if (!vpcId) return;
|
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 });
|
|
|
|
|
}
|
|
|
|
|
}
|