3 min read
Workload Identity Federation: Keyless Authentication from Anywhere
Workload identity federation allows external systems to authenticate to Azure without storing secrets. This is a game-changer for CI/CD pipelines and multi-cloud scenarios.
How Federation Works
Instead of client secrets:
- External identity provider issues a token
- Token is exchanged for an Azure AD token
- Application uses Azure AD token to access resources
Setting Up Federation for GitHub Actions
// Create service principal and configure federation
resource appRegistration 'Microsoft.Graph/applications@v1.0' = {
displayName: 'GitHub Actions Deploy'
web: {
redirectUris: []
}
}
resource federatedCredential 'Microsoft.Graph/applications/federatedIdentityCredentials@v1.0' = {
name: 'github-main-branch'
parent: appRegistration
properties: {
name: 'github-main-branch'
issuer: 'https://token.actions.githubusercontent.com'
subject: 'repo:myorg/myrepo:ref:refs/heads/main'
audiences: ['api://AzureADTokenExchange']
}
}
Using Azure CLI:
# Create app registration
az ad app create --display-name "GitHub Actions Deploy"
APP_ID=$(az ad app list --display-name "GitHub Actions Deploy" --query "[0].appId" -o tsv)
# Create federated credential
az ad app federated-credential create \
--id $APP_ID \
--parameters '{
"name": "github-main-branch",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:myorg/myrepo:ref:refs/heads/main",
"audiences": ["api://AzureADTokenExchange"]
}'
# Create service principal
az ad sp create --id $APP_ID
# Grant permissions
az role assignment create \
--assignee $APP_ID \
--role "Contributor" \
--scope "/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP}"
GitHub Actions Workflow
# .github/workflows/deploy.yml
name: Deploy to Azure
on:
push:
branches: [main]
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Azure Login (OIDC)
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy Infrastructure
run: |
az deployment group create \
--resource-group myResourceGroup \
--template-file main.bicep
- name: Deploy Application
uses: azure/webapps-deploy@v2
with:
app-name: mywebapp
package: ./app
Federated Credentials for Different Scenarios
# For pull requests
az ad app federated-credential create \
--id $APP_ID \
--parameters '{
"name": "github-pull-requests",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:myorg/myrepo:pull_request",
"audiences": ["api://AzureADTokenExchange"]
}'
# For specific environment
az ad app federated-credential create \
--id $APP_ID \
--parameters '{
"name": "github-production-env",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:myorg/myrepo:environment:production",
"audiences": ["api://AzureADTokenExchange"]
}'
# For tags
az ad app federated-credential create \
--id $APP_ID \
--parameters '{
"name": "github-release-tags",
"issuer": "https://token.actions.githubusercontent.com",
"subject": "repo:myorg/myrepo:ref:refs/tags/v*",
"audiences": ["api://AzureADTokenExchange"]
}'
Azure DevOps OIDC
# azure-pipelines.yml
trigger:
- main
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureCLI@2
inputs:
azureSubscription: 'Azure-OIDC-Connection' # Service connection with OIDC
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az account show
az deployment group create \
--resource-group myResourceGroup \
--template-file main.bicep
Service connection configuration:
{
"name": "Azure-OIDC-Connection",
"type": "AzureRM",
"authorizationType": "WorkloadIdentityFederation",
"data": {
"subscriptionId": "xxx",
"subscriptionName": "My Subscription",
"servicePrincipalId": "xxx",
"tenantId": "xxx",
"creationMode": "Manual"
}
}
Kubernetes Workload Identity
# Service account with federated identity
apiVersion: v1
kind: ServiceAccount
metadata:
name: workload-identity-sa
namespace: default
annotations:
azure.workload.identity/client-id: "<CLIENT_ID>"
labels:
azure.workload.identity/use: "true"
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: myapp
spec:
template:
metadata:
labels:
azure.workload.identity/use: "true"
spec:
serviceAccountName: workload-identity-sa
containers:
- name: myapp
image: myregistry.azurecr.io/myapp:latest
env:
- name: AZURE_CLIENT_ID
value: "<CLIENT_ID>"
- name: AZURE_TENANT_ID
value: "<TENANT_ID>"
// Application code using workload identity
var credential = new DefaultAzureCredential();
// Works automatically with workload identity in Kubernetes
var blobClient = new BlobServiceClient(
new Uri("https://mystorage.blob.core.windows.net"),
credential);
Multi-Cloud Federation
# AWS to Azure federation
az ad app federated-credential create \
--id $APP_ID \
--parameters '{
"name": "aws-lambda-federation",
"issuer": "https://cognito-identity.amazonaws.com",
"subject": "arn:aws:iam::123456789012:role/MyLambdaRole",
"audiences": ["api://AzureADTokenExchange"]
}'
Workload identity federation eliminates the need for stored credentials, dramatically improving security posture.