Read Do you use OIDC instead of long-lived secrets in your pipelines? first. This rule covers the GitHub Actions to Azure specifics.
If your workflow still logs in to Azure with a service principal client secret saved as AZURE_CREDENTIALS, replace it with a federated credential. Each workflow run gets a short-lived token that Azure accepts only from the repository and environment you name, and there is nothing left in GitHub to leak or rotate.
Create an app registration (or a user-assigned managed identity) for the deployment, and give it the least privilege it needs, for example Contributor on a single resource group instead of the whole subscription. Do not create a client secret for it.
On the app registration, add a federated credential for GitHub Actions. The subject says exactly what is trusted:
| Setting | Value |
|---|---|
| Issuer | https://token.actions.githubusercontent.com |
| Subject | repo:YourOrg/YourRepo:environment:production |
| Audience | api://AzureADTokenExchange |
Scope the subject to a GitHub environment, not just the repository. Then the environment's protection rules (required reviewers, allowed branches) decide who can deploy.
In the repository settings, create the environment (for example production), limit it to protected branches, and require a pull request approval before a deployment starts.
Give the job permission to request a token, name the environment, and pass the identifiers to azure/login. There is no AZURE_CREDENTIALS and no secret key.
permissions:id-token: writecontents: readjobs:deploy:runs-on: ubuntu-latestenvironment: productionsteps:- uses: actions/checkout@v4- uses: azure/login@v3with:client-id: ${{ secrets.AZURE_CLIENT_ID }}tenant-id: ${{ secrets.AZURE_TENANT_ID }}subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}- run: az webapp deploy --resource-group my-rg --name my-app --src-path ./app.zip
✅ Figure: Good example - The workflow signs in with a short-lived token. The three IDs identify the app but are not credentials
- uses: azure/login@v1with:creds: ${{ secrets.AZURE_CREDENTIALS }}
❌ Figure: Bad example - AZURE_CREDENTIALS holds a client secret that works from anywhere until it expires or someone rotates it
Warning: Only pass values from secrets into client-id, tenant-id and subscription-id. Never pass values an outsider can influence, such as pull request titles, branch names or workflow inputs. Scope the federated credential to an environment, not repo:YourOrg/YourRepo:ref:refs/heads/*, or any workflow that can run in that repository can deploy.