Skip to main content

Azure Federated Identity Credentials

Configure Azure for secretless authentication from external systems like GitHub Actions, GitLab CI, and Buildkite using OIDC without storing client secrets.

Overview​

Azure's Workload Identity Federation enables external systems to authenticate using OIDC tokens instead of client secrets or certificates.

Authentication Flow​

  1. External system generates OIDC token
  2. Token sent to Azure AD/Entra ID
  3. Azure validates issuer, subject, and audience
  4. Azure issues access token (valid ~1 hour)
  5. Access token used for Azure resources

Configuration Requirements​

Every federated credential requires five parameters:

ParameterDescriptionRequirements
NameUnique identifier3-120 characters, alphanumeric/dash/underscore
IssuerExternal identity provider URLMust match token iss claim
SubjectExternal workload identifierMust match token sub claim (max 600 chars)
AudienceAcceptable token audiencesRecommended: api://AzureADTokenExchange
DescriptionOptional contextMax 600 characters

Setup Methods​

Using Azure CLI​

Create Service Principal​

# Create app registration
appId=$(az ad app create --display-name "github-oidc-app" --query appId -otsv)

# Create service principal
az ad sp create --id $appId

Add Federated Credential​

# Create credential JSON
cat <<EOF > credential.json
{
"name": "github-actions-prod",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:myorg/myrepo:environment:Production",
"audiences": ["api://AzureADTokenExchange"],
"description": "GitHub Actions production environment"
}
EOF

az ad app federated-credential create --id $appId --parameters credential.json

Using Terraform​

# Create Azure AD Application
resource "azuread_application" "github_app" {
display_name = "github-actions-app"
}

# Create Service Principal
resource "azuread_service_principal" "github_sp" {
client_id = azuread_application.github_app.client_id
}

# Add Federated Identity Credential
resource "azuread_application_federated_identity_credential" "github_prod" {
application_id = azuread_application.github_app.id
display_name = "github-actions-production"
description = "Production environment"
audiences = ["api://AzureADTokenExchange"]
issuer = "https://token.actions.githubusercontent.com"
subject = "repo:myorg/myrepo:environment:Production"
}

# Assign RBAC role
resource "azurerm_role_assignment" "contributor" {
scope = azurerm_resource_group.example.id
role_definition_name = "Contributor"
principal_id = azuread_service_principal.github_sp.object_id
}

Subject Patterns​

GitHub Actions​

Entity TypeSubject Pattern
Environmentrepo:<org>/<repo>:environment:<name>
Branchrepo:<org>/<repo>:ref:refs/heads/<branch>
Tagrepo:<org>/<repo>:ref:refs/tags/<tag>
Pull Requestrepo:<org>/<repo>:pull_request

Note: Standard credentials don't support wildcards. Each branch/environment needs separate credential.

GitLab CI​

ScenarioSubject Pattern
Specific branchproject_path:<group>/<project>:ref_type:branch:ref:<branch>
Specific tagproject_path:<group>/<project>:ref_type:tag:ref:<tag>

RBAC Configuration​

Assign Roles​

# Resource group level
az role assignment create \
--assignee $appId \
--role "Contributor" \
--scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP"

# Specific resource
az role assignment create \
--assignee $appId \
--role "Storage Blob Data Contributor" \
--scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RG/providers/Microsoft.Storage/storageAccounts/$STORAGE"
Use CaseRoleScope
Infrastructure deploymentContributorResource Group
Container deploymentAcrPushACR
Static site deploymentStorage Blob Data ContributorStorage Account
Secrets accessKey Vault Secrets UserKey Vault

Security Best Practices​

1. Use Specific Subjects​

Prefer environment-based:

repo:org/repo:environment:Production

Over branch-based:

repo:org/repo:ref:refs/heads/main

2. Least Privilege RBAC​

Assign roles at narrowest scope:

# Specific resource > Resource group > Subscription
az role assignment create \
--assignee $appId \
--role "Reader" \
--scope "/subscriptions/$SUB_ID/resourceGroups/$RG/providers/Microsoft.Compute/virtualMachines/$VM"

3. Use GitHub Environments​

Configure protection rules:

  • Required reviewers
  • Wait timers
  • Allowed branches

4. Monitor Sign-ins​

View in Entra ID → Sign-in logs → Service principal sign-ins

Key metrics:

  • Failed authentication attempts
  • Sign-ins from unexpected locations
  • Token exchange failures

5. Enable Diagnostic Settings​

az monitor diagnostic-settings create \
--name "entra-sign-ins" \
--resource "/tenants/$TENANT_ID/providers/Microsoft.aadiam" \
--workspace "/subscriptions/$SUB_ID/resourceGroups/$RG/providers/Microsoft.OperationalInsights/workspaces/$WORKSPACE" \
--logs '[{"category":"ServicePrincipalSignInLogs","enabled":true}]'

Troubleshooting​

AADSTS70021: No matching federated identity record found​

Causes:

  1. Subject claim mismatch
  2. Issuer URL mismatch
  3. Audience mismatch
  4. Missing trailing slash (for some issuers)

Solution:

# Verify configuration
az ad app federated-credential show --id $appId --federated-credential-id "cred-name"

# Check exact values match token claims

AADSTS90061: External OIDC endpoint request failed​

Causes:

  1. OIDC issuer not accessible from internet
  2. GitLab instance behind firewall

Solution:

  • Ensure /.well-known/openid-configuration is publicly accessible
  • Test: curl https://your-issuer.com/.well-known/openid-configuration

AADSTS700016: Application not found​

Causes:

  1. Incorrect client ID
  2. Wrong tenant ID

Solution:

# Verify application exists
az ad app show --id $CLIENT_ID

# Check tenant ID
az account show --query tenantId -o tsv

Limitations​

  • Maximum 20 federated credentials per application/managed identity
  • No wildcard support in standard credentials (use Flexible FIC preview for patterns)
  • Case-sensitive matching of issuer, subject, audience
  • Replication delays of 2-5 minutes after creation

Complete Example​

#!/bin/bash
set -e

APP_NAME="github-actions-oidc"
GITHUB_ORG="myorg"
GITHUB_REPO="myrepo"
RG="production-rg"
SUB_ID="your-subscription-id"

# Create app and SP
APP_ID=$(az ad app create --display-name "$APP_NAME" --query appId -otsv)
az ad sp create --id $APP_ID

# Add federated credentials
for env in Production Staging Development; do
cat <<EOF > ${env}-cred.json
{
"name": "github-${env,,}",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:${GITHUB_ORG}/${GITHUB_REPO}:environment:${env}",
"audiences": ["api://AzureADTokenExchange"]
}
EOF
az ad app federated-credential create --id $APP_ID --parameters ${env}-cred.json
done

# Assign RBAC
az role assignment create \
--assignee $APP_ID \
--role "Contributor" \
--scope "/subscriptions/${SUB_ID}/resourceGroups/${RG}"

# Get IDs for GitHub secrets
TENANT_ID=$(az account show --query tenantId -otsv)

echo "Add these to GitHub secrets:"
echo "AZURE_CLIENT_ID: $APP_ID"
echo "AZURE_TENANT_ID: $TENANT_ID"
echo "AZURE_SUBSCRIPTION_ID: $SUB_ID"

Next Steps​

Additional Resources​