sondahub

sondahub / Azure Key Vault sandbox

An Azure Key Vault mock that the Azure SDK signs in to

Key Vault’s secrets API, answered by sondahub: the bearer challenge first, a Microsoft Entra ID token endpoint for azure-identity to sign in at, then secrets with versions, content types, tags and attributes, soft delete with recover and purge, backup and restore, and paging — Key Vault’s errors and ids, so the official SDK works unchanged but for its URLs.

An independent imitation for testing. Not affiliated with, or endorsed by, Microsoft.

Connect

Instead of
https://<vault>.vault.azure.net
Vault URL
https://api.sondahub.com
Sign-in (authority)
https://api.sondahub.com/sandbox/azure-key-vault/login
Tenant
9e3779b1-c302-605a-db25-3734c1b23920
App
9e3779b1-6169-8a45-9fb9-5cba1e342fb5 / sondahub~sandbox.client-secret — or any app id
API versions
2016-10-01, 7.0, 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 2025-07-01
OpenAPI 3
https://api.sondahub.com/sandbox/azure-key-vault/openapi.json

Python (azure-keyvault-secrets, azure-identity)

from azure.identity import ClientSecretCredential
from azure.keyvault.secrets import SecretClient
from azure.core.pipeline.policies import SansIOHTTPPolicy

credential = ClientSecretCredential(
    '9e3779b1-c302-605a-db25-3734c1b23920',                    # the tenant the vault's challenge names
    '9e3779b1-6169-8a45-9fb9-5cba1e342fb5',
    'sondahub~sandbox.client-secret',
    authority='https://api.sondahub.com/sandbox/azure-key-vault/login',
    disable_instance_discovery=True,
)

class SondahubSession(SansIOHTTPPolicy):   # carry the sandbox session from each answer to the next request
    token = None
    def on_request(self, request):
        if SondahubSession.token:
            request.http_request.headers['X-Sondahub-Session'] = SondahubSession.token
    def on_response(self, request, response):
        SondahubSession.token = response.http_response.headers.get('X-Sondahub-Session') or SondahubSession.token

client = SecretClient('https://api.sondahub.com', credential, per_call_policies=[SondahubSession()])

client.set_secret('my-api-key', 's3cr3t', content_type='text/plain', tags={'env': 'dev'})
print(client.get_secret('my-api-key').value)

Node (@azure/keyvault-secrets, @azure/identity)

import { ClientSecretCredential } from '@azure/identity'
import { SecretClient } from '@azure/keyvault-secrets'

const credential = new ClientSecretCredential('9e3779b1-c302-605a-db25-3734c1b23920', '9e3779b1-6169-8a45-9fb9-5cba1e342fb5', 'sondahub~sandbox.client-secret', {
  authorityHost: 'https://api.sondahub.com/sandbox/azure-key-vault/login',
  disableInstanceDiscovery: true,
})

// carry the sandbox session from each answer to the next request
let session = null
const sondahubSession = {
  name: 'sondahubSession',
  async sendRequest(request, next) {
    if (session) request.headers.set('X-Sondahub-Session', session)
    const response = await next(request)
    session = response.headers.get('X-Sondahub-Session') ?? session
    return response
  },
}

const client = new SecretClient('https://api.sondahub.com', credential, { additionalPolicies: [{ policy: sondahubSession, position: 'perCall' }] })
const secret = await client.getSecret('orders-db-connection')

Import it. In Sonda: Import → From a URL, paste the OpenAPI address. You get a folder per resource and a request per operation, with example bodies and names from the seed, so most requests work as they are. Then set the auth: any bearer token (sandbox-token works). Any client that imports OpenAPI 3 takes the same address.

Try it here

These examples share one session: run them in order and each sees what the one before it did.
No token: the challenge (401, WWW-Authenticate)
curl "https://api.sondahub.com/secrets?api-version=2025-07-01"
List the secrets
curl "https://api.sondahub.com/secrets?api-version=2025-07-01" \
  -H "Authorization: Bearer sandbox-token"
Read one
curl "https://api.sondahub.com/secrets/orders-db-connection?api-version=2025-07-01" \
  -H "Authorization: Bearer sandbox-token"
Set a secret
curl -X PUT "https://api.sondahub.com/secrets/my-api-key?api-version=2025-07-01" \
  -H "Authorization: Bearer sandbox-token" \
  -H "Content-Type: application/json" \
  -d '{"value":"s3cr3t-value","contentType":"text/plain","tags":{"env":"dev"}}'
