In this post, let's have a look at how to deploy to Azure from GitHub Actions without storing any secrets, using Microsoft Entra ID Workload Identity Federation (WIF).
The traditional way to let a GitHub Actions workflow talk to Azure is to create a service principal, export its credentials, and paste them into a repository secret (commonly named AZURE_CREDENTIALS). It works, but now you own
a long-lived secret: it sits in GitHub, it can leak, and someone has to remember to rotate it before it expires.
With workload identity federation, there is no secret at all. You tell Entra ID to trust tokens that GitHub issues for a specific repository. At run time, GitHub mints a short-lived OIDC token, Azure validates it against that
trust, and hands back an access token. Nothing long-lived is stored anywhere.
Let's see how to set it up.
1. Add a federated credential to the app registration
You still need an Entra ID application. Instead of giving it a client secret, you add a federated credential that describes which GitHub workflow is allowed to sign in. The most important field is the
subject, which must match the token GitHub sends.
az ad app federated-credential create `
--id <app-client-id> `
--parameters '{
"name": "github-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:my-org/my-repo:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"]
}'
The subject is what scopes the trust. You bind it to exactly the situation that should be allowed to authenticate - a branch, or a GitHub Environment:
# a specific branch repo:my-org/my-repo:ref:refs/heads/main # a GitHub Environment (great for gating production) repo:my-org/my-repo:environment:production
Add one federated credential per subject you need (for example, one for the main branch and one for the production environment). The issuer and audiences values above are the standard ones for GitHub
Actions - leave them as-is.
2. Update the workflow
Here is the old, secret-based login:
- name: Azure Login uses: azure/login@v2 with: creds: ${{ secrets.AZURE_CREDENTIALS }}
And here is the federated version. Two things matter: the job needs the id-token: write permission so GitHub can mint the OIDC token, and azure/login now takes the three identifiers instead of a secret.
permissions: id-token: write # required for OIDC contents: read jobs: deploy: runs-on: ubuntu-latest steps: - name: Azure Login uses: azure/login@v2 with: client-id: ${{ vars.AZURE_CLIENT_ID }} tenant-id: ${{ vars.AZURE_TENANT_ID }} subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
Notice these three values are stored as vars, not secrets. A client ID, tenant ID, and subscription ID are just identifiers - they are not sensitive, so there is nothing to rotate and nothing to leak. That is the whole
point: the deployment now has no stored credentials at all.
One thing to watch: if you get an AADSTS70021: No matching federated identity record found error, the subject on your federated credential does not match what the workflow actually sent. Double check the branch name or
environment name, they have to line up exactly.
More read:
Hope this helps.
Happy Coding.
Regards,
Jaliya
No comments:
Post a Comment