Roadmap
Same discipline as fabric-emulator: each phase independently useful, real-SDK e2e-verified, ≥90% coverage floor in CI from the first commit.
P0 — secrets + the real auth handshake
Section titled “P0 — secrets + the real auth handshake”The core value: an Azure SDK acquires a token from entra-emulator via the challenge flow and round-trips secrets.
- Foundations: clock, config (
KV_*env + flags), store (vault, secret, secret_version, deleted_secret), self-signed TLS (*.vault.azure.net), Host-routed vault resolution + default seeded vault. - Auth: challenge
401advertising the entra authority; RS256 validation against entra JWKS (issuer, vault audience, clock-based expiry). Reuses the validator pattern from fabric-emulator’sinternal/auth. - Secrets: set/get/get-version/patch/list/list-versions (paged),
new-version-per-PUT,
enabledgating, informationalnbf/exp, backup/restore. - Soft delete: delete → deleted state with
scheduledPurgeDateon the clock; recover; purge; name-reuse conflict while deleted. -
/_emulatorclock + faults (incl. 429 throttling injection). - Docker (distroless) + docker-compose with entra-emulator (challenge authority pre-wired; vault resource app seeded via entra’s admin API).
- e2e (in-process entra, like fabric-emulator’s fixture): azsecrets +
azidentity.ClientSecretCredential complete the challenge flow
unmodified; managed-identity path via entra’s
/msi/token(IDENTITY_ENDPOINT/IDENTITY_HEADER); forged wrong-audience / expired tokens rejected; clock-advance expires a live token.
P1 — keys (real crypto) + hardening
Section titled “P1 — keys (real crypto) + hardening”- Keys CRUD/versions/soft-delete; RSA + EC generation (software-protected).
- sign/verify, encrypt/decrypt, wrap/unwrap with real Go crypto — output verifiable against the returned JWK.
- Optional per-principal permission map (
/_emulator/permissions) for authorization-denied paths. - e2e: azkeys SDK sign → local JWK verify; encrypt → decrypt round trip.
P2 — certificates
Section titled “P2 — certificates”- Certificates CRUD + policy; self-signed issuance; PFX/PEM import; linked key/secret materialization under the same name.
- e2e: azcertificates SDK create-self-signed → fetch → TLS-use the cert.
P3 — family integration
Section titled “P3 — family integration”- fabric-emulator AKV-reference connections resolve against this
emulator (its roadmap item, built on the fabric side):
workspace identity → entra token → vault secret → connection, fully offline. - e2e: the secret-as-SP-credential chain — the canonical “SP secret
lives in Key Vault” pattern across all three emulators
(
e2e/chain/run.py, in CI): a client-credentials call stores an SP secret in the vault, a managed-identity token (entra/msi/token, no credential in the workload) reads it back, that secret authenticates the SP to entra for a Fabric-audience token, and the token calls fabric-emulator. Three real processes; a wrong secret breaks the chain exactly where it would in Azure. - entra-emulator enhancement (shipped in entra v0.2.1): recognize
https://vault.azure.net(+ Storage, ARM) as well-known Azure resources, so client-credentials/MSI resolve the vault audience without seeding a resource app. - Compose file with all three emulators (
docker-compose.yml,fullprofile adds fabric).
P4 — SDK parity surface (shipped in v0.2.0)
Section titled “P4 — SDK parity surface (shipped in v0.2.0)”Round out the secondary operations the Azure SDKs expose beyond core CRUD, so a
test written against azkeys / azcertificates never hits an endpoint the
emulator lacks. Measured against the
Key Vault REST API reference
and what the real SDKs call; we keep our real-auth and real-crypto posture
throughout. With these, the emulator reaches full parity on the
SDK-observable surface.
- Keys: import a caller-supplied JWK (
PUT /keys/{name}, real RSA/EC material — a subsequent sign/verify round-trips), update-latest (PATCH /keys/{name}), backup/restore, and rotation policy get/set. - GetRandomBytes (
POST /rng). - Certificates: backup/restore, update attributes/policy
(
PATCH /certificates/{name}), policy update (PATCH /certificates/{name}/policy), issuers (GET/PUT/PATCH/DELETE /certificates/issuers/{name}+ list) and contacts (GET/PUT/DELETE /certificates/contacts). - Secure Key Release (
POST /keys/{name}/{version}/release) — a genuine signed JWS carrying the released public JWK. No HSM attestation (there is no enclave to attest), so any enabled key is releasable; the call path and token shape are emulated. - Certificate CSR merge (
POST /certificates/{name}/pending/merge) — a named issuer creates a pending operation with a real PKCS#10 CSR; you sign it with your own CA and merge the chain back, completing the async-issuance path fully offline. A live third-party CA remains the only certificate non-goal (the emulator never phones out).
P5 — real-service fidelity (shipped in v0.4.0)
Section titled “P5 — real-service fidelity (shipped in v0.4.0)”Close the remaining implementable gaps against real Key Vault, each landed with its witness (Go test + the multi-language SDK suites where the SDKs surface the operation):
- Rotate key (
POST /keys/{name}/rotate) — a new version with fresh material of the same type and size;key_ops/tags carry over. -
key_opsenforcement — operations outside the key’s list get403 Forbidden; the JWK’skey_opsalso drives SDK-local refusal. - Certificate operation cancel + delete (
PATCH/DELETE/certificates/{name}/pending) — cancelled operations refuse merge; deleted operations read absent until the next create. - Purge protection (
-purge-protection,KV_PURGE_PROTECTION,/_emulator/purge-protection) — purge403s,recoveryLevelreportsRecoverable. -
oct/oct-HSMrefused faithfully — vaults hold RSA/EC only; symmetric keys (and their AES algorithms) are Managed HSM territory. -
api-versionrequired + validated — 7.x and the date-based versions current SDKs send; the create-operation LRO now reportsinProgress→completedas the real service does.
P6 — remaining distance to full parity (shipped in v0.4.0)
Section titled “P6 — remaining distance to full parity (shipped in v0.4.0)”The whole remaining gap between the emulator and real Key Vault, in three honesty grades. Everything not listed here is either already 🟢 in parity.md or a declared scope boundary (ARM, Managed HSM, networking, attestation, live CAs) where full parity means keeping the refusal faithful.
Closable for real (the emulator genuinely does the work):
-
nbf/expenforced on key crypto operations — sign/verify/ encrypt/decrypt/wrap/unwrap with an expired or not-yet-valid key returns403 Forbidden, as real Key Vault refuses; deterministic on the controllable clock. Object retrieval stays permissive (as in real KV, where reads return the object and its attributes). - Certificate delete cascade — closing the documented divergence: deleting a certificate soft-deletes its linked key and secret; recover and purge carry them along too.
- Opaque backup blobs — backup output becomes a sealed blob (AEAD under an emulator-held key persisted in the data dir), restorable only by the same emulator instance — the honest analog of real Key Vault’s same-subscription/geography restore rule. Transparent-JSON blobs from earlier versions stop restoring.
- Multiple trusted issuers —
KV_ENTRA_ISSUERaccepts a comma-separated list; tokens from any listed issuer validate against that issuer’s JWKS. The 401 challenge advertises the first. - Auto-rotation from the rotation policy — the stored policy acts:
when a
lifetimeActionsrotate trigger (timeAfterCreate, ISO-8601) elapses on the emulator clock, the next read of the key lazily mints a new version, withattributes.expiryTimedriving the new version’sexp— the same lazy clock-driven pattern as soft-delete retention.
Emulatable contract (real document shapes + real enforcement, no ARM):
- Access policies —
POST /_emulator/access-policyaccepts the real vault access-policy document (objectId+permissions: {secrets, keys, certificates}) and compiles it onto the internal per-principal op allowlist. - RBAC built-in roles —
POST /_emulator/rbacassigns the real built-in roles (Key Vault Administrator, Secrets User/Officer, Crypto User/Officer, Certificates User/Officer, Reader) by name, expanded to their documented data-plane operation sets on the same allowlist.
Sequencing: enforcement first (nbf/exp, cascade — small, immediately SDK-witnessable), then sealing + multi-issuer, then the rotation engine, then authorization. Each lands with Go tests inside the ≥90% floor; the Python suite witnesses the expiry refusal with a real SDK.
P7 — full parity (shipped in v0.4.0)
Section titled “P7 — full parity (shipped in v0.4.0)”The final stretch: everything still short of 🟢 that can move without faking a trust property.
- BYOK for real —
PUT /keys/{name}accepts the.byoktransfer blob (key_hsm): the KEK lives in this vault, and the emulator genuinely undoesCKM_RSA_AES_KEY_WRAP— RSA-OAEP(SHA-1) unwraps the ephemeral AES-256 key, AES-KWP (RFC 5649, clean-room) unwraps the target key. The round-trip test proves possession by verifying a vault-produced signature against the original public key. The KEK is software-held, per the documented HSM normalisation. - Exportable + release policy — only a key created
attributes.exportable: truemay be released, as real Key Vault enforces;release_policyis stored, echoed on the bundle, and carried through rotation. Attestation remains the honest boundary. - Issuer registry drives issuance — a named issuer must be
registered under
/certificates/issuersbefore it can issue (Unknownremains the external-CSR escape hatch), as the real service requires. - Object-scoped authorization — allowlist entries accept
{type}/{op}:{object}, and RBAC assignments acceptscope: "/keys/{name}"— the same object-level scoping data-plane RBAC supports. Operations without an object (list, restore) need vault-level grants, as in real RBAC. - Soft-delete regraded — the delete→recover→purge state machine and clock-driven retention were always real enforced logic; lazy purge on observation is indistinguishable from a background job to any caller. The parity map now grades them accordingly.
Cross-cutting (throughout)
Section titled “Cross-cutting (throughout)”- CI: vet/build/test + 90% coverage floor + the three-emulator chain e2e.
- Starlight docs site on GitHub Pages (
/docs= source of truth), live at https://calvinchengx.github.io/azure-keyvault-emulator/. - GoReleaser: binaries + distroless Docker (GHCR) + Homebrew + winget. v0.1.0 shipped P0–P3; v0.2.0 adds the full P4 parity surface (import, backup/restore, rng, rotation policy, key release, issuers, contacts, certificate CSR merge) and the operator portal.
- Svelte operator portal (v0.2.0) — dashboard,
secrets/keys/certificates/deleted views, clock + fault-injection
controls. Svelte 5, built to a committed
portal/dist, embedded viago:embed, served at/_emulator/portal/, with a CI drift guard + Playwright mount smoke. Mirrors the family pattern.
Sequencing note
Section titled “Sequencing note”Build the challenge handshake before any storage — it is this emulator’s reason to exist, every SDK call path runs through it, and it defines the integration contract with entra-emulator. Secrets storage is straightforward once auth is honest.