bbabafemi
All posts
DevOps

Azure DevOps Variable Groups and Key Vault: the right way

Linking Azure Key Vault to Azure DevOps Variable Groups is the cleanest way to handle secrets in pipelines. Here's how to set it up properly, and the gotchas to avoid.

March 24, 2026 4 min readby Babafemi Bulugbe

There are several ways to handle secrets in Azure Pipelines. Pasting them directly into Variable Groups (the lazy way), reading them from a file (the hacky way), or what you should be doing linking a Variable Group to Azure Key Vault.

The result: secrets live in Key Vault (where they belong), pipelines reference them by name, rotation is invisible to your YAML.

Here's the right way to set it up.

The architecture

[Key Vault]
    │  (secret: "stripe-api-key")
    │
    ▼
[Variable Group "shared-prod-secrets"]
    │  (linked to vault, syncs the secret as a variable)
    │
    ▼
[Pipeline]
    └── variables: { group: shared-prod-secrets }
        steps: { uses: $(stripe-api-key) }

When the pipeline runs, the variable is fetched from Key Vault at runtime and made available, masked in logs, scoped to the job.

Step 1: Create the Key Vault and add secrets

az keyvault create \
  --name kv-myproject \
  --resource-group rg-myproject \
  --location uksouth \
  --enable-rbac-authorization true

az keyvault secret set \
  --vault-name kv-myproject \
  --name stripe-api-key \
  --value 'sk_live_...'

Use RBAC mode (covered in my Key Vault RBAC post). Don't use Access Policies; they're the legacy model.

Step 2: Create a service connection from Azure DevOps to Azure

In Azure DevOps: Project Settings → Service connections → New → Azure Resource Manager → Workload identity federation (recommended).

Workload identity federation is the OIDC equivalent for Azure DevOps. No client secret stored anywhere, just like the GitHub Actions OIDC pattern.

Step 3: Grant the service connection access to Key Vault

You need two roles on the service connection's underlying service principal:

SP_OBJECT_ID=<from the service connection page>

az role assignment create \
  --role "Key Vault Secrets User" \
  --assignee $SP_OBJECT_ID \
  --scope $(az keyvault show -n kv-myproject --query id -o tsv)

Key Vault Secrets User gives read access, exactly what the variable group needs. Don't grant Secrets Officer as pipelines don't need to write secrets.

Step 4: Create the linked Variable Group

In Azure DevOps: Pipelines → Library → + Variable group:

  • Name: shared-prod-secrets.
  • Toggle Link secrets from an Azure key vault as variables.
  • Pick the service connection from Step 2.
  • Pick your Key Vault.
  • Click Add and select the secrets you want to expose.

Each selected secret becomes a variable in the group. The variable name matches the secret name.

Step 5: Use it in the pipeline

variables:
  - group: shared-prod-secrets

steps:
  - script: |
      curl -H "Authorization: Bearer $(stripe-api-key)" \
        https://api.stripe.com/v1/charges
    displayName: Call Stripe

That's it. The secret is fetched at job start, masked in logs (***), and never appears in the pipeline definition.

Per-environment variable groups

A pattern I use:

  • shared-dev-secrets linked to kv-myproject-dev.
  • shared-prod-secrets linked to kv-myproject-prod.

In your stages:

- stage: deploy_dev
  variables: { group: shared-dev-secrets }
  ...

- stage: deploy_prod
  variables: { group: shared-prod-secrets }
  ...

Environment-specific vaults mean dev secrets and prod secrets never coexist in the same group, so a misconfiguration can't accidentally hand a prod secret to a dev pipeline.

The gotchas

Cache invalidation lag. When you update a secret in Key Vault, the variable group can take a few minutes to pick it up. For most secrets that's fine; for time-critical rotations, plan for it.

Secrets aren't passed to scripts as environment variables by default. They're set as pipeline variables. To use them in a script's environment, map them explicitly:

- script: ./deploy.sh
  env:
    STRIPE_KEY: $(stripe-api-key)

This is the most common confusion I see. Secrets work in inline scripts but seem to disappear in shell scripts. The env: mapping is the bridge.

Group permissions are per-group, not per-secret. If a service connection has access to the vault, it can read every secret in that group's filter. Use multiple groups and multiple service connections to scope tighter.

Don't put non-secret variables in linked groups. Mixing config and secrets in the same Variable Group creates governance ambiguity. Use a separate, regular Variable Group for non-secret config.

Why this beats the alternatives

  • Pasted Variable Groups: secrets sit in Azure DevOps, easy to leak, hard to rotate. Bad.
  • Key Vault tasks in pipelines: works but adds boilerplate to every pipeline. Linked Variable Groups centralize the wiring.
  • Service connections to Key Vault per pipeline: possible but creates lots of service connections to manage.

The linked Variable Group is the cleanest abstraction Azure DevOps offers for this. Use it.

The summary

If your pipeline references $(stripe-api-key) directly from a non-linked Variable Group, the next thing you build should be: move that secret to Key Vault, link the group, change nothing in your pipeline. Twenty minutes of work, infinitely better security posture.