Skip to content

Azure OIDC

Use OpenID Connect (OIDC) when a GitHub Actions workflow needs to authenticate to Azure without storing a client secret in GitHub. This approach allows GitHub to issue a short-lived identity token to the workflow, and Azure validates that token before granting access to the configured resources.

This is the preferred approach for workflows that need to deploy to Azure, read from Azure Key Vault, or call Azure-managed services from GitHub Actions.

For Azure’s reference documentation, see Authenticate GitHub Actions workflows to Azure using OpenID Connect.

Use Azure OIDC for GitHub Actions when a workflow needs to:

  • Deploy infrastructure or applications into Azure
  • Read secrets from Azure Key Vault during CI/CD
  • Avoid storing long-lived Azure credentials in GitHub
  • Restrict Azure access to a specific repository and GitHub environment

Before configuring OIDC, make sure you have:

  • Access to the target Azure subscription, tenant, and resource scope
  • Permission to create or manage an Azure app registration and service principal
  • Permission to configure GitHub Actions for the target repository
  • The Azure CLI installed locally if you are following the CLI examples below
  • Environment is set up in the target GitHub repository

1. Create the App Registration and Service Principal

Section titled “1. Create the App Registration and Service Principal”

Create an Azure app registration for the repository and environment:

Terminal window
az ad app create --display-name github-sp-<repo-name>-<environment-name>

Then, create the service principal for the app using the application client id generated in the previous step:

Terminal window
az ad sp create --id <app-client-id>

The application client ID from this step is used later by the GitHub Actions workflow.

Grant the service principal only the permissions it actually needs, and scope the role assignment as narrowly as possible.

For example, if the workflow only needs to read secrets from a specific Key Vault:

Terminal window
az role assignment create \
--assignee <app-client-id> \
--role "Key Vault Secrets User" \
--scope /subscriptions/<sub-id>/resourceGroups/<rg-name>/providers/Microsoft.KeyVault/vaults/<vault-name>

This example assumes the Key Vault uses Azure RBAC, which is the recommended permission model. If the vault still uses legacy Key Vault access policies, grant the service principal get permission for secrets there instead of assigning an Azure RBAC role.

If the workflow needs access to other Azure resources, assign the appropriate role at the resource group, subscription, or resource level instead of granting broader permissions by default.

3. Add a Federated Credential for the GitHub Repository

Section titled “3. Add a Federated Credential for the GitHub Repository”

Create a file named federated-credential.json:

federated-credential.json
{
"name": "github-<environment-name>-environment",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:<org-name>/<repo-name>:environment:<environment-name>",
"description": "GitHub Actions for <environment-name> environment",
"audiences": ["api://AzureADTokenExchange"]
}

Then, create the federated credential on the app registration:

Terminal window
az ad app federated-credential create \
--id <app-client-id> \
--parameters federated-credential.json

The subject value limits which GitHub workflow context can exchange a token with Azure. In this example, only jobs running in the specified GitHub environment for the named repository are trusted.

GitHub environment protection rules remain an important part of the access model. If you need to restrict which branches, tags, or approvals can deploy through that environment, configure those controls on the GitHub environment itself. The Azure federated credential trusts the environment identity, while GitHub environment rules determine which workflow runs are allowed to use that environment.

If the repository deploys to multiple environments such as Staging and Production, create a separate federated credential for each environment instead of widening access unnecessarily.

Make the Azure identifiers available to the workflow through GitHub Actions configuration:

  • AZURE_CLIENT_ID
  • AZURE_TENANT_ID
  • AZURE_SUBSCRIPTION_ID
  • AZURE_KEYVAULT_NAME

Store AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_SUBSCRIPTION_ID as GitHub Actions environment secrets. Store AZURE_KEYVAULT_NAME as a GitHub Actions environment variable so the same workflow can target a different vault per environment without changing the workflow file.

5. Use OIDC in the GitHub Actions Workflow

Section titled “5. Use OIDC in the GitHub Actions Workflow”

The workflow must request permission to mint an OIDC token by setting id-token: write.

Example workflow for a single secret:

name: Use Key Vault secret with OIDC
on:
workflow_dispatch:
push:
branches:
- main
permissions:
contents: read
id-token: write
jobs:
run:
runs-on: ubuntu-latest
environment: Staging
steps:
- name: Check out repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd #v6.0.2
with:
fetch-depth: 0
- name: Log in to Azure with OIDC
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 #v3.0.0
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Load Key Vault secret
id: keyvault
shell: bash
env:
AZURE_CORE_OUTPUT: none
run: |
value="$(az keyvault secret show \
--vault-name "${{ vars.AZURE_KEYVAULT_NAME }}" \
--name "api-key-name" \
--query value \
--output tsv \
--only-show-errors)"
echo "::add-mask::$value"
echo "my_api_key=$value" >> "$GITHUB_OUTPUT"
- name: Use the secret in another step
env:
MY_API_KEY: ${{ steps.keyvault.outputs.my_api_key }}
run: |
curl -H "Authorization: Bearer $MY_API_KEY" https://example.internal/api/health

If a job needs multiple secrets, keep the same pattern but fetch only the explicit secret names required by that job:

- name: Load Key Vault secrets
id: keyvault
shell: bash
env:
AZURE_CORE_OUTPUT: none
run: |
declare -A SECRETS=(
["MY_API_KEY"]="api-key-name"
["MY_DB_PASSWORD"]="db-password-name"
["MY_BASE_URL"]="base-url-name"
)
for OUTPUT_NAME in "${!SECRETS[@]}"; do
value="$(az keyvault secret show \
--vault-name "${{ vars.AZURE_KEYVAULT_NAME }}" \
--name "${SECRETS[$OUTPUT_NAME]}" \
--query value \
--output tsv \
--only-show-errors)"
echo "::add-mask::$value"
delimiter="$(uuidgen)"
{
echo "${OUTPUT_NAME}<<${delimiter}"
echo "$value"
echo "${delimiter}"
} >> "$GITHUB_OUTPUT"
done
- name: Use the secrets
env:
MY_API_KEY: ${{ steps.keyvault.outputs.MY_API_KEY }}
MY_DB_PASSWORD: ${{ steps.keyvault.outputs.MY_DB_PASSWORD }}
MY_BASE_URL: ${{ steps.keyvault.outputs.MY_BASE_URL }}
run: |
curl -H "Authorization: Bearer $MY_API_KEY" "$MY_BASE_URL/health"

Prefer explicit secret mappings like this over loading every secret in the vault into the workflow environment. If a Key Vault secret contains multiline content such as a certificate or private key, prefer writing it to a file instead of passing it through a step output.

When using Azure OIDC with GitHub Actions:

  • Prefer OIDC over stored Azure client secrets
  • Grant the minimum role required for the workflow
  • Scope access to the smallest practical Azure resource boundary
  • Create separate federated credentials for separate repositories, branches, or environments
  • Keep deployment access for production workflows more restrictive than non-production workflows

This pattern reduces credential management overhead while improving traceability and access control for CI/CD automation.