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 }); } }