Thursday, June 18, 2026

Using Microsoft Entra ID Workload Identity Federation (WIF) to Deploy from GitHub Actions to Azure

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.

Hope this helps.

Happy Coding.

Regards,
Jaliya

No comments:

Post a Comment