seekrit
Docs/Azure Key Vault

Sync to Azure Key Vault

Key Vault is a store rather than a runtime: nothing redeploys when a value changes, and whatever reads the vault — App Service, AKS with the Secrets Store CSI driver, or an Azure SDK client — picks up the new version on its next read. A binding owns one vault.

At a glance
What seekrit writesOne vault secret per name, each push adding a new version
Addressed byThe vault name (acme-prod), plus an optional name prefix
Connection carriesThe Entra tenant and application IDs, and which Azure cloud they live in
Token permissionThe Key Vault Secrets Officer role on the vault
Takes effectOn the next read by your app — no deployment is triggered
Value visibilityReadable by any principal with get on the vault

New to sync? Read Third-party sync first — the decryption grant, name mapping, deletions, and failure handling are the same on every destination.

caution

Key Vault cannot store an underscore, so DATABASE_URL is written as DATABASE-URL.

Secret names in Key Vault allow only letters, digits, and hyphens. That rules out the shape of almost every environment variable ever written, so seekrit rewrites _ as - by default rather than failing your whole environment.

The rename is never silent: it is stated on the binding, shown in the dashboard, and visible in each run's log. Your application has to read the hyphenated name — that is what is in the vault. If you would rather name every secret yourself, pass --name-mode reject and seekrit will fail the names it cannot store instead, leaving you to map them with the binding's --prefix and rename.

Because the rewrite is many-to-one, two secrets can collide — A_B and A-B both give A-B. seekrit fails both names and tells you which, rather than letting whichever ran last win.

This is what every tool that writes to Key Vault does — Doppler and Infisical both document the same substitution — because Azure has declined to allow underscores. What seekrit adds is the collision check and the reject escape hatch.

1. Register an application

Key Vault authenticates through Microsoft Entra, so a connection signs in as a service principal rather than holding an API key. In the Entra admin center, under App registrations → New registration, create one for seekrit. Its Overview page gives you the two IDs a connection needs, and Certificates & secrets → New client secret gives you the credential.

printf '%s' "$AZURE_CLIENT_SECRET" \
  | seekrit sync connect --name acme-azure --provider azure-key-vault \
      --azure-tenant-id 72f988bf-86f1-41af-91ab-2d7cd011db47 \
      --azure-client-id 0a1b2c3d-4e5f-6071-8293-a4b5c6d7e8f9

The tenant and client IDs are identifiers, not credentials — they appear in every token request and in your sign-in logs — so they are stored in the clear, which is what lets the dashboard show you which app a connection uses. Only the client secret is encrypted to the connection's key.

note

Sovereign clouds. Add --azure-cloud usgov or --azure-cloud china. The cloud sets both the sign-in authority and the vault's DNS suffix, and — unlike AWS, where the China partition falls out of the region string — nothing about a tenant GUID or a vault name reveals which cloud it belongs to, which is why the connection states it. public is the default.

--azure-cloudAuthorityVault suffix
public (default)login.microsoftonline.comvault.azure.net
usgovlogin.microsoftonline.usvault.usgovcloudapi.net
chinalogin.chinacloudapi.cnvault.azure.cn

2. Grant it access to the vault

On the vault, under Access control (IAM), assign the app the Key Vault Secrets Officer role. On a vault still using the older access-policy model, grant the set, delete, recover, and list secret permissions instead — list is what seekrit sync verify uses to prove the connection.

3. Bind an environment

A destination is one vault:

seekrit sync verify acme-azure --provider azure-key-vault --vault acme-prod

seekrit sync enable --connection acme-azure --provider azure-key-vault \
  --vault acme-prod \
  --app storefront --env production --acknowledge-decryption

--vault is the vault name, not its URL. --path adds a prefix to every name (--path storefront-). Key Vault names are flat — there are no folders, and / is not a character it accepts — so unlike Parameter Store's path this is a naming convention and nothing more.

Verify lists one secret, which is why the connection needs list on top of the write permissions. It proves the three things you can get wrong from the connection dialog at once: the client secret is current, the tenant will issue a token, and the vault exists and answers to this principal.

