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 writes | One vault secret per name, each push adding a new version |
| Addressed by | The vault name (acme-prod), plus an optional name prefix |
| Connection carries | The Entra tenant and application IDs, and which Azure cloud they live in |
| Token permission | The Key Vault Secrets Officer role on the vault |
| Takes effect | On the next read by your app — no deployment is triggered |
| Value visibility | Readable 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.
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.
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-cloud | Authority | Vault suffix |
|---|---|---|
public (default) | login.microsoftonline.com | vault.azure.net |
usgov | login.microsoftonline.us | vault.usgovcloudapi.net |
china | login.chinacloudapi.cn | vault.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.
SetSecretis 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
429stops the run and waits one throttling window. Key Vault's guidance documents noRetry-Afterand tells clients to back off on their own; the header is honored when it does appear. - A
401or403fails 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
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.
.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.
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.
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.
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
| Symptom | Cause | Fix |
|---|---|---|
| Entra will not issue a token | The client secret expired (Entra caps them at 24 months), or a tenant/application ID is wrong | Create a new client secret and re-create the connection |
403 on every name | The app has no role assignment on the vault, no equivalent access policy, or the vault's firewall is restricted to selected networks | Assign 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 reject | Rename it in the binding, or switch the destination to rewrite underscores |
| Two names failed, each naming the other | Both 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 size | The value is over Key Vault's 25KB limit, or the mapped name over 127 characters | Shorten 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 it | Nothing to do — the next run writes it |
A run reports moreWork | It hit the 300-operation budget for one vault | Nothing to do — the engine re-runs immediately until the environment is caught up |
| The app still reads the old value | Nothing redeploys on a Key Vault write | Restart or let its cache expire — App Service and the CSI driver both cache |
See also
- Third-party sync — the shared model, naming and filtering, deletions
- Kubernetes with ESO — the pull-based path, for a network-restricted vault
- CLI reference — every
seekrit syncflag