ADR 0031: Purge protection on, Microsoft-managed keys, and no private endpoints¶
Status: Accepted Decision date: 2026-09-14 Owners: Workload owner
Context¶
Qlty's security view on main listed 19 open findings from checkov and trivy.
Seventeen were Terraform rows that
scanner triage had parked under Needs owner
decision because each costs money, cannot be undone, or both. The owner's
direction was to clear them with real changes where a real change exists, and
to record a decision wherever the answer is no, so no finding stays open
without one.
The owner decided each group on 2026-09-14:
| Group | Findings | Decision |
|---|---|---|
| Key Vault purge protection | checkov CKV_AZURE_110, CKV_AZURE_42, trivy AZU-0016 (skipped until now under ADR 0021) | On |
| Customer-managed keys | checkov CKV2_AZURE_1 (x2), CKV_AZURE_100 | No. Keep Microsoft-managed keys |
| Private endpoints | checkov CKV2_AZURE_33, CKV2_AZURE_32 | No |
| Public network access disabled | checkov CKV_AZURE_59, CKV_AZURE_101 | Turn it off where the existing service-endpoint design allows |
| Storage infrastructure encryption | trivy AZU-0061 (x2) | No. Keep the current accounts |
| Function host storage geo-redundancy | checkov CKV_AZURE_206, trivy AZU-0058 | Yes |
| Storage resource logs | checkov CKV_AZURE_33 (x2), CKV2_AZURE_21, trivy AZU-0057 | Yes |
| Flex zone redundancy and always-ready instances | checkov CKV_AZURE_212, CKV_AZURE_225 | No |
Purpose and decision drivers¶
- Recoverability of the secrets. Purge protection closes the gap ADR 0021 accepted. With it off, a principal that can delete the vault can also purge it. The estate has not moved region since 2026-08-19, which was ADR 0021's first revisit trigger.
- Cost against a USD 150/month budget. Private endpoints, private DNS and always-ready instances are standing charges for a single-region, single-environment site.
- No one-way changes without a need. CMK on an existing Cosmos account is irreversible, and losing the key locks the data. Infrastructure encryption can be set only when an account is created.
- Alerts must keep working. The Log Analytics workspace caps ingestion at 0.25 GB/day, and a tripped cap stops the log alert rules.
Decision¶
- Key Vault purge protection is on.
var.purge_protection_enableddefaults totrue. This supersedes ADR 0021. The inline skips for CKV_AZURE_110, CKV_AZURE_42 and AZU-0016 are removed, because those checks now pass. - Encryption at rest stays on Microsoft-managed keys for both storage accounts and Cosmos. CKV2_AZURE_1 and CKV_AZURE_100 are skipped inline, citing this record.
- No private endpoints. The service firewalls stay:
- Key Vault
network_acls, Cosmosvirtual_network_rule, and storagenetwork_rulesall default to Deny; - each admits the Functions integration subnet through its service
endpoint (
infra/network.tf).
CKV2_AZURE_33 (content storage) and CKV2_AZURE_32 are skipped inline.
4. Public network access stays enabled at the resource level, because
Disabled would also refuse the service-endpoint traffic the app uses.
On Storage, Key Vault and Cosmos, publicNetworkAccess = Disabled allows
private endpoint traffic only. A subnet rule reaches the service on its
public endpoint and is refused along with everything else. Without private
endpoints (decision 3), turning it off takes the site down.
- "Where possible" is therefore already the live state: every data service
is set to selected networks, and every operator IP list is empty in
steady state.
- CKV_AZURE_59 (content storage) and CKV_AZURE_101 are skipped inline.
5. Infrastructure encryption is not added. Both storage accounts are kept.
AZU-0061 is ignored inline on each.
6. The Function host storage account moves from LRS to GRS. This is an
in-place settings update. Timer schedule status, singleton and listener
leases, and in-flight queue messages survive a regional loss. The cost is
cents a month on an account holding a few MB.
7. Storage resource logs go to Log Analytics through Azure Monitor
diagnostic settings (infra/observability.tf):
- content storage queues: read, write and delete;
- host storage queues: read, write and delete;
- host storage blobs: write and delete only.
The checks cannot see a diagnostic setting:
- CKV_AZURE_33 and trivy AZU-0057 read only classic queue_properties.logging.
Terraform writes that through the storage data plane, which the firewall
closes to Terraform Cloud's runners.
- CKV2_AZURE_21 passes only with azurerm_log_analytics_storage_insights,
which reads classic logs with an account key, and the keys are disabled.
These are skipped inline, each naming the diagnostic setting that does the job. 8. Host storage blob reads are not logged. This was measured on 2026-09-14 (Transactions by ApiName, one day): - reads: about 573,000 a day, all the host's own OAuth traffic (GetBlob 409,304, GetBlobProperties 139,323, ListContainers 24,966); - writes and deletes: about 20,000 a day.
At the workspace's 1.08 KB average row, reads alone come to about
0.6 GB/day. The workspace ingested 0.107 GB/day that week against its 0.25 GB
cap, so logging reads would trip the cap every day and silence
function_http_5xx and function_response_time. The CKV2_AZURE_21 skip on
function_releases records this.
9. No zone redundancy and no always-ready instances on Flex Consumption.
FC1 has no worker_count. Zone redundancy there forces at least two
always-ready instances, about USD 40/month. CKV_AZURE_212 and CKV_AZURE_225
are skipped inline.
Consequences and accepted risks¶
- Purge protection is permanent. Azure refuses to turn it off. A deleted
vault stays soft-deleted for 90 days: it can't be purged, and its name stays
reserved. A future rebuild must recover the vault, not recreate it. Setting
the variable back to
falseproduces a failed apply. - Microsoft holds the encryption keys. Data is still encrypted at rest with 256-bit AES. What is given up is customer control of key rotation and revocation.
- Data services keep public endpoints behind firewalls. A firewall misconfiguration, not a missing DNS record, is the failure mode to watch. The subnet rules and empty operator IP lists are the control.
- Single-layer encryption at rest on both storage accounts.
- Host storage reads leave no per-request log. Writes, deletes, leases and container changes do. Transaction metrics by ApiName still count the reads.
- Single-zone Function App. A zone outage in centralus is an outage of the API until Azure restores the zone.
- Log ingestion rises by about 32 MB/day, to roughly 56% of the cap. That
is under the 80%
logs_daily_capalert. Confirm after apply with the daily usage query in that rule.
Alternatives considered¶
- Private endpoints for Storage, Key Vault and Cosmos. Rejected by the owner on standing cost and DNS ordering. They would make decision 4 possible.
- CMK with purge protection. Rejected. The owner took the purge protection half on its own, since CMK needs it anyway, and declined the key-loss failure mode.
- New storage accounts with infrastructure encryption, then migrate.
Rejected for the migration risk on accounts guarded by
prevent_destroy. - Host blob reads to a storage-account archive rather than Log Analytics. Deferred. It avoids the cap, but it was not verified that Azure Monitor can write diagnostic logs to an account with shared-key access disabled. It also adds a third storage account with its own scanner findings.
- Raising the Log Analytics cap to fit host blob reads. Rejected. About 0.6 GB/day more ingestion would cost more than the always-ready instances declined above, to log the host reading its own state.
Validation and revisit triggers¶
- Validation:
az keyvault show -n kv-site-prod-cus-01 -o jsonshowsproperties.enablePurgeProtectionastrue.az storage account show -n stsitefuncprodcus01 -g rg-web-site-prod-cus -o jsonshowssku.nameasStandard_GRS.az monitor diagnostic-settings liston each storage account'squeueServices/default, and on the host account'sblobServices/default, lists the settings above.- checkov and trivy report no failures on
infra/, and Qlty's security view shows none of the findings in the table above. - Revisit when:
- private endpoints become affordable or required (that also reopens decision 4);
- a compliance obligation names CMK or infrastructure encryption;
- the log cap is raised for another reason, which reopens host blob reads;
- the API gains an availability target a single zone cannot meet.
Related decisions and references¶
- Supersedes ADR 0021
- ADR 0008: service firewalls instead of Private Link
- ADR 0025: the Cosmos firewall
- Security scanner triage
infra/storage.tf,infra/keyvault.tf,infra/cosmos.tf,infra/functionapp.tf,infra/observability.tf,infra/variables.tf