Prevent Destroy Encryption
Adds prevent_destroy = true to KMS keys and Secrets Manager secrets. Blocks accidental deletion of encryption infrastructure.
When to use this rule
Use this when: Your modules create KMS keys or Secrets Manager secrets used by production workloads. Losing these resources would make encrypted data inaccessible or break applications.
Do not use this when: You are rotating KMS keys as part of a planned key management lifecycle, or deleting test secrets in a non-production environment.
Why this rule exists
Deleting a KMS key is irreversible after the waiting period (7-30 days). Any data encrypted with that key becomes permanently inaccessible: S3 objects, RDS snapshots, EBS volumes, everything. Deleting a Secrets Manager secret can break applications that depend on stored credentials, API keys, or configuration values.
Unlike data resources where backups might help, losing a KMS key means losing access to the data itself. There is no recovery path.
Related Terraform and OpenTofu issues
| Repository | Issue | Title |
|---|---|---|
| hashicorp/terraform | #3116 | Cannot use interpolations in lifecycle attributes |
| hashicorp/terraform | #18367 | Feature request: support prevent_destroy for modules |
Limitations this rule answers
Per-module pages under Workarounds where this rule is the answer, each with the native workaround first and the diff the rule makes:
- KMS: terraform destroy scheduled deletion of a KMS key
- Secrets Manager: terraform destroy deleted a secret other systems depend on
Affected resources
| Resource | Service | Why |
|---|---|---|
aws_kms_key | AWS KMS | Key deletion makes all encrypted data permanently inaccessible |
aws_secretsmanager_secret | AWS Secrets Manager | Secret deletion breaks applications referencing stored credentials |
What this rule does
Adds a lifecycle { prevent_destroy = true } block to each matching resource. Terraform and OpenTofu will refuse to destroy these resources during terraform destroy or resource replacement.
Before and after
Before (upstream module):
resource "aws_kms_key" "this" {
# ... resource configuration ...
tags = var.tags
}After (with Prevent Destroy Encryption applied):
resource "aws_kms_key" "this" {
# ... resource configuration ...
tags = var.tags
lifecycle {
prevent_destroy = true
}
}The only change is the rule transformation. All existing arguments, outputs, and module behavior remain identical.
Real-world scenario
During a Terraform workspace cleanup, terraform destroy deleted a KMS key used to encrypt an RDS production database. The 30-day deletion window passed before anyone noticed. All database backups encrypted with that key became permanently unrecoverable.
The example above is illustrative. To see this rule run for real, open it in the
Rules Playground β it applies lifecycle_prevent_destroy_encryption to a
reviewed upstream module that actually declares the resources this rule targets,
and shows the diff the registry would serve. No account needed.
Known limits
- Does not cover
aws_kms_alias,aws_kms_grant, or other KMS-related resources. - Does not prevent KMS key disabling (only deletion). A disabled key can be re-enabled.
- Does not cover AWS Certificate Manager (ACM) certificates or other encryption-adjacent resources.
- For
prevent_destroy,addmeans ensure this value, not add it if absent. A resource that explicitly declaresprevent_destroy = falseis rewritten totrue; before pofix 0.21 it was left unprotected while the run still reported the rule as applied. A value that is not atrue/falseliteral is left alone and warned about instead of being rewritten. - Only
.tfand.tofufiles are read. Modules written in JSON syntax (.tf.json,.tofu.json) are left untouched. - Only the module's own directory and its first-level
modules/*submodules are processed. Modules that this module calls by source URL are separate downloads and are handled on their own request.
Compliance framework support
This rule is not a compliance control. It supports these framework objectives as an operational safeguard:
| Framework | Controls | Role |
|---|---|---|
| SOC 2 | CC6.1, CC6.7 | Supports cryptographic key protection and data confidentiality |
| PCI DSS 4.0 | 3.6, 3.7 | Supports cryptographic key management lifecycle |
Default configuration
These are the values the rule ships with. They are not settable from a module source URL: ?rules= carries rule names only, so a per-request enable uses exactly the defaults below. Your organization sets them per rule, in its Baseline or in a ruleset, from the Operational Rules page; see Baseline rules and rulesets.
| Parameter | Type | Default | Description |
|---|---|---|---|
resource_types | list(string) | ["aws_kms_key", "aws_secretsmanager_secret"] | Encryption resource types to protect |
The rule's definition
The HCL this rule is written in - its parameters, their shipped defaults, and the transformers it runs - is published at lifecycle_prevent_destroy_encryption, alongside every other selectable rule.
How to enable
On a framework host, per request:
Add ?rules=lifecycle_prevent_destroy_encryption to your HTTPS module source, using the id exactly as written.
module "example" {
source = "https://soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&rules=lifecycle_prevent_destroy_encryption"
}A ?rules= list is either all bare names, which replace your organization's rules for that download, or all - prefixed names, which subtract from them. Mixing the two is refused with a 400 rather than silently treated as a replacement. To add this rule on top of your organization's rules instead, use ?add_rules=lifecycle_prevent_destroy_encryption; a bare ?rules= list cannot be combined with it, because a bare list already replaced the set there was something to add to.
The + add prefix was removed. It reached the registry only when written %2B, and a literal + in a query string decodes to a space everywhere, so ?rules=+lifecycle_prevent_destroy_encryption arrived as a bare name with a leading space β one step from silently replacing your organization's rules. Any +, in either spelling, is now a 400 naming ?add_rules=.
On your organization's host:
Add the rule to the organization's Baseline, which applies to every module served from the bare organization form <alias>.compliance.tf/<namespace>/<name>/<provider>, or to a named ruleset that one module selects with ?ruleset=<name> on the HTTPS form of that address. A project environment (<alias>.compliance.tf/<project>/<env>/<module>) serves the rules frozen in its bound snapshot instead: there the rule goes into the config draft and reaches the environment by promotion. On both organization forms ?rules= and ?add_rules= are refused with a 400 β including the subtractive - prefix β rather than dropped, so you cannot mistake an ignored override for an applied one. Change the configuration, not the URL. See Baseline rules and rulesets for the configuration flow, and registry resolution for how the address forms differ.
Break-glass path
On a framework host, add ?rules=-lifecycle_prevent_destroy_encryption to the module source URL and run terraform init -upgrade to fetch a copy without this rule. The copy in .terraform/modules/ stays until the next upgrade, so remove the override when you are done. On your organization's host the parameter is refused with a 400 rather than ignored. The rule comes from the organization's configuration: a Baseline rule applies to every module served from the bare organization form, so an admin disables it for the duration of the operation, or keeps it out of the Baseline and in a ruleset that only the modules needing protection select. On a project environment the posture is the bound snapshot and its waivers. See Baseline rules and rulesets.
Failure modes
| Scenario | Result |
|---|---|
| KMS key scheduled for deletion needs to be kept | Cancel the deletion schedule via AWS Console or CLI. The prevent_destroy block prevents Terraform from initiating deletion. |
| You need to intentionally delete a KMS key | Use the break-glass path to download the module without this rule, then run terraform destroy. |
Terraform and OpenTofu compatible
This rule works with both Terraform (1.x+) and OpenTofu (1.6+). It reads ordinary HCL, and anything it changes stays ordinary HCL, so the module behaves the same under either tool.
Help us improve this page
Operational Rules are a new feature. We'd love your feedback on this rule page β what's useful, what's missing, what's confusing. Share feedback.