HOW-TO GUIDE | CI/CD SECURITY
10 min read

GitHub Actions to Azure: Implementing OIDC Federation with Microsoft Entra ID

How to configure a Microsoft Entra ID federated credential and update a GitHub Actions workflow to authenticate to Azure with azure/login and no client secret

0 secrets
Long-lived credentials left in GitHub once AZURE_CLIENT_SECRET is removed from the workflow
AADSTS70021
Entra ID error code returned when no federated credential subject matches the incoming token
~1 hour
Typical maximum lifetime of the Entra ID access token issued after a successful federated exchange
id-token: write
GitHub Actions job permission required before a workflow can even request an OIDC token

SponsoredHorizon3.ai

Proactive Security for the AI Era

NodeZero continuously and autonomously pentests infrastructure, identity, cloud, and now web applications, chaining weaknesses across every domain the way real attackers do. Every finding ships with replayable proof showing exploitable business impact, not theoretical risk.

See NodeZero WebApp in action

Workload Identity Federation using OIDC is already well covered for GitHub Actions to AWS and GitHub Actions to GCP in our Workload Identity Federation practitioner guide; if that is the pairing you need, start there. This article is the Azure-specific companion. It walks through the parts that are unique to Microsoft Entra ID: how to create the federated credential on an App Registration, the exact subject claim format Entra ID matches against the GitHub-issued token, how to scope the Azure RBAC role assignment, and how to wire the azure/login action into a workflow with no AZURE_CLIENT_SECRET anywhere in GitHub. It also covers the failure that trips up almost everyone doing this for the first time: a subject claim that looks close enough but does not match exactly, which Entra ID rejects with error AADSTS70021.

Why a static Azure service principal secret in GitHub Actions is a real risk

The conventional way to let a GitHub Actions workflow deploy to Azure is to create a service principal, generate a client secret for it, and store that secret (often as part of an AZURE_CREDENTIALS JSON blob, or as a standalone AZURE_CLIENT_SECRET) in GitHub Actions secrets. That secret is a bearer credential: whoever holds the string can authenticate as that service principal from anywhere, not just from your workflow. Client secrets on an Entra ID App Registration commonly get created with no expiration, or with an expiration far enough out that they are functionally permanent until someone remembers to rotate them. In practice that means a single leaked value, whether from a misconfigured log, a forked pull request workflow, a compromised third-party action, or a runner that got popped, grants standing access to Azure for as long as the secret remains valid, which is often measured in months or years rather than minutes. The service principal behind that secret is also frequently over-scoped, holding Contributor or Owner at the subscription level because it was easier to set up that way, which turns a single credential leak into a subscription-wide compromise. OIDC federation removes the secret from the equation entirely: there is nothing long-lived to leak, because Azure never issues a durable credential to the workflow in the first place.

Prerequisites

Before starting, confirm you have the access this procedure requires. Each of these gaps is a common reason the setup stalls partway through.

Subscribe to unlock Remediation & Mitigation steps

Free subscribers unlock full IOC lists, Sigma detection rules, remediation steps, and every daily briefing.

Free daily briefing

Briefings like this, every morning before 9am.

Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.

Step 1: Create or choose the App Registration

If you already have a service principal your pipeline uses, you can attach a federated credential to its existing App Registration and skip straight to Step 2. To create a new one:

az ad app create --display-name "gh-actions-<repo>-<environment>"

Record the appId (this is the client ID you will use later) and then fetch the app's object ID, which the federated-credential command needs:

az ad app show --id <appId> --query id -o tsv

Create a corresponding service principal for the app if one does not already exist, since the RBAC role assignment in Step 3 is made against the service principal, not the App Registration object itself:

az ad sp create --id <appId>

Use a naming convention that ties the App Registration to a single repository and, ideally, a single environment (for example gh-actions-payments-api-prod). A separate App Registration per deployment target keeps the blast radius of any single federated credential misconfiguration contained to one pipeline.

Step 2: Configure the federated credential with the correct subject claim

This is the step that determines whether the whole setup works, and it is where almost every first attempt fails. Microsoft Entra ID authorizes the token exchange by matching the sub (subject) claim in the OIDC token GitHub issues against the subject string on the federated credential. The two have to match exactly, including case. The issuer and audience are fixed values for GitHub Actions:

issuer: https://token.actions.githubusercontent.com audience: api://AzureADTokenExchange

The subject claim format depends on what triggers the workflow. The common patterns are:

repo:<org>/<repo>:ref:refs/heads/main (a specific branch) repo:<org>/<repo>:environment:production (a specific GitHub Environment, regardless of branch) repo:<org>/<repo>:pull_request (any pull request against the repo) repo:<org>/<repo>:ref:refs/tags/v1.0.0 (a specific tag)