How a push behaves

  • One request per secret, and no listing. SetSecret is a true upsert — it creates the secret or adds a version to the one already there — so unlike the AWS connectors there is no create/update branch. seekrit never reads values back to compare them either: that would pull plaintext it does not own into the sync engine.
  • Every push mints a new version. Key Vault keeps the old ones, so a rotation is recoverable at the destination and your app's next read gets the current value.
  • Secrets seekrit writes are tagged managed-by: seekrit, so anyone reading the vault in the portal can see which entries a binding owns. Tags are metadata, never values.
  • A run is capped at 300 operations. That is Key Vault's scarcest documented bucket — CREATE secret allows 300 transactions per 10 seconds per vault, where everything else gets 4,000, and every write here spends from it. A bigger environment finishes across consecutive runs rather than throttling itself, and the run ledger says so.
  • A 429 stops the run and waits one throttling window. Key Vault's guidance documents no Retry-After and tells clients to back off on their own; the header is honored when it does appear.
  • A 401 or 403 fails the whole run once, rather than repeating the same fact beside all fifty names — an application that cannot write to a vault is one fact about the connection.
  • A value the service echoes back in an error is scrubbed before the reason is stored on the run row, and the oversize guard names only a byte count.

Name and value rules

note

Deletions are soft, and the name stays reserved. Every vault created since 2020 has soft delete on, and it cannot be turned off. A removed secret goes to the vault's recycle bin for its retention window — 90 days by default — and during that time the name cannot be re-created.

seekrit handles the round trip: a name that comes back is recovered and then written, so an exclude glob edited twice does not strand a secret for three months. Recovery is not instantaneous, so a name that returns within seconds of its deletion may take one more run to land. Nothing here ever purges a secret — that is irreversible, and not a secrets manager's call to make.

One deletion is deliberately skipped: a removed name whose vault name a live secret has taken over in the same run (DB_URL dropped as DB-URL is added) is left alone, because deleting it would destroy the value that just replaced it.

note

.NET configuration survives the trip intact. ASP.NET Core's Key Vault configuration provider reads a double dash as the : section separator, which is the same job __ does in an environment variable. So a secret named Database__ConnectionString is stored as Database--ConnectionString and read back as Database:ConnectionString — the hierarchy you meant, without a code change. That is not a coincidence: -- exists in Key Vault precisely because : is not allowed there either.

note

Key Vault caps a secret value at 25KB and a name at 127 characters, and a name that breaks either is reported as a failure against that name alone — the rest of the environment still pushes. A client secret in Entra expires after at most 24 months, so a connection that starts failing to authenticate has usually just outlived its credential.

caution

The vault must allow public network access. seekrit calls Key Vault from its own network, so a vault whose firewall is restricted to selected virtual networks or private endpoints will refuse the connection with a 403. If your vault has to stay network-restricted, keep decryption on your side instead — seekrit run, the SDKs, or ESO all reach it from inside your own network.

note

There is no json-bundle layout here. The reason one exists for AWS Secrets Manager is billing — AWS charges per secret per month — and Key Vault charges per operation, so fifty names cost the same stored fifty ways. One secret per name is simply correct.

Troubleshooting

SymptomCauseFix
Entra will not issue a tokenThe client secret expired (Entra caps them at 24 months), or a tenant/application ID is wrongCreate a new client secret and re-create the connection
403 on every nameThe app has no role assignment on the vault, no equivalent access policy, or the vault's firewall is restricted to selected networksAssign Key Vault Secrets Officer, or open the vault to all networks — seekrit calls the public endpoint
A name failed with "cannot contain _"The destination is set to --name-mode rejectRename it in the binding, or switch the destination to rewrite underscores
Two names failed, each naming the otherBoth map to the same vault name (A_B and A-B)Rename one in the binding, or give it its own prefix
One name failed on sizeThe value is over Key Vault's 25KB limit, or the mapped name over 127 charactersShorten it; the rest of the environment still pushed
A name is "being recovered"It was soft-deleted seconds ago and Key Vault is still restoring itNothing to do — the next run writes it
A run reports moreWorkIt hit the 300-operation budget for one vaultNothing to do — the engine re-runs immediately until the environment is caught up
The app still reads the old valueNothing redeploys on a Key Vault writeRestart or let its cache expire — App Service and the CSI driver both cache

See also