Azure Workload Identity Federation Without Secrets
We ripped every client secret out of our CI pipelines by pointing Azure federated credentials at GitHub's OIDC issuer. Here's the exact setup and the claims that trip people up.
Key takeaways
- We ripped every client secret out of our CI pipelines by pointing Azure federated credentials at GitHub's OIDC issuer.
- Here's the exact setup and the claims that trip people up.
On this page
Azure Workload Identity Federation Without Secrets
Every Azure service principal secret we ever created eventually became a liability. It sat in a GitHub Actions secret, a Key Vault, someone's .env, and a Terraform state file all at once. The default lifetime is two years, so nobody rotated it, and when it finally expired at 3am on a Sunday the deploy pipeline died with AADSTS7000215: Invalid client secret provided. That is the whole problem in one error code: a long-lived string that grants full access, copied to places you stopped tracking.
Federated identity credentials remove the string. Instead of your workload proving who it is with a secret only it should know, it presents a short-lived OIDC token that some trusted issuer already minted for it. Entra ID checks the token's claims against a rule you configured and, if they match, hands back an Azure access token. Nothing to store, nothing to rotate, nothing to leak. This is the Azure flavor of workload identity federation, and the mechanics line up closely with the AWS OIDC equivalent for GitHub Actions if you've done that already.
What a federated credential actually is#
You attach a federated credential to either an app registration or a user-assigned managed identity. The credential is not a secret. It is a small object describing which external tokens you trust:
issuer— the OIDC issuer URL that signs the incoming token, e.g.https://token.actions.githubusercontent.comfor GitHub Actions.subject— the exactsubclaim you expect, e.g.repo:acme/payments-api:ref:refs/heads/main.audiences— what the token was minted for. For Entra this is almost alwaysapi://AzureADTokenExchange.
When a workload calls Entra's token endpoint with a signed OIDC assertion, Entra fetches the issuer's public keys from its .well-known metadata, validates the signature, then compares iss, sub, and aud against your federated credential. All three match, you get a token. One character off, you get AADSTS70021: No matching federated identity record found.
Configuring it with the az CLI#
Say you have an app registration for your CI. Grab its object ID and add a federated credential that trusts pushes to main:
az ad app federated-credential create \
--id "$APP_OBJECT_ID" \
--parameters '{
"name": "gh-payments-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:acme/payments-api:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"]
}'
For a user-assigned managed identity the command is slightly different, but the claim fields are identical:
az identity federated-credential create \
--name gh-payments-main \
--identity-name id-payments-ci \
--resource-group rg-ci \
--issuer "https://token.actions.githubusercontent.com" \
--subject "repo:acme/payments-api:ref:refs/heads/main" \
--audience "api://AzureADTokenExchange"
Then give the underlying principal whatever RBAC it needs, scoped as tightly as you can stand:
az role assignment create \
--assignee "$APP_CLIENT_ID" \
--role "Contributor" \
--scope "/subscriptions/$SUB/resourceGroups/rg-payments"
That is the entire Azure side. No az ad app credential reset, no secret to paste anywhere.
The GitHub Actions side#
The workflow needs id-token: write so the runner can request an OIDC token, and it passes the three IDs to azure/login. There is no client-secret.
permissions:
id-token: write
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- run: az group show -n rg-payments -o table
The three values in secrets are just identifiers, not credentials. Leaking them buys an attacker nothing without a token from the trusted issuer bearing the exact subject you allowed.
Token TTL and why it matters#
The GitHub OIDC token lives about five to ten minutes. The Azure access token you exchange it for defaults to roughly an hour and can't be refreshed with the original assertion once that assertion expires. This is the point: the credential exists only for the length of one job. There is no standing secret an attacker can find next quarter. The tradeoff is that long-running jobs past the token lifetime need to re-authenticate, which for most deploy pipelines never comes up.
Gotchas that cost us an afternoon#
The subject is an exact string match, not a prefix or a glob. repo:acme/payments-api:ref:refs/heads/main will reject a push to release, a tag, and every pull request. Environment-scoped runs use repo:acme/payments-api:environment:production instead of the ref: form, so you add a separate federated credential per pattern. You can attach up to 20 per identity.
The audience must be api://AzureADTokenExchange. GitHub's default OIDC audience is your tenant URL, so the azure/login action overrides it for you; if you mint the token yourself, set the audience explicitly or Entra rejects it.
Kubernetes workloads follow the same path — the cluster's service account issuer becomes the issuer, and the subject looks like system:serviceaccount:namespace:sa-name. Cross-cloud is identical too: an AWS or GCP workload presenting its own OIDC token works as long as you register that issuer.
The call we'd make#
Federated credentials are the default for any Azure workload that runs somewhere with an OIDC issuer, which is CI, Kubernetes, and most managed compute now. Keep client secrets only for the rare legacy case that genuinely can't present a token, and put a 90-day expiry and an alert on those. Start with your noisiest pipeline, delete its secret the same afternoon, and watch the expiry pages stop.
Stay Updated
Get the latest tutorials, guides, and insights on AI, DevOps, Cloud, and Infrastructure delivered directly to your inbox.
GitHub Actions Best Practices in 2026: Workflows You Can Trust
A production-focused GitHub Actions guide: reusable workflows, least-privilege permissions, keyless OIDC to the cloud, SHA-pinned actions, environments with approvals, concurrency-safe deploys, and CodeQL/Dependabot gates — with copy-paste examples.
Kubernetes Workload Identity — Projected Tokens and OIDC to Cloud IAM
How pods can talk to AWS, GCP, and Azure with no static keys — using audience-bound projected ServiceAccount tokens and the cluster OIDC issuer.
More from Cloud
Explore more articles in this category
Best Serverless Databases in 2026 (Compared)
A practitioner comparison of the leading serverless databases by use case, cold-start behavior, branching, pricing model, and lock-in.
Cloudflare D1: The Edge SQLite Database Guide (2026)
A practitioner's look at Cloudflare D1, the serverless SQLite database built for Workers, covering setup, read replication, limits, and fit.
Neon vs PlanetScale: Serverless SQL Compared (2026)
A practitioner comparison of Neon's serverless Postgres against PlanetScale's Vitess-backed MySQL to help you pick the right database.
You might have missed
Evergreen posts worth revisiting.