The Problem with Client Secrets in CI/CD
The traditional way to authenticate GitHub Actions to Azure involves creating a service principal, generating a client secret, and storing it in GitHub Secrets. This works, but it has real problems:
- Client secrets expire (default 2 years, often set to 1 year). When they expire, your pipelines break silently on the next run.
- The secret exists as a static string in GitHub. Anyone with repo admin access can read it.
- If the secret leaks, an attacker can authenticate as your service principal from anywhere until you notice and revoke it.
Workload identity federation solves all of this. Instead of a stored secret, GitHub Actions presents a short-lived OIDC token that Azure verifies directly. No secrets are stored anywhere.
How OIDC Federation Works
The flow has three participants:
- GitHub issues a signed OIDC token for every workflow run. The token contains claims about the repository, branch, and workflow.
- Entra ID has a federated credential configured on the app registration that trusts tokens from GitHub's OIDC provider, but only for your specific repo and branch.
- azure/login action exchanges the GitHub token for an Azure access token using the federated credential. Your workflow can then call Azure APIs.
The token is short-lived (valid for the duration of the workflow job), scoped to the specific repo/branch, and never stored anywhere.
Step 1: Create the App Registration and Federated Credential
You can set this up with the Azure CLI:
The subject claim is where you control which workflows can authenticate. Some common patterns:
- repo:org/repo:ref:refs/heads/main allows only the main branch
- repo:org/repo:environment:production allows only the "production" GitHub Environment
- repo:org/repo:pull_request allows pull request workflows (useful for plan/preview steps)
Step 2: Assign Azure Roles
Scope the role to the narrowest level possible. Contributor on a specific resource group is better than Contributor on the entire subscription.
Step 3: Configure the GitHub Actions Workflow
Add three secrets to your GitHub repository (Settings > Secrets > Actions): AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_SUBSCRIPTION_ID. Note: none of these are actual secrets. They are just identifiers.
The key parts:
- permissions.id-token: write tells GitHub to generate an OIDC token for this workflow
- No client-secret field in the azure/login step. The action detects OIDC mode automatically when only client-id, tenant-id, and subscription-id are provided.
Terraform with OIDC
If you prefer to manage the app registration and federated credential as infrastructure, here's the Terraform equivalent:
Old Way vs New Way
Here's what you can remove from your setup after migrating to OIDC:
- Before: App registration + client secret + AZURE_CREDENTIALS JSON in GitHub Secrets + secret rotation process + monitoring for expiry
- After: App registration + federated credential + three non-secret IDs in GitHub. No expiry, no rotation, no stored credentials.
The migration is straightforward: create the federated credential, update the workflow file, remove the old AZURE_CREDENTIALS secret from GitHub, and delete the client secret from the app registration.
Further Reading
For more details, see the workload identity federation documentation and GitHub Actions OIDC with Azure.