import * as cdk from "aws-cdk-lib"; import * as kms from "aws-cdk-lib/aws-kms"; import * as iam from "aws-cdk-lib/aws-iam"; import * as backup from "aws-cdk-lib/aws-backup"; import { Construct } from "constructs"; /** * Offsite AWS Backup vault for Sea Haven (account 328440206208), in us-west-2. * * This is the Copy3 / offsite leg of the 3-2-1 strategy and the only immutable * recovery path in the account. The primary plan (see backup-stack.ts, us-east-1) * copies recovery points here cross-region. Closes audit finding C-7 together * with backup-stack. * * Vault Lock is GOVERNANCE mode for now (minRetention only, no `changeableFor`): * recovery points cannot be silently deleted, but a principal with explicit * `backup:DeleteRecoveryPoint` / `backup:DeleteBackupVaultLockConfiguration` * permission can still intervene while we validate the plan. Graduate to * COMPLIANCE mode later by adding `changeableFor` (irreversible after the * cooling-off window) — a one-line change + redeploy. */ export class BackupOffsiteStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // CMK encrypting offsite recovery points (rotation on; RETAIN so a stack // teardown never strands/destroys the only immutable copy). const vaultKey = new kms.Key(this, "OffsiteVaultKey", { alias: "backup-offsite-vault", description: "Encrypts offsite AWS Backup recovery points (us-west-2)", enableKeyRotation: true, removalPolicy: cdk.RemovalPolicy.RETAIN, }); // The L2 BackupVault does NOT grant the backup service use of a customer // CMK — without this, cross-region COPY jobs of encrypted RDS/EBS recovery // points fail (and silently, with no CloudTrail). Grant backup.amazonaws.com // the minimum KMS actions on this destination key, incl. CreateGrant. vaultKey.addToResourcePolicy( new iam.PolicyStatement({ sid: "AllowAwsBackupUseOfTheKey", principals: [new iam.ServicePrincipal("backup.amazonaws.com")], // Action set matches AWS's documented Backup vault-key policy; scoped // to this account so only this account's Backup service can use it. actions: [ "kms:Decrypt", "kms:GenerateDataKey", "kms:GenerateDataKeyWithoutPlaintext", "kms:ReEncrypt*", "kms:DescribeKey", ], resources: ["*"], conditions: { StringEquals: { "aws:SourceAccount": this.account } }, }) ); vaultKey.addToResourcePolicy( new iam.PolicyStatement({ sid: "AllowAwsBackupCreateGrant", principals: [new iam.ServicePrincipal("backup.amazonaws.com")], actions: ["kms:CreateGrant"], resources: ["*"], conditions: { Bool: { "kms:GrantIsForAWSResource": "true" }, StringEquals: { "aws:SourceAccount": this.account }, }, }) ); new backup.BackupVault(this, "OffsiteVault", { backupVaultName: "seahaven-offsite", encryptionKey: vaultKey, removalPolicy: cdk.RemovalPolicy.RETAIN, // Governance-mode Vault Lock: no `changeableFor`, so it stays adjustable. lockConfiguration: { minRetention: cdk.Duration.days(30), }, }); 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"); new cdk.CfnOutput(this, "OffsiteVaultName", { value: "seahaven-offsite" }); new cdk.CfnOutput(this, "OffsiteVaultKmsKeyArn", { value: vaultKey.keyArn }); } }