A disabled secret refuses reads (403)
curl "https://api.sondahub.com/secrets/legacy-ftp-password?api-version=2025-07-01" \
  -H "Authorization: Bearer sandbox-token"
Delete one: it goes to the deleted secrets
curl -X DELETE "https://api.sondahub.com/secrets/sendgrid-api-key?api-version=2025-07-01" \
  -H "Authorization: Bearer sandbox-token"
The deleted secrets
curl "https://api.sondahub.com/deletedsecrets?api-version=2025-07-01" \
  -H "Authorization: Bearer sandbox-token"
Recover one
curl -X POST "https://api.sondahub.com/deletedsecrets/old-webhook-secret/recover?api-version=2025-07-01" \
  -H "Authorization: Bearer sandbox-token"

From the shell, keep the token yourself: curl -i shows X-Sondahub-Session; send it back with -H "X-Sondahub-Session: …". How sessions work.

The seed vault

SecretWhat it holds
orders-db-connection1 version, text/plain
stripe-api-key3 versions, text/plain
storage-account-key1 version, expires in two months
app-settings2 versions, application/json
sendgrid-api-key1 version, text/plain
legacy-ftp-password1 version, disabled
old-webhook-secret1 version, deleted (recoverable)

A set makes a new version and the newest is what a read gets; PATCH changes a version’s attributes, content type and tags, not its value. A delete is soft: the secret moves to /deletedsecrets for 90 days, its name can’t be reused (409 ObjectIsDeletedButRecoverable), and it can be recovered or purged. A backup is every version in one signed blob; restore it into a vault that doesn’t have the name.

Every value in the seed is fake and made up when the site is built. What you write is kept in your own session token and nowhere else — but this is a public playground: never send it a real secret. Not affiliated with, or endorsed by, Microsoft.

What it answers

PUT/secrets/{name}Set: a new version. value, contentType, tags, attributes (enabled, nbf, exp).
GET/secrets/{name}/{version}Get the latest (version empty) or one version. And PATCH to update attributes.
GET/secretsList, and /secrets/{name}/versions; maxresults up to 25, nextLink with $skiptoken.
DELETE/secrets/{name}Soft delete. /deletedsecrets: list, get, DELETE to purge, POST …/recover.
POST/secrets/{name}/backupAnd POST /secrets/restore.
POST/sandbox/azure-key-vault/login/{tenant}/oauth2/v2.0/tokenClient credentials, Entra ID errors (AADSTS…). With …/v2.0/.well-known/openid-configuration and …/discovery/v2.0/keys.

Every vault call takes ?api-version=. Errors are Key Vault’s: {"error": {"code", "message", "innererror"}}.

Questions

Is this Azure?

No — an independent imitation of Azure Key Vault’s secrets API for testing, not affiliated with or endorsed by Microsoft. No subscription, tenant or HSM is involved.

Will the Azure SDK work against it?

Yes. The Python set-up on this page was run against the sandbox with azure-keyvault-secrets and azure-identity: the challenge, the sign-in at the sandbox’s token endpoint, set, get by version, update, list with paging, disable, delete and its poller, recover, purge, backup and restore, with the SDK’s challenge-resource check left on. The Node set-up follows @azure/keyvault-secrets’ and @azure/identity’s documented options.

Why is the vault at the root of the host?

Because the SDKs read a vault’s address from the host of every id they get back (…/secrets/{name}/{version}), and an id with more path than that is refused. So the vault is https://api.sondahub.com itself, and there is one: every session starts from the same seed vault.

How does the sign-in work?

A request without a token answers 401 with Key Vault’s challenge: WWW-Authenticate: Bearer authorization="https://login.microsoftonline.com/9e3779b1-c302-605a-db25-3734c1b23920", resource="https://sondahub.com". The SDK takes the tenant from it, asks its credential for a token for that resource, and retries. Point the credential’s authority at https://api.sondahub.com/sandbox/azure-key-vault/login (with instance discovery off, since the host isn’t Microsoft’s) and it gets one there: client credentials, any app id — 9e3779b1-6169-8a45-9fb9-5cba1e342fb5 has its secret checked. The vault then takes any bearer token. On another host than api.sondahub.com (a laptop, say) pass verify_challenge_resource=False.

What is not modelled?

Keys and certificates (and the secrets that back certificates), managed HSM, purge protection, RBAC and access policies, private endpoints and firewalls. Expiry and not-before are kept and reported but, as at Key Vault for secrets, not enforced on reads. A secret keeps its newest 25 versions.