Azure · lesson 18 of 19
Beginner
4Intermediate
13- 5 Azure Virtual Network Basics
- 6 Azure SQL Basics
- 7 Azure Functions Fundamentals
- 8 Bicep and ARM Templates Basics
- 9 Azure Monitor Basics
- 11 Azure Cosmos DB: Items and Partition Keys
- 12 Azure Cosmos DB: Queries
- 13 Azure Cosmos DB: ETags and Patch
- 14 Azure Service Bus: Queues and Settlement
- 15 Azure Service Bus: Dead-Lettering
- 16 Azure Service Bus: Topics and Filters
- 17 Azure Key Vault: Secrets and Versions
- 18 Azure Key Vault: Soft-Delete and Recovery
Also part of Azure
5- + Dead-Letter Queues: What Happens When Retries Keep Failing from Event-Driven Architecture
- + Idempotent Consumers: Surviving a Duplicate Delivery from Event-Driven Architecture
- + Envelope Encryption and Key Rotation Without Re-encrypting Data from Cryptography
- + Deployments and Rollouts from Kubernetes
- + Services and Networking from Kubernetes
Azure Key Vault: Soft-Delete and Recovery
Delete a Key Vault secret against Lowkey Vault: get_secret 404s immediately, but the secret stays recoverable in a deleted-items list until purge_deleted_secret removes it — which purge protection can block even in the emulator.
By SkillByExample · Published
Deleting a Key Vault secret doesn’t delete it. begin_delete_secret moves it into a
soft-deleted state: unreachable through the normal get_secret/set_secret calls, but
still sitting in the vault’s deleted-items list, recoverable by name, until something actually
purges it. This lesson continues against the same Lowkey Vault test double as the previous
one — no official Key Vault emulator exists.
A Deleted Secret 404s Immediately but Isn’t Gone
import time
import warnings
from urllib3.exceptions import InsecureRequestWarning
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
from azure.core.credentials import AccessToken, TokenCredential
from azure.core.exceptions import ResourceNotFoundError
from azure.keyvault.secrets import SecretClient
class StubCredential(TokenCredential):
def get_token(self, *scopes, **kwargs):
return AccessToken("fake-token", int(time.time()) + 3600)
client = SecretClient(
vault_url="https://localhost:8443",
credential=StubCredential(),
verify_challenge_resource=False,
connection_verify=False,
)
client.set_secret("api-token", "sk_live_abc123")
deleted = client.begin_delete_secret("api-token").result()
print("recovery_id set:", deleted.recovery_id is not None)
try:
client.get_secret("api-token")
except ResourceNotFoundError as e:
print("get_secret right after delete ->", type(e).__name__, e.status_code)
seen = client.get_deleted_secret("api-token")
print("get_deleted_secret has no .value attribute:", not hasattr(seen, "value"))
print("deleted_date set:", seen.deleted_date is not None)
print("scheduled_purge_date set:", seen.scheduled_purge_date is not None)
client.begin_recover_deleted_secret("api-token").result()
back = client.get_secret("api-token")
print("get_secret after recover:", back.value)
$ docker run -d --name az-kv-demo2 nagyesta/lowkey-vault:7.4.0
91e187ee594f804cd9e6c579433480af9bcc6ef86e2448c6698620d0d029863f
$ until docker logs az-kv-demo2 2>&1 | grep -q 'Started LowkeyVaultApp'; do sleep 0.3; done
$ docker run --rm --network container:az-kv-demo2 -v "$(pwd)":/app -w /app python:3.14-slim bash -c "pip install -q azure-keyvault-secrets==4.12.0 2>&1 | tail -1 && python kv_delete_recover.py"
[notice] To update, run: pip install --upgrade pip
recovery_id set: True
get_secret right after delete -> ResourceNotFoundError 404
get_deleted_secret has no .value attribute: True
deleted_date set: True
scheduled_purge_date set: True
get_secret after recover: sk_live_abc123
The get_secret call fails the instant after delete — there’s no delay before a soft-deleted
secret stops being readable the normal way. But get_deleted_secret can still see it, and what
it returns has no value attribute at all; the fields it does expose (deleted_date,
scheduled_purge_date, recovery_id) are all about when and whether it can come back, not
what it held. Only begin_recover_deleted_secret restores the readable value, and the recovered
secret keeps the same version history it had before deletion.
Purge Protection Can Block a Purge Even on the Emulator
import time
import warnings
from urllib3.exceptions import InsecureRequestWarning
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
from azure.core.credentials import AccessToken, TokenCredential
from azure.core.exceptions import HttpResponseError
from azure.keyvault.secrets import SecretClient
class StubCredential(TokenCredential):
def get_token(self, *scopes, **kwargs):
return AccessToken("fake-token", int(time.time()) + 3600)
client = SecretClient(
vault_url="https://localhost:8443",
credential=StubCredential(),
verify_challenge_resource=False,
connection_verify=False,
)
client.set_secret("temp-key", "throwaway")
client.begin_delete_secret("temp-key").result()
print("in deleted-items list:", sorted(s.name for s in client.list_deleted_secrets()))
deleted = client.get_deleted_secret("temp-key")
print("recovery_level:", deleted.properties.recovery_level)
try:
client.purge_deleted_secret("temp-key")
print("purge succeeded")
except HttpResponseError as e:
print("purge_deleted_secret ->", type(e).__name__, e.status_code)
$ docker run --rm --network container:az-kv-demo2 -v "$(pwd)":/app -w /app python:3.14-slim bash -c "pip install -q azure-keyvault-secrets==4.12.0 2>&1 | tail -1 && python kv_purge.py"
[notice] To update, run: pip install --upgrade pip
in deleted-items list: ['temp-key']
recovery_level: DeletionRecoveryLevel.RECOVERABLE
purge_deleted_secret -> HttpResponseError 500
list_deleted_secrets is the “recycle bin” view — every soft-deleted secret the vault is still
holding, temp-key included, regardless of whether anyone intends to recover it.
recovery_level names the reason the purge below fails before the purge is even attempted:
RECOVERABLE is the vault’s own statement that this secret can only be recovered, not purged,
until purge protection is lifted (which, on a real vault, it never can be once enabled) or the
retention period’s automatic purge takes over. Lowkey Vault refuses the purge for the same
reason a real vault would, though not with the same status code — a real vault with purge
protection on returns 403 Forbidden, while Lowkey Vault returns 500, and the SDK’s own
retry logic resends the request a few times before giving up and raising, which is why the
emulator’s logs show more than one purge attempt for this single call. On a real vault without
purge protection, the same call does remove the secret permanently, and on any vault — protected
or not — the retention period’s scheduled automatic purge eventually removes every soft-deleted
secret nobody recovered.
Practice
1. Delete a secret, recover it, then delete it again, and confirm it's still recoverable the second time.
import time
import warnings
from urllib3.exceptions import InsecureRequestWarning
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
from azure.core.credentials import AccessToken, TokenCredential
from azure.keyvault.secrets import SecretClient
class StubCredential(TokenCredential):
def get_token(self, *scopes, **kwargs):
return AccessToken("fake-token", int(time.time()) + 3600)
client = SecretClient(
vault_url="https://localhost:8443",
credential=StubCredential(),
verify_challenge_resource=False,
connection_verify=False,
)
client.set_secret("round-trip", "v1")
client.begin_delete_secret("round-trip").result()
client.begin_recover_deleted_secret("round-trip").result()
client.begin_delete_secret("round-trip").result()
seen = client.get_deleted_secret("round-trip")
print("still recoverable after a second delete:", seen.recovery_id is not None)
$ docker run --rm --network container:az-kv-demo2 -v "$(pwd)":/app -w /app python:3.14-slim bash -c "pip install -q azure-keyvault-secrets==4.12.0 2>&1 | tail -1 && python kv_delete_twice.py"
[notice] To update, run: pip install --upgrade pip
still recoverable after a second delete: True
Soft-delete isn’t a one-time safety net that’s used up after the first recovery — the same secret name can go through delete-and-recover as many times as needed.
2. Try to recover a secret name that was never deleted and see what exception that raises.
import time
import warnings
from urllib3.exceptions import InsecureRequestWarning
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
from azure.core.credentials import AccessToken, TokenCredential
from azure.core.exceptions import ResourceNotFoundError
from azure.keyvault.secrets import SecretClient
class StubCredential(TokenCredential):
def get_token(self, *scopes, **kwargs):
return AccessToken("fake-token", int(time.time()) + 3600)
client = SecretClient(
vault_url="https://localhost:8443",
credential=StubCredential(),
verify_challenge_resource=False,
connection_verify=False,
)
try:
client.begin_recover_deleted_secret("never-deleted").result()
except ResourceNotFoundError as e:
print("begin_recover_deleted_secret on a name that was never created ->", type(e).__name__, e.status_code)
$ docker run --rm --network container:az-kv-demo2 -v "$(pwd)":/app -w /app python:3.14-slim bash -c "pip install -q azure-keyvault-secrets==4.12.0 2>&1 | tail -1 && python kv_recover_nonexistent.py"
[notice] To update, run: pip install --upgrade pip
begin_recover_deleted_secret on a name that was never created -> ResourceNotFoundError 404
The same 404 shape as every other “no such soft-deleted item” case in this lesson — recovery is only ever valid against something that’s actually in the deleted-items list.
3. List every deleted secret currently in the vault and confirm a brand-new, never-deleted secret doesn't appear in it.
import time
import warnings
from urllib3.exceptions import InsecureRequestWarning
warnings.filterwarnings("ignore", category=InsecureRequestWarning)
from azure.core.credentials import AccessToken, TokenCredential
from azure.keyvault.secrets import SecretClient
class StubCredential(TokenCredential):
def get_token(self, *scopes, **kwargs):
return AccessToken("fake-token", int(time.time()) + 3600)
client = SecretClient(
vault_url="https://localhost:8443",
credential=StubCredential(),
verify_challenge_resource=False,
connection_verify=False,
)
client.set_secret("still-live", "untouched")
deleted_names = sorted(s.name for s in client.list_deleted_secrets())
print("deleted secrets:", deleted_names)
print("still-live is deleted:", "still-live" in deleted_names)
$ docker run --rm --network container:az-kv-demo2 -v "$(pwd)":/app -w /app python:3.14-slim bash -c "pip install -q azure-keyvault-secrets==4.12.0 2>&1 | tail -1 && python kv_list_deleted.py"
[notice] To update, run: pip install --upgrade pip
deleted secrets: ['round-trip', 'temp-key']
still-live is deleted: False
round-trip and temp-key from earlier in this lesson are still in the deleted-items list
(temp-key’s purge was refused); a brand new, never-deleted secret like still-live never
appears there.
That’s the data-plane half of Key Vault this track covers on a local emulator. Key Vault’s cryptographic key operations, RBAC, and managed-identity authentication all need either a real Entra tenant or hit quirks specific to the Lowkey test double — see key management and rotation for the general pattern Key Vault’s key operations implement.
Cleanup
$ docker rm -f az-kv-demo2
az-kv-demo2
Next: the Cosmos DB change feed — reading every modification to a container as an ordered, resumable stream.