Skip to content

ADR 0021: Key Vault purge protection stays disabled

Status: Accepted Decision date: 2026-08-24 Owners: Workload owner

Context

kv-site-prod-cus-01 is RBAC-authorised, holds 18 live secrets, and carries soft_delete_retention_days = 90 with purge_protection_enabled = false. The Go-Live readiness review of 2026-08-24 raised that as blocker B2.

This needs a decision record rather than a status line for a specific reason. ADR 0018 listed "Key Vault purge protection defaulting off" in its remediation debt table — the half of that ADR that is explicitly not ratified — and closed with "the debt table is a commitment: items there may not be silently re-classified as accepted posture." Deciding to live with it is exactly such a re-classification, so it gets a numbered record.

The Deployment Runbook §6 and infra/README.md both carried the instruction that purge protection "must be true before production secrets are written". The secrets were written. The instruction had become a standing contradiction that every reviewer re-raised.

Purpose and decision drivers

  • It is a one-way switch. Once enabled, purge protection cannot be disabled. A soft-deleted vault can no longer be purged, and its name stays reserved for the full retention period — 90 days here.
  • This estate rebuilds itself. The whole platform moved region on 2026-08-19, which required lifting prevent_destroy on every stateful resource. A single-environment estate with no staging depends on teardown-and-recreate being available, and purge protection removes it for the one resource whose name is global.
  • Vault names are global and unrecoverable. kv-site-prod-scus is held by an unrelated Azure customer and is not soft-deleted in this tenant, so it cannot be reclaimed (Naming-Convention). Reserving this vault's own name for 90 days after any delete is a concrete cost, not a theoretical one.
  • The exposure is not "unprotected during setup". All 18 secrets are seeded and resolving; there is no window here that closes on its own.

Decision

var.purge_protection_enabled keeps its default of false and the vault stays without purge protection.

This supersedes ADR 0018's remediation-debt row for purge protection, and retires the "must be true before production secrets are written" instruction in the Deployment Runbook §6 and infra/README.md. The owner's authorisation and the compensating control are recorded in TODO.md under Accepted risks; infra/variables.tf and infra/README.md state it in the same terms beside the code.

Consequences and accepted risks

  • Compensating control: soft delete at 90 days. An accidental or automated delete is still fully recoverable, which is the failure this platform is actually likely to have.
  • What is given up: protection against a deliberate purge by a principal that already holds the rights to perform one. The purge action is carried by subscription-level Contributor and Owner, so the set of principals that could purge the vault is the same set that could delete it. Purge protection would harden against an actor who has already reached that level of access.
  • Secret loss remains recoverable by re-seeding: every value in the vault is owner-supplied and external (provider API keys, integration credentials). Nothing in the vault is generated by the platform and irreproducible.
  • A future compliance baseline or Azure Policy assignment that requires purge protection will fail this vault. That is a known, visible failure rather than a surprise, and this record is the exemption to present.

Alternatives considered

  • Enable it now — rejected for the driver above: it is one-way, and this estate was rebuilt from scratch six days before the decision.
  • Enable it and accept the name reservation — rejected while the estate's shape is still moving. It becomes the right answer later; see revisit triggers.
  • Split the secrets into a second, purge-protected vault — rejected: two vaults to keep consistent, the Function App's Key Vault references split across both, and the same one-way trap on whichever one holds anything that matters.
  • Leave it as unratified debt — rejected. That is what produced a runbook instruction contradicting the live estate for weeks and a blocker re-raised at every review.

Validation and revisit triggers

  • Validated by az keyvault show -n kv-site-prod-cus-01 --query properties.enableSoftDelete and --query properties.softDeleteRetentionInDays returning true and 90, and properties.enablePurgeProtection returning null or false — the state this ADR describes.
  • Revisit when: the platform has gone a full quarter with no region move or full rebuild; ALZ absorption brings a policy that audits or denies vaults without purge protection (that negotiation gets its own ADR per ADR 0018); a compliance obligation names the control; or the vault starts holding a secret that cannot be re-seeded from its owner.
  • Supersedes the purge-protection row of ADR 0018's remediation-debt table
  • TODO.mdAccepted risks, where the owner authorisation lives
  • Deployment Runbook §6 · infra/variables.tf · infra/README.md