Skip to content

Azure OIDC with GitHub Actions: Federated Credentials

Lesson 3 of 10Advanced4 min readGitHub Actions & CI/CD · Actions SecurityVerified: azure/login v3, Entra ID federated identity credentials, August 2026

Azure calls its version of this a federated identity credential: a record attached to an app registration saying “a token from this issuer, with this subject, may sign in as this application”. No client secret exists.

Terminal window
az ad app create --display-name github-deploy-production
az ad sp create --id APP_ID

The app registration is the identity; the service principal is its instance in your tenant. Both are needed — role assignments attach to the service principal.

Note what you are not doing: no --sdk-auth, no create-for-rbac, no client secret. If you find yourself copying a JSON blob into a repository secret, you are on the old path.

Terminal window
az ad app federated-credential create --id APP_ID --parameters '{
"name": "github-main",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:OWNER/REPO:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"],
"description": "Deployments from the main branch"
}'

Four fields carry the meaning:

issuer — GitHub’s OIDC issuer, identical for every GitHub.com repository.

subject — the sub claim this credential accepts. This is the security boundary, and it must match exactly: Azure does not support wildcards here, so each ref or environment needs its own federated credential record.

audiencesapi://AzureADTokenExchange is what azure/login requests.

name — an identifier for this credential, useful when an app has several.

Jobsubject
Push to mainrepo:OWNER/REPO:ref:refs/heads/main
Push of a tagrepo:OWNER/REPO:ref:refs/tags/v1.0.0
Pull requestrepo:OWNER/REPO:pull_request
Job with environment: productionrepo:OWNER/REPO:environment:production

Prefer the environment form. It composes with environment protection rules, so the credential is only usable by a job that has passed required reviewers.

For organisations needing patterns, Azure supports a flexible federated identity credential using a claims-matching expression rather than a literal subject. It is more capable and correspondingly easier to write too broadly; if you use it, restrict it to a specific repository rather than a repository-owner pattern.

permissions:
contents: read
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: azure/login@v3
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- run: az account show

vars, not secrets. These three are identifiers, not credentials — useless without a federated credential trusting your repository. Storing them as secrets masks them in logs, which makes debugging a federation failure noticeably harder for no security benefit.

Federation controls who signs in. Azure RBAC controls what they can do, and it is assigned separately:

Terminal window
az role assignment create \
--assignee APP_ID \
--role "Website Contributor" \
--scope /subscriptions/SUB_ID/resourceGroups/example-rg/providers/Microsoft.Web/sites/example-app

Two rules:

Assign at the narrowest scope that works — a specific resource, or a resource group. Contributor at subscription scope means the deploy identity can delete anything in the subscription, including things it has no business touching.

Prefer a specific built-in role over Contributor. “Website Contributor”, “AcrPush” and “Azure Kubernetes Service Cluster User” are far smaller grants than the general-purpose ones.

Avoid granting User Access Administrator or Owner. An identity that can assign roles can grant itself anything, which makes the rest of the scoping decorative.

SymptomUsual cause
AADSTS70021: No matching federated identity record foundThe subject does not match the actual sub claim
Same, after adding environment:The subject changed to the environment: form; add a credential for it
AADSTS700016: Application not found in the directoryWrong client ID, or the service principal was never created
Unable to get ACTIONS_ID_TOKEN_REQUEST_URLid-token: write missing from permissions
Login succeeds, the deployment failsFederation is fine; the role assignment is missing or too narrow

That last row is worth internalising. azure/login succeeding proves the federation works. Any failure after it is an RBAC problem, and looking for it in the federated credential wastes time.

AADSTS70021 names no expected value, so compare against reality: print the actual claims with the inspection step in OIDC, then compare with:

Terminal window
az ad app federated-credential list --id APP_ID --query "[].{name:name, subject:subject}" -o table

The pattern that scales:

  1. One app registration per environmentgithub-deploy-staging, github-deploy-production.

  2. One federated credential on each, with the subject naming the matching GitHub environment.

  3. Role assignments per app, scoped to that environment’s resource group.

  4. Environment variables in GitHub holding each app’s client ID, so the workflow needs no conditionals.

Separate app registrations rather than one with several credentials, because the role assignments differ. One identity that can deploy to both environments is one compromise away from production.

  1. Create an app registration and service principal in a test tenant.

  2. Add a federated credential with the subject repo:OWNER/REPO:ref:refs/heads/main.

  3. Assign Reader at one resource group’s scope.

  4. Run a workflow on main with id-token: write that logs in and runs az account show. Confirm success with no secret stored.

  5. Add environment: production to the job. Confirm it now fails with AADSTS70021 — the subject changed.

  6. Add a second federated credential for the environment subject and confirm it works again. You now know what that error means for life.

GitHub Actions Security ChecklistAudit your workflows against the failure modes that actually cause incidents. Free and complete.

Get the production security checklists and Actions hardening templates from the Professional Toolkit.