For a production deployment pipeline, scoping to a GitHub Environment is usually the better choice over a raw branch ref, because it lets you layer GitHub's own environment protection rules (required reviewers, wait timers) on top of the Azure federation, and because the environment name stays stable even if branching strategy changes later. Save the credential definition to a file and create it:

{ "name": "gh-actions-payments-api-prod", "issuer": "https://token.actions.githubusercontent.com", "subject": "repo:contoso/payments-api:environment:production", "audiences": ["api://AzureADTokenExchange"] }

az ad app federated-credential create --id <app-object-id> --parameters ./credential.json

One additional detail worth planning for now rather than later: as of mid-2026, GitHub also supports an immutable subject format that appends the numeric owner and repository IDs to the name-based subject (for example repo:contoso@5544123/payments-api@821093847:environment:production), and GitHub is rolling this out by default for newly created, renamed, or transferred repositories. If your repository is on the immutable format, or moves to it later, the subject string above will stop matching and you will need either a second federated credential for the immutable subject or a flexible federated credential using a claims matching expression, which is covered in the failure cases section below.

Step 3: Assign a least-privilege Azure RBAC role

The federated credential only proves identity to Entra ID. It grants nothing on its own; the service principal still needs an Azure RBAC role assignment to do anything in the subscription. Resist the default of reaching for Contributor at the subscription scope. Scope the role assignment to the narrowest resource group, or ideally the narrowest individual resource, that the workflow actually needs to touch, and use the most specific built-in role that covers the actions the pipeline performs (for example Storage Blob Data Contributor for a workflow that only needs to push build artifacts to a storage account, rather than Contributor over the whole resource group):

az role assignment create --assignee <appId> --role "Storage Blob Data Contributor" --scope /subscriptions/<sub-id>/resourceGroups/<rg-name>

This mirrors the same reasoning covered in our zero trust network architecture guide: a federated identity is still an identity, and it should be granted only the access its specific task requires, not broad standing access justified by convenience. Treat the RBAC assignment as a separate control decision from the OIDC trust relationship, and revisit both whenever the pipeline's actual responsibilities change.

Step 4: Update the GitHub Actions workflow

With the federated credential and RBAC role in place, update the workflow to authenticate with azure/login and OIDC instead of a stored secret. Two things are easy to miss: the id-token: write permission has to be declared explicitly (GitHub Actions does not grant it by default), and none of the three values passed to azure/login are secret in the traditional sense, so it is reasonable to store the client ID, tenant ID, and subscription ID as repository or environment variables rather than encrypted secrets if your workflow structure benefits from that.

permissions: id-token: write contents: read

jobs: deploy: runs-on: ubuntu-latest environment: production steps: - uses: actions/checkout@v4 - 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 there is no client-secret input at all. If your workflow currently references AZURE_CLIENT_SECRET or a full AZURE_CREDENTIALS JSON blob anywhere, this step replaces both. Once the login step is confirmed working end to end, delete the old secret from GitHub Actions secrets rather than leaving it in place as a fallback; an unused static credential still sitting in the secrets store is exactly the risk this migration is meant to close, and see our GitHub Actions security hardening guide for the broader set of workflow permission and third-party action controls worth reviewing at the same time.

Validation: confirming the federated token exchange actually worked

A workflow that reaches the azure/login step without an authentication error is a good sign, but it is worth confirming the exchange happened the way you intended rather than assuming it. Check these three places.

Subscribe to unlock Remediation & Mitigation steps

Free subscribers unlock full IOC lists, Sigma detection rules, remediation steps, and every daily briefing.

Failure cases

Most first-time setup failures come down to one of these four causes, roughly in order of how often they show up.

Subscribe to unlock Remediation & Mitigation steps

Free subscribers unlock full IOC lists, Sigma detection rules, remediation steps, and every daily briefing.

Security tradeoffs to weigh before you scope broadly

