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 for every VPC, 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). * * 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. */ // All 5 VPCs in 328440206208 / us-east-1 (4 custom + default), audit Agent 7. const VPC_IDS = [ "vpc-061d66990b6a4d1fb", "vpc-0542a9e934b417d23", "vpc-062d200c68bd4ca0e", "vpc-0d3d4b67bd0cf8a68", "vpc-02c10a89d66f6f9b8", ]; export class FlowLogs extends Construct { constructor(scope: Construct, id: string) { super(scope, id); const stack = cdk.Stack.of(this); const bucket = new s3.Bucket(this, "FlowLogsBucket", { bucketName: `seahaven-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}:*`, }, }, }) ); VPC_IDS.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 }); } }