How to migrate from Azure Key Vault

Key Vault is a good place for an Azure workload to fetch its own configuration with a managed identity. It is a poor place to look when you want to know what changed last Tuesday, or to get a working .env onto a new developer's machine before lunch.

Updated

Register an app for the migration

Key Vault's data plane authenticates a service principal, so the import needs one. In Microsoft Entra ID, register an application, create a client secret with a short expiry, and note three values: the directory (tenant) ID, the application (client) ID, and the secret itself.

Then give it read access to the vault and nothing more — Key Vault Secrets User under RBAC, or a get/list secrets access policy on the older model. Delete the client secret when the migration is finished.

You name the vault

Sink cannot offer you a list of your vaults. Enumerating them is an Azure Resource Manager call against the management plane, which is a different credential scope from the data plane the secrets live on — and asking for management-plane rights just to populate a dropdown would be the wrong trade. So the vault is typed:

sink login
sink init

export SINK_AZURE_KEY_VAULT_TENANT_ID=...
export SINK_AZURE_KEY_VAULT_CLIENT_ID=...
export SINK_AZURE_KEY_VAULT_CLIENT_SECRET=...

sink import azure_key_vault production \
  --scope vault=my-vault \
  --scope prefix=prod- \
  --dry-run

The prefix is optional; it filters which secrets are read and is stripped when naming the variable. Disabled secrets are skipped — the vault will not hand over their values anyway.

Names change, and you see how

Key Vault secret names allow hyphens, which environment variables do not, so the import rewrites them and prints every rename before anything is written:

prod-db-password   →  DB_PASSWORD   (prefix prod-)
prod-api-key       →  API_KEY       (prefix prod-)

A secret whose value is a flat JSON object is split into one variable per field, qualified by the secret's own name. --no-expand-json turns that off. Two names that would collide are reported and skipped, never silently merged.

Saving the credential

For a one-off migration, use the environment variables above and delete the client secret afterwards. If the import is going to recur, a workspace admin can store it:

sink connections add azure_key_vault "Key Vault production" \
  --scope vault=my-vault --scope prefix=prod-

sink import --connection "Key Vault production" production

The credential is verified against Azure before it is saved, encrypted at rest the same way your secrets are, and never returned by any endpoint. Revoke it from Settings › Connections. Give the service principal the narrowest role that works, and rotate the client secret on the schedule you would use anywhere else.

Afterwards

  • Re-run with --dry-run to spot drift between the vault and Sink.
  • Rotate in Azure first, re-import, then let everyone re-pull.
  • More than 500 secrets in one import is refused — narrow the prefix and batch it.
  • Key Vault stays in the runtime path for your Azure workloads; Sink is where people read and diff the values.
Try it on your own project

Sink keeps every environment under AES-256-GCM envelope encryption, with roles, versions and an audit trail — and puts it back in your .env with one command. The free tier does not ask for a card.