Azure · lesson 17 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: Secrets and Versions
Store and read Key Vault secrets with the Python SDK against the Lowkey Vault emulator: every set_secret call creates a new version rather than overwriting, get_secret without a version returns the newest one, and a stale version id raises a 404.
By SkillByExample · Published
A Key Vault secret name is really a chain of versions: set_secret on an existing name
never overwrites, it appends. get_secret without a version argument walks to the newest one.
This lesson runs against Lowkey Vault, a third-party local test double (there’s no official
Key Vault emulator) — it accepts any bearer token without checking it against Entra ID, and the
script below disables TLS verification against its self-signed certificate. Neither is safe
outside a local test double; RBAC, managed identity, and DefaultAzureCredential all need a real
tenant and aren’t covered here.
Every set_secret Call Is a New Version
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):
"""Lowkey Vault accepts any bearer token; it never checks it against Entra ID."""
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,
)
first = client.set_secret("db-password", "first-value")
time.sleep(1.1) # two sets in the same second sort ambiguously by created_on
second = client.set_secret("db-password", "second-value")
print("version 1 id:", first.properties.version)
print("version 2 id:", second.properties.version)
versions = list(client.list_properties_of_secret_versions("db-password"))
print("total versions:", len(versions))
latest = client.get_secret("db-password")
print("get_secret (no version):", latest.value)
pinned = client.get_secret("db-password", version=first.properties.version)
print("get_secret (pinned to version 1):", pinned.value)
$ docker run -d --name az-kv-demo nagyesta/lowkey-vault:7.4.0
24866501eececb773ba17d0aaf100df646010a45b4b05dad9f591d12b2bcf317
$ until docker logs az-kv-demo 2>&1 | grep -q 'Started LowkeyVaultApp'; do sleep 0.3; done
$ docker run --rm --network container:az-kv-demo -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_versions.py"
[notice] To update, run: pip install --upgrade pip
version 1 id: 93ddbfe7e81f47249346ef13a4dbe43c
version 2 id: c8cd7e30eeeb45f2963528f6f4c13baa
total versions: 2
get_secret (no version): second-value
get_secret (pinned to version 1): first-value
Both set_secret calls return a SecretProperties.version id — a vault-generated string, never
something to type by hand. get_secret("db-password") with no version argument walked straight
to version 2, and list_properties_of_secret_versions shows both versions still exist. The
time.sleep(1.1) matters: Lowkey Vault orders versions by created_on, and two writes inside
the same second can sort ambiguously — code that assumes “the last item in the list is the
newest” is one clock tick away from being wrong. Code that needs a specific version should hold
onto the id the write itself returned, as first.properties.version does here, not re-derive
it from list order.
A Version That Doesn’t Exist Raises the Same Exception as a Missing Secret
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.get_secret("db-password", version="0" * 32)
except ResourceNotFoundError as e:
print("bad version ->", type(e).__name__, e.status_code)
try:
client.get_secret("never-created")
except ResourceNotFoundError as e:
print("missing secret ->", type(e).__name__, e.status_code)
$ docker run --rm --network container:az-kv-demo -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_bad_version.py"
[notice] To update, run: pip install --upgrade pip
bad version -> ResourceNotFoundError 404
missing secret -> ResourceNotFoundError 404
Same exception type, same status code, for two different problems: a real secret with a version
id that was never issued, and a secret name that was never created at all. The SDK doesn’t tell
these apart in the exception class — only the message differs — so code that needs to distinguish
“this secret has never existed” from “this specific version is gone” has to read e.message
rather than branch on exception type.
Practice
1. Add a third version and confirm the count becomes 3 without re-running the first two sets.
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("db-password", "third-value")
versions = list(client.list_properties_of_secret_versions("db-password"))
print("total versions:", len(versions))
$ docker run --rm --network container:az-kv-demo -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_third_version.py"
[notice] To update, run: pip install --upgrade pip
total versions: 3
The vault from the lesson above is still running (it’s the same az-kv-demo container), and
db-password already had two versions — adding a third without restarting anything is the point.
2. Sort the versions list by created_on and fetch the oldest one explicitly, instead of trusting list order.
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,
)
versions = list(client.list_properties_of_secret_versions("db-password"))
oldest = sorted(versions, key=lambda v: v.created_on)[0]
value = client.get_secret("db-password", version=oldest.version).value
print("oldest version value:", value)
$ docker run --rm --network container:az-kv-demo -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_oldest_version.py"
[notice] To update, run: pip install --upgrade pip
oldest version value: first-value
sorted(..., key=lambda v: v.created_on)[0] is the explicit version of what “the first version”
actually means — by creation time, not by whatever order the vault happens to return them in.
3. Set a secret with a content_type and confirm it comes back on the properties the SDK returns.
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,
)
created = client.set_secret(
"api-key", "sk_test_12345", content_type="application/x-api-key"
)
print("content_type:", created.properties.content_type)
fetched = client.get_secret("api-key")
print("content_type on fetch:", fetched.properties.content_type)
$ docker run --rm --network container:az-kv-demo -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_content_type.py"
[notice] To update, run: pip install --upgrade pip
content_type: application/x-api-key
content_type on fetch: application/x-api-key
content_type is metadata the vault stores and returns alongside the secret, not something it
validates or acts on — Key Vault doesn’t parse or enforce it, the caller does.
4. List every secret name in the vault, not just their versions.
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,
)
names = sorted(s.name for s in client.list_properties_of_secrets())
print("secret names:", names)
$ docker run --rm --network container:az-kv-demo -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_secrets.py"
[notice] To update, run: pip install --upgrade pip
secret names: ['api-key', 'db-password']
list_properties_of_secrets is one level up from list_properties_of_secret_versions — it
enumerates secret names in the vault, one entry per name regardless of how many versions each
one has accumulated across this lesson and its practice tasks.
Cleanup
$ docker rm -f az-kv-demo
az-kv-demo
Next: soft-delete and recovery — what actually happens when a secret is deleted, and how to get it back.