azureblog.co.uk
← cd ~/posts

Deploy from GitHub Actions to Azure without storing a single secret

Workload identity federation lets GitHub Actions sign in to Azure with short-lived tokens instead of a stored client secret. Here's the full setup.

2 min read⚠ checked 21 Jan 2026Azure · Entra ID · Security
On this page
  1. 1. Create the identity
  2. 2. Add a federated credential
  3. 3. Update the workflow
  4. Gotchas
  5. Troubleshooting

The old way to deploy from GitHub to Azure was to create a , generate a client secret and paste it into GitHub as a secret. That secret lasts months or years and works from anywhere if it leaks.

Client secret in GitHub

  • Valid for months or years
  • Works from anywhere if leaked
  • Has to be rotated

Workload identity federation

  • Token valid for minutes
  • Only accepted from your repo and branch or environment
  • Nothing to rotate
Stored secret versus federated credential.
  1. 01Workflow requests OIDC token
  2. 02GitHub issues signed token
  3. 03Entra checks federated credential
  4. 04Short-lived Azure token
  5. 05Deploy
No secret is stored anywhere.

Workload identity federation replaces it. GitHub issues a short-lived token for each workflow run, and Entra exchanges it for an Azure token, but only if it comes from the repository and branch you trust.

1. Create the identity

Use an or a user-assigned . Then grant it the Azure role it needs, scoped as tightly as possible, such as Contributor on one resource group.

2. Add a federated credential

On the app registration, go to Certificates & secrets → Federated credentials, or the equivalent on the managed identity. Choose the GitHub Actions scenario and enter:

  • Organization and repository, such as contoso/infra
  • Entity type: branch, environment, tag or pull request. For deployments, an environment like production is the best choice, because GitHub environments can have their own approval rules.

The resulting subject looks like repo:contoso/infra:environment:production, with issuer https://token.actions.githubusercontent.com and audience api://AzureADTokenExchange.

GitHub ActionsGitHub OIDCEntra IDAzureRequest ID token(id-token: write)1Signed JWT:repo:org/repo:environment:production2Exchange for Azure token3Issuer, subject and audiencematch federated credential?Short-lived access token5Deploy6
The token exchange on every workflow run.

3. Update the workflow

Store the client ID, tenant ID and subscription ID as GitHub variables or secrets. They're identifiers, not credentials. The workflow needs permission to request an OIDC token:

yaml
permissions:
  id-token: write
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - uses: azure/login@v2
        with:
          client-id: ${{ vars.AZURE_CLIENT_ID }}
          tenant-id: ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
      - run: az group show --name rg-app

Gotchas

  • The subject must match exactly. A job using an environment presents the environment subject, not the branch one.
  • Each matches one subject. Add more if you deploy from several branches or environments.
  • Delete the old client secret once the new setup works.

Troubleshooting

Error mentionsLikely cause
No matching federated identity recordThe subject in the token doesn't match. Check branch versus environment versus pull request
Unable to get ACTIONS_ID_TOKEN_REQUEST_URLThe workflow is missing permissions: id-token: write
Authorization failed (Azure)Sign-in worked, but the identity has no role on the target resource

The JWT decoder is handy here: print the GitHub token's claims in a debug step and compare the sub with your federated credential.

  • Use GitHub environments for production, with required reviewers
  • One federated credential per environment
  • Least-privilege role, scoped to a resource group
  • Delete the old client secret once OIDC works

This post was last checked against Microsoft's documentation over six months ago. The approach should still hold, but check the linked sources for anything that has changed before you act on it.

Next in Automating Entra and Azure safely · part 4 of 5Find expiring app secrets and certificates before they bite →