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.
On this page
The old way to deploy from GitHub to Azure was to create a service principal, 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
- 01Workflow requests OIDC token
- 02GitHub issues signed token
- 03Entra checks federated credential
- 04Short-lived Azure token
- 05Deploy
Workload identity federation replaces it. GitHub issues a short-lived OpenID Connect 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 app registration or a user-assigned managed identity. 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
productionis 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.
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:
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-appGotchas
- The subject must match exactly. A job using an environment presents the environment subject, not the branch one.
- Each federated credential 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 mentions | Likely cause |
|---|---|
| No matching federated identity record | The subject in the token doesn't match. Check branch versus environment versus pull request |
| Unable to get ACTIONS_ID_TOKEN_REQUEST_URL | The 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 →