The temptation once this is working is to make the federated credential cover more than it needs to, usually to avoid repeating this setup for every new repository or branch. Resist it. A federated credential built with a claims matching expression that wildcards the repository, such as matching any subject starting with repo:contoso-org/ regardless of which repository or branch follows, grants every repository in that GitHub organization the ability to authenticate as that Azure identity. If the RBAC role behind it has any meaningful write access, that wildcard has effectively turned a single compromised or malicious repository anywhere in the org into a path to that access, which defeats the reason for scoping OIDC federation in the first place. The same reasoning applies to scoping by branch: a subject that matches any ref (ref:refs/heads/*, where supported) rather than a specific branch means a pull request from an untrusted branch or a compromised dependency in a feature branch's workflow can potentially authenticate with the same access as your production pipeline. Prefer one federated credential per repository and per environment or protected branch, sized to exactly what that pipeline needs, even though it means more App Registrations or more federated credentials to keep track of. The bookkeeping cost is small and predictable; the cost of an over-scoped wildcard being discovered by an attacker is not.

The bottom line

Removing AZURE_CLIENT_SECRET from a GitHub Actions workflow comes down to three things done correctly: a federated credential whose subject claim matches exactly what GitHub's token presents, an RBAC role assignment scoped to only what the pipeline needs, and a workflow that declares id-token: write and passes client ID, tenant ID, and subscription ID to azure/login with no secret at all. Get the subject claim wrong and Entra ID will reject the exchange outright with AADSTS70021, which is a safe failure mode. Get the scoping too broad and it will work, which is the more dangerous outcome, since a wildcarded credential authenticating half your GitHub organization looks identical in the workflow logs to one scoped correctly.

Frequently asked questions

What is the exact subject claim format for a GitHub Actions federated credential in Microsoft Entra ID?

It depends on the trigger: repo:<org>/<repo>:ref:refs/heads/<branch> for a specific branch, repo:<org>/<repo>:environment:<name> for a GitHub Environment, repo:<org>/<repo>:pull_request for any pull request, and repo:<org>/<repo>:ref:refs/tags/<tag> for a tag. The issuer is always https://token.actions.githubusercontent.com and the audience is api://AzureADTokenExchange.

Why does Entra ID return error AADSTS70021 when the workflow tries to authenticate?

AADSTS70021 means no federated credential on the App Registration has a subject string that exactly matches the sub claim in the OIDC token GitHub issued for that run. It is almost always a small mismatch such as a wrong ref path, a typo in an environment name, or a trailing slash, and it is fixed by comparing the credential's subject against the token's actual sub claim character for character.

Do I need azure/login@v2 or a newer major version for OIDC login to work?

OIDC login with client-id, tenant-id, and subscription-id inputs works on azure/login@v2 and later; there is no reason to stay on an older major version that predates OIDC support. Always pin to a specific released major version rather than a floating tag, and check the action's own README for the current recommended version before adopting it.

Can one App Registration and federated credential safely cover multiple repositories or branches?

Technically yes using a claims matching expression with a wildcard, but it is not recommended for anything with meaningful Azure access. A wildcarded subject grants every matching repository or branch the same authentication path, so a compromise anywhere in that wildcard's scope can reach the Azure identity. Scope one federated credential per repository and environment instead.

Does configuring OIDC federated credentials require an Azure AD Premium or Workload ID Premium license?

No. Creating an App Registration and attaching a federated credential for GitHub Actions OIDC is a standard Microsoft Entra ID capability available without a premium add-on. Workload ID Premium licensing is only needed if you additionally want to apply Conditional Access policies to that workload identity's sign-ins, which is a separate, optional layer on top of federation itself.

How is Azure AD federation different from the GitHub Actions OIDC setup for AWS or GCP?

The core pattern, a trusted OIDC issuer exchanged for short-lived cloud credentials, is the same across all three, but the configuration objects differ: Azure uses a federated credential on an Entra ID App Registration, AWS uses an IAM OIDC identity provider and role trust policy, and GCP uses a workload identity pool and provider. AWS and GCP setup is covered separately in our companion Workload Identity Federation guide.

Sources & references

  1. Microsoft Learn: Configure an app to trust a GitHub Actions workflow
  2. Microsoft Learn: Migrate GitHub Actions federated credentials to immutable subjects
  3. GitHub: azure/login action (README, inputs, and OIDC troubleshooting)
  4. GitHub Docs: About security hardening with OpenID Connect

Free resources

25
Free download

Critical CVE Reference Card 2025–2026

25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.

No spam. Unsubscribe anytime.

Free download

Ransomware Incident Response Playbook

Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.

No spam. Unsubscribe anytime.

Free newsletter

Get threat intel before your inbox does.

50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, daily, no spam.

Unsubscribe anytime. We never sell your data.

Eric Bang
Author

Founder & Cybersecurity Evangelist, Decryption Digest

Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals every morning.

Giveaway: InfoSec World 2026 All Access Pass ($3,895 value)

Details →
Daily Briefing

Subscribe to enter the giveaway

Every subscriber is automatically entered. You also get daily threat intel every morning: zero-days, ransomware, and nation-state campaigns. Free. No spam.

Already subscribed? You're already entered.

Giveaway

Win a $3,895 InfoSec World 2026 pass.