azure-keyvault-emulator
v0.7.0 Three defects in rows the ledger already graded green, found by pointing Microsoft's Python SDK at them →Secrets, keys and certificates, served at Azure Key Vault's own wire protocol from a single Go binary. Real RSA and EC keys doing real signing, encryption and wrapping. Real X.509 issuance. Nothing pretended, and nothing that pretends is graded as though it were.
Authentication is not a pass-through. A tokenless call gets the real401 challenge, and the token the SDK brings back is verified against entra-emulator's JWKS: signature first, then issuer, audience and expiry. DefaultAzureCredential walks the same two steps here that it walks in production, so your tests exercise the credential path and not just the storage path.
Every number on this page is counted from this repository's own parity ledger and witness manifest when the site is built, including the — claims that still rest on our own tests alone.
A vault is the first dependency an application acquires and the one it can least easily fake. Three costs follow, and the third is the expensive one.
Working on secrets needs a vault, a role assignment on it, a principal to hold that role, and someone with rights to create all three. Purge protection and soft-delete retention are decisions made once, per vault, by somebody else. So the code that reads secrets is the code that never gets exercised locally.
One binary, a seeded vault, and a reset in one callMost local Key Vault substitutes accept any token, or serve a token endpoint that agrees with anything. The credential path is then the one part of the system your suite has no opinion about, and it is the part that fails in the deployed environment.
Real RS256 validation against a real issuer's JWKSA hand-rolled vault agrees with the client written against it. Ours did too: our own tests parsed nextLink rather than following it, so a pager that no Microsoft SDK could walk stayed green through four releases. Microsoft's Python SDK found it, and two more beside it, in rows already graded real.
Nothing in this sequence is shortened for the emulator. The vault does not know how to mint a token, and the issuer does not know what a vault is.
Azure SDK (SecretClient / azsecrets)
1. unauthenticated probe
-> azure-keyvault-emulator answers 401 WWW-Authenticate: Bearer
authorization="{entra authority}", resource="https://vault.azure.net"
2. SDK acquires a token from the advertised authority
-> entra-emulator mints aud=https://vault.azure.net
3. SDK retries with the token
-> signature checked against entra's JWKS, then iss, aud, exp -> 200Real Azure Key Vault is the sole reference, approached from two directions: Microsoft's published REST documentation says what the service does, and Microsoft's own client libraries, pinned and run in CI, witness that this emulator does the same. Every row graded real names the test or CI job that proves it, and CI fails when one goes missing.
| Witness tier | What it means | Claims credited |
|---|---|---|
ci: | A CI job driving a packaged Microsoft client from its own package manager, over a network, against the running emulator. The strongest evidence here, because nothing in the loop is ours. | — |
sdk: | A Go test in which Azure's own client does the talking:azsecrets, azkeys,azcertificates over the vault's wire. Third-party evidence, but in process rather than a released package over a network, so it ranks below ci:. | — |
go: | Our own Go tests: real HTTP, real signed JWTs, real authorization, but our client on both ends of our own implementation. It proves the emulator agrees with itself. | — |
Each claim is credited once, by its strongest witness, so the tiers above add up to the whole ledger. Counting citations instead would flatter every row, because one claim often carries four or five witnesses.
Python, JavaScript and .NET suites run the same surface on three operating systems each: challenge authentication, the secret lifecycle through soft-delete and purge, RSA cryptography with a tamper negative, on-demand rotation, a key_ops refusal, and a self-signed certificate operation polled to completion.
The Go suite reconstructs an *rsa.PublicKey from the returned JWK and verifies an SDK-produced signature outsidethe SDK, with a tampered-signature negative alongside. A round trip that only checks itself would pass on two broken halves.
One job reaches the vault at https://contoso.vault.azure.netwith the SDK's challenge-resource check left on, which the localhost suites have to disable, and verifies the TLS chain rather than skipping it. The wildcard SAN is load-bearing: remove it and the suite fails by name.
A chain job stores a service principal's secret in the vault, reads it back with a managed-identity token that carries no credential, uses that secret to authenticate to entra-emulator, and calls fabric-emulator with the result. Three real processes, and a wrong secret breaks it exactly where Azure would.
ci:chain · ci:arm-chain · ci:az-cliGrades and evidence lines below are the ledger's own, not a summary written for this page. The full table, with the reasoning for each row, is the parity map.
Set, get, list, list versions and get by version, with a new 32-hex version minted on every write. enabled is enforced;nbf and exp behave as real Key Vault does. Backup produces an opaque sealed blob restorable only by the same instance, which is the honest analogue of the same-geography rule.
RSA 2048/3072/4096 and EC P-256/P-384/P-521 generated withcrypto/rsa and crypto/ecdsa. Sign and verify across RS, PS and ES families, with Azure's raw r‖sencoding. Encrypt, decrypt, wrap, unwrap, JWK import, rotation on the emulator clock, and key_ops enforced with a403.
Self-signed issuance through x509.CreateCertificate, PKCS#12 and PEM import, a real PKCS#10 CSR for a named issuer, and a merge that verifies the signed leaf's public key against the pending key before accepting it. The linked key and secret materialise under the same name, as they do in Azure.
The 401 carries Key Vault's own AKV10000code and points at entra-emulator's authority. Signatures are checked before any claim is read; issuer, audience and validity follow, on a clock you can move. Several trusted issuers are allowed at once, each validating against its own JWKS.
Delete, list deleted, recover and purge as an enforced state machine, with retention validated between 7 and 90 days and name reuse refused with 409 while an object is still deleted. Retention expiry is genuinely clock-driven rather than announced.
By default the sibling arm-emulator governs:az role assignment create andaz keyvault set-policy write over ARM's real wire and this data plane enforces the result, no-assignment-means-no-access included. A group grant authorises any caller carrying that group.
The Host header selects the vault, object identifiers are always rendered canonically whatever the listen address, and the self-signed material covers *.vault.azure.net with a stable fingerprint across restarts. Paging is capped at 25 with anextLink the SDK pagers can actually follow.
exportable is enforced and a non-exportable key refuses release, the release policy round-trips, and the JWS is genuinely signed. There is no attestation, because there is no enclave to attest. That is why the row is amber and not green.
The emulator generates the key and a real CSR and merges whatever chain your CA signs. Attach a real CA and the X.509 is real; attach nothing and this row is a contract only. Live DigiCert and GlobalSign integrations are out of scope.
go:TestCertMergeFlow · sdk:TestAzcertificatesMergeA controllable clock at /_emulator/clock makes token expiry and retention windows deterministic, fault injection forces429 with Retry-After or 500 for the next few requests, and a read-only portal shows vault state in a browser. These exist for tests and have no Key Vault counterpart.
The counts below come from the same manifests as the flattering ones at the top of the page, so they cannot be the half that goes stale.
There is no differential test against a real vault, and no credentials for one. The reference is Microsoft's published REST documentation plus Microsoft's own client libraries, pinned and executed in CI. Those libraries are strong evidence about the wire, and no evidence at all about behaviour they never exercise. Read every grade on this page as "agrees with the documentation and with Microsoft's clients", not as "checked against Azure".
Named, because a claim proved only by the code that implements it proves that the emulator agrees with itself: —. Everything else in the ledger is carried by a Microsoft client.
Secure Key Release is emulated, since no enclave exists to attest. Assignment through the /_emulator control surface is emulated, because it is real enforcement over a wire that is not ARM's. Certificate issuance by a real CA is contract-only until you attach a CA.
The line is drawn around the vault. Everything below is the infrastructure surrounding it, which no process on a laptop can honestly provide. There are no 501 stubs in this emulator: an unimplemented feature has no route, so a client sees a404.
| Azure feature | Status | Why |
|---|---|---|
| ARM control plane | not implemented | Creating and deleting vaults is a different plane. This serves the data plane a vault client talks to; the sibling arm-emulator serves the other one. |
| Managed HSM | not implemented | A distinct service and a hardware trust boundary. Returning software material under an HSM name would tell a caller their keys are hardware-backed when they are not. |
| Private endpoints, firewall, network ACLs | not implemented | Network topology, not vault behaviour. |
| Customer-managed-key encryption of the vault itself | not implemented | Infrastructure level. |
| Real CA issuance (DigiCert, GlobalSign) | not implemented | Needs a real CA. The bring-your-own-engine CSR and merge path above is the substitute. |
| Diagnostic logs, Event Grid, metrics | not implemented | An Azure Monitor surface, not a vault surface. |
Publicly known seeded principals and secrets, self-signed TLS, an open/_emulator control surface that can move the clock and rewrite permissions. It exists so a laptop can serve a vault that behaves closely enough to develop against. Run it on localhost. Never point real secrets at it.
A distroless image with a built-in healthcheck, or a static binary for six platform pairs across Linux, macOS and Windows. Pure Go, no cgo, no runtime dependency.
brew install calvinchengx/tap/azure-keyvault-emulator # macOS and Linux
winget install calvinchengx.azure-keyvault-emulator # Windows
go install github.com/calvinchengx/azure-keyvault-emulator/cmd/azure-keyvault-emulator@latest
docker run -p 8444:8444 ghcr.io/calvinchengx/azure-keyvault-emulator:latestThe vault on its own has no issuer to trust, so the useful unit is the pair. The repository'sdocker-compose.yml wires the challenge to entra-emulator's authority and points token validation at its JWKS, with no configuration to write. Every install method is ininstallation, and the prerequisites per platform are in platform setup.
Three verbs, the same on Linux, macOS and Windows.
make doctor # toolchain and docker context check, run this first
make up # entra-emulator :8443 and keyvault-emulator :8444
make status # is the pair usable? containers, endpoints, the 401 challengeThen point a real client at it. Only the vault URL and the authority change; the seeded application values below are public development credentials from entra-emulator's deterministic seed, so a fixture written today still resolves next year.
import os
from azure.identity import DefaultAzureCredential, DefaultAzureCredentialOptions
from azure.keyvault.secrets import SecretClient
os.environ["AZURE_TENANT_ID"] = "6f89cf12-978b-4d23-ac18-9ef0c127cf87"
os.environ["AZURE_CLIENT_ID"] = "00d88624-f0d7-46f6-a641-6232c2608928"
os.environ["AZURE_CLIENT_SECRET"] = "daemon-app-secret"
os.environ["AZURE_AUTHORITY_HOST"] = "https://localhost:8443"
cred = DefaultAzureCredential(
DefaultAzureCredentialOptions(disable_instance_discovery=True))
client = SecretClient(
vault_url="https://localhost:8444",
credential=cred,
# localhost is not *.vault.azure.net, so the challenge-resource check is
# relaxed here. Addressing the vault by its real hostname does not need it.
disable_challenge_resource_verification=True,
connection_verify=False, # self-signed certificate, local only
)
client.set_secret("db-password", "hunter2")
print(client.get_secret("db-password").value) # -> hunter2The Go, JavaScript and .NET clients work the same way. The handshake is described in detail underauthentication, the family wiring under family integration, and how all of this is tested under testing.