Skip to main content

Retained / Deletion-Protected Resources

CloudForge deliberately protects certain resources from accidental deletion depending on security profile and compliance framework. A plain DeleteStack (or cdk destroy) will not remove them — CloudFormation either leaves the stack in DELETE_FAILED (deletion-protected resources) or the resource just outlives the stack (RemovalPolicy.RETAIN). This is correct, intentional behavior for real AWS environments; for LocalStack testing/cleanup it means every one of these needs an explicit extra step before a stack can be fully torn down.

All commands below target LocalStack (--endpoint-url=http://localhost:4566). Drop that flag for real AWS (and add proper --region/credentials).

RDS DB Instance — DeletionProtection

Where: RdsFactory.java (.deletionProtection(...)), condition in {Dev,Staging,Production}SecurityProfileConfiguration#isRdsDeletionProtectionEnabled(). When: DEV — never. STAGING/PRODUCTION — whenever any selected compliance framework marks DELETION_PROTECTION as a required control (ComplianceMatrix.isControlRequired). This fires under both ADVISORY and ENFORCE compliance mode — mode only ever excludes DISABLED, it does not distinguish ADVISORY from ENFORCE for this check. ENFORCE's real effect is elsewhere: it's what makes cfn-guard (L3) block the deploy outright when a framework's rules are violated (e.g. HIPAA/PRODUCTION fails to deploy at all under ENFORCE) — a separate mechanism from which resources get retained. Remove:

aws --endpoint-url=http://localhost:4566 rds modify-db-instance \
--db-instance-identifier <db-id> --no-deletion-protection --region us-east-1
aws --endpoint-url=http://localhost:4566 rds delete-db-instance \
--db-instance-identifier <db-id> --skip-final-snapshot --region us-east-1

Caveat: if AWS Config auto-remediation is active for this stack (see "SSM auto-remediation" below), it can silently re-enable deletion protection shortly after you disable it. Delete the Config remediation rule first, or delete the DB instance immediately after disabling protection.

ALB — deletionProtection

Where: AlbFactory.java#shouldEnableDeletionProtection(). When: PRODUCTION only (unconditional — not compliance-framework-gated). Remove:

aws --endpoint-url=http://localhost:4566 elbv2 modify-load-balancer-attributes \
--load-balancer-arn <alb-arn> \
--attributes Key=deletion_protection.enabled,Value=false --region us-east-1

S3 Buckets — RemovalPolicy.RETAIN

Several buckets retain on PRODUCTION; none of these are deletion-protected (no API call blocks deleting them), they just outlive the stack and need manual cleanup + delete:

BucketWhereCondition
ALB access-log bucketAlbFactory.javaPRODUCTION
CMS media storage bucketCmsMediaStorageConfiguration.java#determineRemovalPolicyPRODUCTION
Compliance/audit bucket (e.g. CloudTrail)ComplianceFactory.java ~line 4289security == PRODUCTION || enableObjectLock (also disables autoDeleteObjects, separate from this)

Remove:

aws --endpoint-url=http://localhost:4566 s3 rm s3://<bucket-name> --recursive --region us-east-1
aws --endpoint-url=http://localhost:4566 s3api delete-bucket --bucket <bucket-name> --region us-east-1

Object Lock buckets (enableObjectLock=true) may refuse deletion until retained objects' retain-until dates pass — not overridable, by design.

EFS FileSystem — RemovalPolicy.RETAIN

Where: EfsFactory.java#createFileSystem. When: only when the deployment context explicitly sets retainStorage: true — not profile-driven, purely a user opt-in. Remove:

aws --endpoint-url=http://localhost:4566 efs delete-file-system \
--file-system-id <fs-id> --region us-east-1

(Delete any mount targets/access points on it first if the API complains about dependents.)

AWS Backup Vault — RemovalPolicy.RETAIN

Where: BackupFactory.java#createBackupVault, condition in {Dev,Staging,Production}SecurityProfileConfiguration#isBackupVaultRetentionEnabled(). When: DEV — never. STAGING/PRODUCTION — same compliance-matrix-driven pattern as RDS deletion protection above (any framework requiring the control, ADVISORY or ENFORCE alike). Remove:

# Delete all recovery points in the vault first -- AWS refuses to delete a non-empty vault.
aws --endpoint-url=http://localhost:4566 backup list-recovery-points-by-backup-vault \
--backup-vault-name <vault-name> --region us-east-1
aws --endpoint-url=http://localhost:4566 backup delete-recovery-point \
--backup-vault-name <vault-name> --recovery-point-arn <arn> --region us-east-1
aws --endpoint-url=http://localhost:4566 backup delete-backup-vault \
--backup-vault-name <vault-name> --region us-east-1

Route53 Hosted Zone — RemovalPolicy.RETAIN

Where: DomainFactory.java. When: PRODUCTION only. Remove:

# Delete all non-NS/SOA record sets first.
aws --endpoint-url=http://localhost:4566 route53 list-resource-record-sets --hosted-zone-id <zone-id>
aws --endpoint-url=http://localhost:4566 route53 delete-hosted-zone --id <zone-id>

CloudWatch Log Groups — RemovalPolicy.RETAIN

Where: LoggingCwFactory.java, ProductionSecurityProfileConfiguration.java, StagingSecurityProfileConfiguration.java. When: STAGING and PRODUCTION both (DEV does not retain). Remove:

aws --endpoint-url=http://localhost:4566 logs delete-log-group --log-group-name <name> --region us-east-1

Cognito User Pool (+ SAML identity provider) — RemovalPolicy.RETAIN

Where: CognitoAuthenticationFactory.java, CognitoSamlFactory.java. When: PRODUCTION only. Remove:

aws --endpoint-url=http://localhost:4566 cognito-idp delete-user-pool \
--user-pool-id <pool-id> --region us-east-1

AWS Config infrastructure (Recorder, Delivery Channel, IAM Role) — RemovalPolicy.RETAIN

Where: ComplianceFactory.java (~lines 884/899/909). When: always, unconditionally — these are account-level singletons (only one recorder per region per account is allowed), retained deliberately so a second stack in the same region doesn't try to recreate them (see createConfigInfrastructure / awsConfigEnabled split documented in that class). Remove:

aws --endpoint-url=http://localhost:4566 configservice stop-configuration-recorder \
--configuration-recorder-name cloudforge-config-recorder --region us-east-1
aws --endpoint-url=http://localhost:4566 configservice delete-configuration-recorder \
--configuration-recorder-name cloudforge-config-recorder --region us-east-1
aws --endpoint-url=http://localhost:4566 configservice delete-delivery-channel \
--delivery-channel-name cloudforge-config-delivery-channel --region us-east-1
# IAM role deletion needs its attached policies detached first.
aws --endpoint-url=http://localhost:4566 iam delete-role --role-name <config-role-name>

AWS Config auto-remediation — all 9 actions

ComplianceFactory.java wires 9 CfnRemediationConfigurations as AWS Config auto-remediation actions (automatic(true), 3-5 retries). Each one watches a specific Config rule and, if AWS Config finds a non-compliant resource, runs an SSM Automation document to fix it automatically — independent of anything CloudFormation itself does. Manually undoing any of these by hand (like modify-db-instance --no-deletion-protection above) can get silently reverted a few minutes later if the remediation is still active. Two use AWS-managed SSM documents directly; the other seven use a custom document authored in this codebase.

RemediationSSM DocumentKindTargetsLine
Set IAM account password policyAWSConfigRemediation-SetIAMPasswordPolicyAWS-managedAccount found without the required password policy~1264
Enable S3 bucket versioningAWS-ConfigureS3BucketVersioningAWS-managedBucket found with versioning disabled~1332
Fix CloudTrail bucket policycloudTrailFixDocumentcustomCloudTrail S3 bucket with an incorrect/insecure policy~1526
Enable RDS deletion protectionrdsDeletionProtectionDocumentcustomRDS instance found without DeletionProtection~1642
Enable RDS auto minor-version upgraderdsAutoUpgradeDocumentcustomRDS instance found without AutoMinorVersionUpgrade~1753
Enable Security HubsecurityHubDocumentcustomAccount found with Security Hub disabled~4675
Enable InspectorinspectorDocumentcustomAccount found with Inspector disabled~4749
Enable MaciemacieDocumentcustomAccount found with Macie disabled~4822
Enable GuardDutyguardDutyDocumentcustomAccount found with GuardDuty disabled~4936

The IAM password policy, S3 versioning, RDS, and CloudTrail-bucket ones are stack-scoped (only relevant to that one deployment's resources). The Security Hub / Inspector / Macie / GuardDuty ones are account-level service toggles — remediation re-enables the service for the whole account/region, not just this stack, which matters if you're trying to disable these services broadly on a shared test account rather than clean up one specific deployment.

Which of the 9 actually apply to a given configuration depends on which Config rules that config deploys (framework/profile-driven, same as everything else in this doc) — see the "Remediation" tab of the 🚀 LocalStack button on any row of the compliance dashboard for the real, per-configuration list.

Remove a remediation configuration (stops it from re-applying, does not undo what it already changed):

aws --endpoint-url=http://localhost:4566 configservice delete-remediation-configuration \
--config-rule-name <rule-name> --region us-east-1

Find <rule-name> from the stack's AWS::Config::ConfigRule resources (aws configservice describe-config-rules --region us-east-1).

The pragmatic alternative: don't clean up individual resources at all

For LocalStack testing specifically, none of the above is actually necessary — LocalStack has no real persistence, so restarting the container is a guaranteed clean slate regardless of what any individual stack retained. deploy-localstack-compliance-matrix.sh restarts LocalStack between every config for exactly this reason. Only use the commands in this file when you need to clean up a specific retained resource without a full reset (e.g. investigating one config's real output), or when working against real AWS where a full reset isn't an option and these commands are what you'd actually run in production too.