Terraform GCP Cloud KMS rotation_period, destroy_scheduled_duration, prevent_destroy, and a Rust census
Destroy removes the key from state. It does not remove it from the project. It does remove your ability to decrypt.
Destroy removes the key from state. It does not remove it from the project. It does remove your ability to decrypt.
§I. Frame
The last three TF Ops lessons were GCP Artifact Registry on 09-29, AWS CloudWatch Logs on 10-02, and Azure Storage on 10-05. Today returns to GCP with Cloud KMS, which no lesson on SoT owns.
A customer-managed encryption key (CMEK) sits under a Cloud SQL instance, a bucket, or a BigQuery dataset. The data belongs to the service. The ability to read it belongs to the key. Three Terraform arguments decide how that key ages and how it dies: rotation_period, destroy_scheduled_duration, and the prevent_destroy lifecycle flag.
The problem for today: declare a key that rotates on a schedule and cannot be destroyed by a stray plan, then census an existing key ring for keys that never rotate, rotate too slowly, or would vanish too fast.
§II. The key that destroy cannot delete
The provider page opens with a warning. CryptoKeys cannot be deleted from Google Cloud. Destroying a Terraform-managed key removes it from state and deletes all its versions, which renders the key unusable. The key name stays in the project. Any data encrypted under it becomes unrecoverable.
The Tombstone Rule (named technique). For a google_kms_crypto_key, terraform destroy writes a tombstone, not a cleanup. The name stays taken, the versions go to DESTROY_SCHEDULED, and every ciphertext waits on a clock.
That clock is destroy_scheduled_duration. If you omit it at creation time, versions spend 30 days in DESTROY_SCHEDULED before they become DESTROYED. During that window a version can be restored. Shorten it and you shorten the time anyone has to notice.
Brikman puts prevent_destroy = true on the S3 bucket that holds Terraform state (PDF pp.151-152): a plan that would destroy the resource fails instead. The provider doc recommends the same guard for keys, and its own example carries it:
resource "google_kms_crypto_key" "orders" {
name = "orders-cmek"
key_ring = google_kms_key_ring.app.id
purpose = "ENCRYPT_DECRYPT"
rotation_period = var.rotation_period
destroy_scheduled_duration = "2592000s"
lifecycle {
prevent_destroy = true
}
}
prevent_destroy guards Terraform only. It does nothing about a console click or a gcloud kms keys versions destroy. IAM guards those.
§III. The period is a string
rotation_period takes seconds with an s suffix, such as "7776000s" for 90 days. The provider requires it to exceed one day (86400s). The first rotation happens one period after you set it, not at apply.
Rotation creates a new primary version. Two facts from the Cloud KMS rotation guide matter for the census:
- Rotation does not re-encrypt. Old data stays under old versions, so old versions must stay enabled until the data is rewrapped.
- Cloud KMS does not auto-rotate asymmetric keys. A key with purpose
ASYMMETRIC_SIGNorASYMMETRIC_DECRYPTneeds a manual rotation and a public-key redistribution.
Validate the Seconds (named technique). The bundle's kms.tf puts the policy in a variable validation, so a bad value fails at plan before any provider call:
validation {
condition = can(regex("^[0-9]+s$", var.rotation_period)) && tonumber(trimsuffix(var.rotation_period, "s")) > 86400 && tonumber(trimsuffix(var.rotation_period, "s")) <= 7776000
error_message = "rotation_period must look like 7776000s, exceed 86400s, and be at most 90 days (7776000s)."
}
The 90-day ceiling is house policy, not a provider rule. Checked on the box with terraform 1.16.5: init -backend=false resolved hashicorp/google 7.46.1, validate -json returned "valid": true with zero diagnostics, and plan -var rotation_period=3600s returned Error: Invalid value for variable next to the expected no-credentials provider errors.
§IV. The Rust census
The census is a cargo script that reads gcloud kms keys list --format=json from a file or stdin and deserializes each key with serde. rotationPeriod and destroyScheduledDuration arrive as strings like "7776000s", so one helper strips the suffix and parses seconds. Flags:
| Flag | Rule | Attention |
|---|---|---|
no_rotation | ENCRYPT_DECRYPT with no rotationPeriod | yes |
rotation_over_max=Nd | period longer than --max-days (default 90) | yes |
asymmetric_manual | purpose starts with ASYMMETRIC_ | no, note only |
short_destroy_window=Nd | destroyScheduledDuration under 30 days | yes |
primary_not_enabled=S | primary version state is not ENABLED | yes |
The purpose match carries §III:
if purpose.starts_with("ASYMMETRIC_") {
out.push("asymmetric_manual".to_string());
} else if purpose == "ENCRYPT_DECRYPT" {
match k.rotation_period.as_deref() {
None => { out.push("no_rotation".to_string()); attention = true; }
Some(p) if secs(p)? > max_days * DAY => {
out.push(format!("rotation_over_max={}d", secs(p)? / DAY));
attention = true;
}
Some(_) => {}
}
}
Real run, synthetic input. The box ran the script under cargo +nightly -Zscript against the bundle's fixture/keys.json. The fixture is invented in the documented CryptoKey shape (no GCP credentials this fire). The output below is the script's actual output over that fixture:
key orders-cmek ok -
key media-cmek ATTENTION no_rotation
key logs-cmek ATTENTION rotation_over_max=365d,short_destroy_window=1d,primary_not_enabled=DISABLED
key release-signer ok asymmetric_manual
summary keys=4 attention=2 max_days=90
exit 2
§V. How to run
gcloud kms keys list --location us-east1 --keyring app --format=json \
| cargo +nightly -Zscript ./gcp-kms-rotation-census.rs --max-days 90
IAM for a live run is read-only: cloudkms.cryptoKeys.list on the key ring. The script never writes and exits 2 when any key needs attention. Nix wrapper: gcp-kms-rotation-census.nix (writeBashBin), built on the lab Mac this fire; the built binary printed the same four rows.
§VI. Close
The Tombstone Rule: destroy on a KMS key starts a 30-day clock on your ciphertext, so put prevent_destroy on every key Terraform owns. Validate the Seconds: make a bad rotation_period fail at plan. Run the census against one real key ring and write down which keys you expect to flag before you read the output.
Paired Dev drives terraform validate -json and plan -json from Rust integration tests and asserts the rotation variable rejects 3600s. Paired Cert reads the lock file that pinned hashicorp/google 7.46.1 above.
Related
- Tome: Brikman, Terraform: Up and Running 3e, ch3 prevent_destroy, PDF pp.151-152 (grounded-in)
- Prior TF Ops: Azure Storage 10-05 · CloudWatch Logs 10-02 · Artifact Registry 09-29
- Prior Cert: lifecycle meta-arguments and prevent_destroy 09-29
- Web: google_kms_crypto_key · Cloud KMS key rotation · gcloud kms keys list