diff --git a/lib/backup-stack.ts b/lib/backup-stack.ts index b8b0d53..f53d089 100644 --- a/lib/backup-stack.ts +++ b/lib/backup-stack.ts @@ -87,10 +87,13 @@ export class BackupStack extends cdk.Stack { // is written to MATCH the live lock exactly, so the deploy diff is a no-op // adoption — it does not change the live vault. // - // The vault carries a scoped access policy (below) denying manual recovery- - // point deletion to everyone except a break-glass principal and the Backup - // service role, so lifecycle expiry still works but humans cannot prune - // recovery points by hand. + // NOTE: the scoped vault access policy that previously lived here was DROPPED + // from this deploy (INFRA-94). Attaching a BackupVaultAccessPolicy that + // denies DeleteBackupVault / PutBackupVaultLockConfiguration to all but a + // break-glass principal made the vault unmanageable by CloudFormation/CDK. + // The deny-manual-deletion control is tracked separately in INFRA-94 and + // will be reintroduced via a safe mechanism. Governance lock codify + + // backup selections land here. const primaryVault = new backup.BackupVault(this, "PrimaryVault", { backupVaultName: "seahaven-primary", encryptionKey: vaultKey, @@ -102,49 +105,6 @@ export class BackupStack extends cdk.Stack { }, }); - // Break-glass principal exempt from the deny below. The SSO - // AdministratorAccess permission-set role is the only path to assume an - // admin identity in this account; deletions still require its explicit use - // (and are CloudTrail-audited). The SSO role name carries a hash suffix, so - // a wildcard is required to match it. - const breakGlassRoleArnPattern = `arn:${this.partition}:iam::${this.account}:role/aws-reserved/sso.amazonaws.com/AWSReservedSSO_AdministratorAccess_*`; - const backupServiceRoleArn = `arn:${this.partition}:iam::${this.account}:role/seahaven-backup-service-role`; - - // Scoped vault access policy (INFRA-89): deny MANUAL deletion of recovery - // points / lifecycle tampering to all principals EXCEPT the break-glass SSO - // admin and the AWS Backup service role. - // - // IMPORTANT (cross-review BLOCK, GPT-4.1): `NotPrincipal` does NOT support - // wildcard matching — a wildcarded ARN there is treated literally and would - // silently FAIL to exempt the hash-suffixed SSO role, trapping the vault. - // So we use the robust pattern instead: Effect DENY with Principal `*` and a - // `StringNotLike` condition on `aws:PrincipalArn` (which DOES support - // wildcards). Lifecycle expiry is performed by the AWS Backup service itself - // (not a caller subject to this policy), so automatic expiry is unaffected; - // `UpdateRecoveryPointLifecycle` is denied to humans to prevent retention - // tampering, while the two exempted principals retain full control. - primaryVault.addToAccessPolicy( - new iam.PolicyStatement({ - sid: "DenyManualRecoveryPointDeletion", - effect: iam.Effect.DENY, - principals: [new iam.AnyPrincipal()], - actions: [ - "backup:DeleteRecoveryPoint", - "backup:UpdateRecoveryPointLifecycle", - "backup:DeleteBackupVault", - "backup:DeleteBackupVaultAccessPolicy", - "backup:DeleteBackupVaultLockConfiguration", - "backup:PutBackupVaultLockConfiguration", - ], - resources: ["*"], - conditions: { - StringNotLike: { - "aws:PrincipalArn": [breakGlassRoleArnPattern, backupServiceRoleArn], - }, - }, - }) - ); - // Cross-region copy destination, referenced by literal ARN (the offsite // stack is in another region; a literal ARN avoids crossRegionReferences / // SSM exports). Stack ordering is enforced via addDependency in bin/app.ts.