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 (ALL traffic), forensically * queryable via Athena. Adapted from seahaven-account-baseline/lib/flow-logs.ts. * * Unlike the prod baseline, the VPC ids are NOT hardcoded — they are passed in * via props (sourced from cdk context in bin/app.ts), because this account's * VPCs change as the external dev team provisions their own infrastructure. * Pass an empty list to create the hardened destination bucket without any * flow logs attached yet (e.g. before the team's first VPC exists, or while the * default VPC is pending deletion). */ export interface FlowLogsProps { /** VPC ids to attach ALL-traffic flow logs to. May be empty. */ 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: `seahaven-extdev-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")], 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 }); } }