Add Bedrock invocation logging destinations (#12)

Audit finding H-20: no audit trail of model I/O for seahaven-alex,
which returns payments, invoices, WO/PO, and HR/SA8000 data.

S3 bucket (Glacier at 90d, expire 365d) + CloudWatch log group (90d)
+ delivery role assumable only by bedrock.amazonaws.com scoped by
SourceAccount/SourceArn. The account-level logging configuration has
no CloudFormation resource type, so it is applied via CLI post-deploy
(documented in the construct header) - same pattern as the Config
recorder (INFRA-17).

Cross-reviewed: no BLOCKs. Verified live: converse invocation logged
to /aws/bedrock/model-invocations.
This commit is contained in:
Adam Moussa 2026-06-03 15:17:39 -04:00 • committed by GitHub
parent 14593440cf
commit 2289dcb0c9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 115 additions and 0 deletions

View file

@ -7,6 +7,7 @@ import * as cloudtrail from "aws-cdk-lib/aws-cloudtrail";
import { Construct } from "constructs"; import { Construct } from "constructs";
import { DetectiveControls } from "./detective-controls"; import { DetectiveControls } from "./detective-controls";
import { GovernanceToggles } from "./governance-toggles"; import { GovernanceToggles } from "./governance-toggles";
import { BedrockLogging } from "./bedrock-logging";
import { CisMonitoring } from "./cis-monitoring"; import { CisMonitoring } from "./cis-monitoring";
import { FlowLogs } from "./flow-logs"; import { FlowLogs } from "./flow-logs";
import { SesMonitoring } from "./ses-monitoring"; import { SesMonitoring } from "./ses-monitoring";
@ -158,6 +159,12 @@ export class AccountBaselineStack extends cdk.Stack {
// Shared CloudFront WAF WebACL (M-17); ARN published to SSM for app stacks. // Shared CloudFront WAF WebACL (M-17); ARN published to SSM for app stacks.
new AppWebAcl(this, "AppWebAcl"); new AppWebAcl(this, "AppWebAcl");
// ── Day 5 AI governance ──
// Bedrock model invocation logging destinations + delivery role (H-20).
// The account-level logging configuration itself has no CFN resource type;
// applied via CLI post-deploy (see lib/bedrock-logging.ts header).
new BedrockLogging(this, "BedrockLogging");
cdk.Tags.of(this).add("Project", "account-baseline"); cdk.Tags.of(this).add("Project", "account-baseline");
cdk.Tags.of(this).add("Owner", "adam@seahavenind.com"); cdk.Tags.of(this).add("Owner", "adam@seahavenind.com");
cdk.Tags.of(this).add("Environment", "prod"); cdk.Tags.of(this).add("Environment", "prod");

108
lib/bedrock-logging.ts Normal file
View file

@ -0,0 +1,108 @@
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 logs from "aws-cdk-lib/aws-logs";
import { Construct } from "constructs";
/**
* Destinations + delivery role for Bedrock model invocation logging (audit
* H-20). `seahaven-alex` is employee-facing and returns payments, invoices,
* WO/PO, and HR/SA8000 data — model I/O needs an audit trail.
*
* CloudFormation has no resource type for the logging configuration itself
* (account-level `PutModelInvocationLoggingConfiguration`), so — like the
* Config recorder (INFRA-17) — the toggle is applied via CLI after deploy:
*
* aws bedrock put-model-invocation-logging-configuration --logging-config '{
* "cloudWatchConfig": {
* "logGroupName": "<BedrockInvocationLogGroup>",
* "roleArn": "<BedrockLoggingRole ARN>",
* "largeDataDeliveryS3Config": {"bucketName": "<bucket>", "keyPrefix": "large-payloads"}
* },
* "s3Config": {"bucketName": "<bucket>", "keyPrefix": "invocation-logs"},
* "textDataDeliveryEnabled": true,
* "imageDataDeliveryEnabled": true,
* "embeddingDataDeliveryEnabled": false
* }'
*/
export class BedrockLogging extends Construct {
public readonly bucket: s3.Bucket;
public readonly logGroup: logs.LogGroup;
public readonly deliveryRole: iam.Role;
constructor(scope: Construct, id: string) {
super(scope, id);
const stack = cdk.Stack.of(this);
this.bucket = new s3.Bucket(this, "InvocationLogsBucket", {
bucketName: `seahaven-bedrock-invocation-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,
});
// Bedrock writes invocation logs to S3 directly via bucket policy — no role.
this.bucket.addToResourcePolicy(
new iam.PolicyStatement({
sid: "AmazonBedrockLogsWrite",
effect: iam.Effect.ALLOW,
principals: [new iam.ServicePrincipal("bedrock.amazonaws.com")],
actions: ["s3:PutObject"],
resources: [this.bucket.arnForObjects("*")],
conditions: {
StringEquals: { "aws:SourceAccount": stack.account },
ArnLike: {
"aws:SourceArn": `arn:aws:bedrock:${stack.region}:${stack.account}:*`,
},
},
}),
);
this.logGroup = new logs.LogGroup(this, "InvocationLogGroup", {
logGroupName: "/aws/bedrock/model-invocations",
retention: logs.RetentionDays.THREE_MONTHS,
removalPolicy: cdk.RemovalPolicy.RETAIN,
});
// CloudWatch delivery requires a role Bedrock can assume, scoped to this
// account/source and to the one log group.
this.deliveryRole = new iam.Role(this, "DeliveryRole", {
roleName: "seahaven-bedrock-invocation-logging",
assumedBy: new iam.ServicePrincipal("bedrock.amazonaws.com", {
conditions: {
StringEquals: { "aws:SourceAccount": stack.account },
ArnLike: {
"aws:SourceArn": `arn:aws:bedrock:${stack.region}:${stack.account}:*`,
},
},
}),
});
this.deliveryRole.addToPolicy(
new iam.PolicyStatement({
actions: ["logs:CreateLogStream", "logs:PutLogEvents"],
resources: [this.logGroup.logGroupArn, `${this.logGroup.logGroupArn}:log-stream:*`],
}),
);
new cdk.CfnOutput(this, "BedrockLogBucketName", { value: this.bucket.bucketName });
new cdk.CfnOutput(this, "BedrockLogGroupName", { value: this.logGroup.logGroupName });
new cdk.CfnOutput(this, "BedrockLoggingRoleArn", { value: this.deliveryRole.roleArn });
}
}