Back to Blog
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:

  1. External identity provider issues a token
  2. Token is exchanged for an Azure AD token
  3. 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.

Michael John Peña

Michael John Peña

Senior Data Engineer based in Sydney. Writing about data, cloud, and technology.