diff --git a/bin/app.ts b/bin/app.ts index 72de6ac..1a08184 100644 --- a/bin/app.ts +++ b/bin/app.ts @@ -4,16 +4,43 @@ import * as cdk from "aws-cdk-lib"; import { AccountBaselineStack } from "../lib/account-baseline-stack"; import { BackupOffsiteStack } from "../lib/backup-offsite-stack"; import { BackupStack } from "../lib/backup-stack"; +import { RegionalBaselineStack } from "../lib/regional-baseline-stack"; + +const ACCOUNT = "328440206208"; const app = new cdk.App(); new AccountBaselineStack(app, "account-baseline", { stackName: "seahaven-account-baseline", - env: { account: "328440206208", region: "us-east-1" }, + env: { account: ACCOUNT, region: "us-east-1" }, monthlyBudgetUsd: 1200, budgetAlertEmail: "adam@seahavenind.com", }); +// ── Secondary-region baselines (INFRA-16, INFRA-91) ────────────────────────── +// The us-east-1 baseline above is region-pinned by design. These stacks extend +// a minimal detective/logging footprint into the secondary regions, codifying +// state applied out-of-band this week so it lives in IaC. + +// us-west-2: Bedrock invocation logging only (INFRA-91). Shares the region with +// the offsite backup vault but is an independent concern (separate stack). +new RegionalBaselineStack(app, "regional-baseline-us-west-2", { + stackName: "seahaven-regional-baseline-us-west-2", + env: { account: ACCOUNT, region: "us-west-2" }, + bedrockLogging: true, +}); + +// us-east-2: Bedrock invocation logging (INFRA-91) + the still-missing AWS +// Config recorder and Security Hub (INFRA-16). GuardDuty + flow logs already +// live here (adopted as a follow-up, see lib/regional-baseline-stack.ts). +new RegionalBaselineStack(app, "regional-baseline-us-east-2", { + stackName: "seahaven-regional-baseline-us-east-2", + env: { account: ACCOUNT, region: "us-east-2" }, + bedrockLogging: true, + configRecorder: true, + securityHub: true, +}); + // AWS Backup (audit C-7). Offsite vault (us-west-2) must exist before the // primary plan that copies to it, hence the explicit dependency. const backupOffsite = new BackupOffsiteStack(app, "backup-offsite", { diff --git a/lib/bedrock-logging-regional.ts b/lib/bedrock-logging-regional.ts new file mode 100644 index 0000000..ba0d3e6 --- /dev/null +++ b/lib/bedrock-logging-regional.ts @@ -0,0 +1,71 @@ +import * as cdk from "aws-cdk-lib"; +import * as iam from "aws-cdk-lib/aws-iam"; +import * as logs from "aws-cdk-lib/aws-logs"; +import { Construct } from "constructs"; + +/** + * Regional Bedrock model-invocation logging destination + delivery role + * (INFRA-91). Bedrock invocation logging is account-level *per region*, so + * extending the us-east-1 coverage (lib/bedrock-logging.ts) to the other + * regions where Bedrock is reachable (us-west-2, us-east-2) requires a separate + * regional stack with its own log group + delivery role per region. + * + * This codifies the out-of-band CLI state applied 2026-06 to MATCH exactly so + * the adoption diff is minimal: + * - IAM role seahaven-bedrock-invocation-logging- + * - log group /aws/bedrock/model-invocations (90d) + * - CloudWatch-only delivery (no S3 leg — unlike us-east-1, these regions log + * to CloudWatch only; the large-payload S3 bucket is us-east-1 only). + * + * Like us-east-1, the account-level logging configuration itself has no CFN + * resource type (`PutModelInvocationLoggingConfiguration`); it is applied via + * CLI per region (already live — see README). This construct owns only the + * destinations + role the live config references. + */ +export class BedrockLoggingRegional extends Construct { + 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.logGroup = new logs.LogGroup(this, "InvocationLogGroup", { + logGroupName: "/aws/bedrock/model-invocations", + retention: logs.RetentionDays.THREE_MONTHS, + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); + + // Region-suffixed role name matches the live CLI-created role so CFN can + // adopt it by import rather than creating a colliding new one. + this.deliveryRole = new iam.Role(this, "DeliveryRole", { + roleName: `seahaven-bedrock-invocation-logging-${stack.region}`, + 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, "BedrockLogGroupName", { + value: this.logGroup.logGroupName, + }); + new cdk.CfnOutput(this, "BedrockLoggingRoleArn", { + value: this.deliveryRole.roleArn, + }); + } +} diff --git a/lib/regional-baseline-stack.ts b/lib/regional-baseline-stack.ts new file mode 100644 index 0000000..2ddf055 --- /dev/null +++ b/lib/regional-baseline-stack.ts @@ -0,0 +1,189 @@ +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 securityhub from "aws-cdk-lib/aws-securityhub"; +import { Construct } from "constructs"; +import { BedrockLoggingRegional } from "./bedrock-logging-regional"; + +/** + * Regional security-baseline stack for secondary regions (INFRA-16, INFRA-91). + * + * The account baseline (lib/account-baseline-stack.ts) is us-east-1 only by + * design. This stack extends a minimal detective/logging footprint into the + * other regions where workloads or Bedrock traffic land, WITHOUT duplicating + * the full us-east-1 stack. + * + * Composition is opt-in per region via props so one class serves both + * secondary regions: + * - bedrockLogging → INFRA-91 Bedrock invocation-logging destination + role + * (us-west-2 + us-east-2; codifies live CLI state) + * - configRecorder → INFRA-16 AWS Config recorder role + delivery bucket + * (us-east-2; recorder/channel applied via CLI, see header) + * - securityHub → INFRA-16 Security Hub + FSBP/CIS standards (us-east-2) + * + * GuardDuty and default-VPC flow logs already exist in us-east-2 (applied + * out-of-band) and are intentionally NOT adopted here yet — importing live + * resources of those L1 types risks a replace diff; they are tracked as a + * follow-up so this reconciliation stays additive/non-destructive. + */ +export interface RegionalBaselineStackProps extends cdk.StackProps { + /** INFRA-91: stand up Bedrock invocation-logging destination + delivery role. */ + readonly bedrockLogging?: boolean; + /** INFRA-16: stand up AWS Config recorder role + delivery bucket. */ + readonly configRecorder?: boolean; + /** INFRA-16: enable Security Hub with FSBP + CIS v3.0 standards. */ + readonly securityHub?: boolean; +} + +export class RegionalBaselineStack extends cdk.Stack { + constructor(scope: Construct, id: string, props: RegionalBaselineStackProps) { + super(scope, id, props); + + if (props.bedrockLogging) { + // INFRA-91 — codifies live us-west-2 / us-east-2 Bedrock logging. + new BedrockLoggingRegional(this, "BedrockLogging"); + } + + if (props.configRecorder) { + // INFRA-16 — AWS Config in us-east-2. Same pattern as the us-east-1 + // DetectiveControls construct: the role + delivery bucket live in IaC; + // the recorder + delivery channel are applied via CLI post-deploy because + // the L1 ConfigurationRecorder/DeliveryChannel pair deadlocks CFN (the + // recorder will not reach CREATE_COMPLETE without a channel, and the + // channel cannot be created until the recorder completes — observed in + // us-east-1 2026-06-01). Commands are documented in the README. + const configBucket = new s3.Bucket(this, "ConfigBucket", { + bucketName: `seahaven-config-${this.region}-${this.account}`, + encryption: s3.BucketEncryption.S3_MANAGED, + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + enforceSSL: true, + versioned: true, + lifecycleRules: [ + { + id: "expire-old-config", + expiration: cdk.Duration.days(365), + abortIncompleteMultipartUploadAfter: cdk.Duration.days(7), + }, + ], + removalPolicy: cdk.RemovalPolicy.RETAIN, + }); + + configBucket.addToResourcePolicy( + new iam.PolicyStatement({ + sid: "AWSConfigBucketPermissionsCheck", + effect: iam.Effect.ALLOW, + principals: [new iam.ServicePrincipal("config.amazonaws.com")], + actions: ["s3:GetBucketAcl", "s3:ListBucket"], + resources: [configBucket.bucketArn], + conditions: { + StringEquals: { "aws:SourceAccount": this.account }, + }, + }) + ); + configBucket.addToResourcePolicy( + new iam.PolicyStatement({ + sid: "AWSConfigBucketDelivery", + effect: iam.Effect.ALLOW, + principals: [new iam.ServicePrincipal("config.amazonaws.com")], + actions: ["s3:PutObject"], + resources: [ + configBucket.arnForObjects(`AWSLogs/${this.account}/Config/*`), + ], + conditions: { + StringEquals: { + "s3:x-amz-acl": "bucket-owner-full-control", + "aws:SourceAccount": this.account, + }, + }, + }) + ); + + // Region-suffixed role name so it does not collide with the us-east-1 + // seahaven-config-recorder-role (IAM roles are global by name). + const recorderRole = new iam.Role(this, "ConfigRecorderRole", { + roleName: `seahaven-config-recorder-role-${this.region}`, + assumedBy: new iam.ServicePrincipal("config.amazonaws.com"), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName( + "service-role/AWS_ConfigRole" + ), + ], + }); + recorderRole.addToPolicy( + new iam.PolicyStatement({ + sid: "ConfigDeliveryToBucket", + effect: iam.Effect.ALLOW, + actions: ["s3:PutObject"], + resources: [ + configBucket.arnForObjects(`AWSLogs/${this.account}/Config/*`), + ], + conditions: { + StringEquals: { "s3:x-amz-acl": "bucket-owner-full-control" }, + }, + }) + ); + recorderRole.addToPolicy( + new iam.PolicyStatement({ + sid: "ConfigBucketAcl", + effect: iam.Effect.ALLOW, + actions: ["s3:GetBucketAcl"], + resources: [configBucket.bucketArn], + }) + ); + + new cdk.CfnOutput(this, "ConfigRecorderRoleArn", { + value: recorderRole.roleArn, + }); + new cdk.CfnOutput(this, "ConfigBucketName", { + value: configBucket.bucketName, + }); + } + + if (props.securityHub) { + // INFRA-16 — Security Hub (FSBP + CIS v3.0) in us-east-2. Mirrors the + // us-east-1 DetectiveControls Security Hub block. Findings populate once + // the Config recorder above is recording. + const hub = new securityhub.CfnHub(this, "SecurityHub", { + enableDefaultStandards: false, + controlFindingGenerator: "SECURITY_CONTROL", + autoEnableControls: true, + }); + + const fsbpArn = cdk.Arn.format( + { + service: "securityhub", + region: this.region, + account: "", + resource: "standards", + resourceName: "aws-foundational-security-best-practices/v/1.0.0", + }, + this + ); + const cisArn = cdk.Arn.format( + { + service: "securityhub", + region: this.region, + account: "", + resource: "standards", + resourceName: "cis-aws-foundations-benchmark/v/3.0.0", + }, + this + ); + + const fsbp = new securityhub.CfnStandard(this, "StandardFSBP", { + standardsArn: fsbpArn, + }); + fsbp.node.addDependency(hub); + + const cis = new securityhub.CfnStandard(this, "StandardCIS", { + standardsArn: cisArn, + }); + cis.node.addDependency(hub); + } + + cdk.Tags.of(this).add("Project", "account-baseline"); + cdk.Tags.of(this).add("Owner", "adam@seahavenind.com"); + cdk.Tags.of(this).add("Environment", "prod"); + cdk.Tags.of(this).add("ManagedBy", "cdk"); + } +}