How Pipeline Secrets Become Liabilities
CI/CD pipelines regularly connect to cloud resources to deploy applications, push container images, and fetch runtime configuration. For a long time, the standard pattern relied on creating an Azure service principal, generating a client secret, and saving that secret as a pipeline variable.
Static secrets spread quickly across pipeline settings, build logs, and repository histories. They rarely expire quickly, and they lack binding to the specific pipeline job using them. When a credential leaks, detection often happens only after unauthorized activity has already occurred.
Workload Identity Federation removes stored secrets from the workflow. Instead of long-lived credentials, Azure Entra ID directly validates the pipeline during execution using OpenID Connect (OIDC).
How Pipeline Authentication Has Evolved
Stage 1: Username and Password
The earliest pattern relied on shared service accounts with static credentials stored in pipeline variables. Password rotations caused unexpected pipeline failures, auditing was difficult across teams, and a single compromised build agent risked exposing all systems accessible to that account.
Stage 2: Client Secrets and App Registrations
Azure App Registrations improved access control by binding permissions to a specific application identity. However, they still require generating and storing long-lived secrets in pipeline variables (such as AZURE_CLIENT_SECRET). These secrets require scheduled rotations, and Azure cannot verify whether the caller presenting the secret is your build agent or an attacker.
Stage 3: Azure Key Vault for Bootstrap Secrets
Fetching secrets from Azure Key Vault at runtime centralized secret storage and audit logging. However, the pipeline still needs an initial credential to access Key Vault in the first place, leaving the bootstrap secret problem unresolved.
Stage 4: Workload Identity Federation
Workload Identity Federation replaces static credentials with short-lived OIDC tokens generated by Azure DevOps for each job. Azure Entra ID validates the token against a configured trust relationship. If the organization, project, and pipeline match the federated credential, Entra ID grants access for that specific run.
| Approach | Stored Secret? | Rotation Required? | Pipeline-Bound? |
|---|---|---|---|
| Username/Password | Yes | Manual | No |
| Client Secret | Yes | Manual (1-2 years) | No |
| Key Vault + Bootstrap Secret | Yes | Partially automated | No |
| Workload Identity Federation | No | None | Yes |
How the OIDC Exchange Works
Azure DevOps acts as an OpenID Connect identity provider, signing JSON Web Tokens (JWTs) for active pipeline jobs. Azure Entra ID evaluates these tokens against configured trust policies:
- The pipeline job starts on an Azure DevOps agent.
- Azure DevOps generates a short-lived OIDC token signed with its private key.
- The pipeline task sends this token to the Azure Entra ID token endpoint.
- Entra ID validates the signature against Azure DevOps public keys fetched via the OIDC discovery document.
- Entra ID verifies that the token claims (issuer, subject, and audience) match the configured Federated Credential.
- Entra ID issues a short-lived Azure access token scoped to the app registration.
- The pipeline uses this access token to execute Azure CLI commands or resource deployments.
Because tokens expire within minutes and reflect exact pipeline run details, they cannot be reused outside their intended run context or replayed later.
Configuring Workload Identity Federation
You can set up federation manually through the Azure Portal and Azure DevOps, or let Azure DevOps configure resources automatically when creating a service connection. The manual steps show how each component connects.
Step 1: Register the Application in Entra ID
In the Azure Portal, go to Microsoft Entra ID → App registrations → New registration. Name the registration (such as sp-azuredevops-myproject) and leave the redirect URI blank.
Save the Application (client) ID and Directory (tenant) ID for the service connection setup.
Step 2: Assign Azure Roles
Grant the application registration permissions on your target subscription, resource group, or resource via Access Control (IAM) → Add role assignment.
Apply least privilege by scoping permissions tightly. For App Service deployments, assign Contributor at the resource group level. For Azure Container Registry, assign AcrPush directly to the registry.
# Assign a scoped role with Azure CLI
az role assignment create \
--assignee "<your-app-registration-object-id>" \
--role "Contributor" \
--scope "/subscriptions/<subscription-id>/resourceGroups/<resource-group-name>"
Step 3: Add a Federated Credential
Under your App Registration, open Certificates & secrets → Federated credentials → Add credential.
Select Azure DevOps as the scenario and specify:
- Organization: Your Azure DevOps organization name (e.g.,
mycompany) - Project: Your Azure DevOps project name
- Entity type:
Branch,Tag,Pull Request, orEnvironment - Ref: The Git ref (such as
mainorrefs/heads/main)
Entra ID uses these fields to construct the expected subject claim. A credential targeting mycompany/myproject/refs/heads/main only accepts requests initiated by the main branch of that specific project.
// Example federated credential configuration
{
"name": "azuredevops-myproject-main",
"issuer": "https://vstoken.dev.azure.com/<organization-id>",
"subject": "sc://mycompany/myproject/main",
"audiences": ["api://AzureADTokenExchange"]
}
Step 4: Create the Service Connection
In Azure DevOps, open Project Settings → Service connections → New service connection → Azure Resource Manager.
Select Workload Identity Federation (manual) and provide:
- Subscription ID and Subscription Name
- Service Principal Id: The Application (client) ID from Step 1
- Tenant ID: Your Directory (tenant) ID
When you save, Azure DevOps sends a test token request to Entra ID to verify that the federated credential matches.
Automatic Configuration Option
Selecting Workload Identity Federation (automatic) in Azure DevOps creates the App Registration, assigns roles, and configures the federated credential directly if your account has sufficient Azure permissions.
Pipeline Examples
Deploying to Azure App Service
trigger:
- main
pool:
vmImage: ubuntu-latest
variables:
azureSubscription: 'my-workload-identity-connection'
resourceGroupName: 'rg-myapp-prod'
appName: 'app-myapp-prod'
stages:
- stage: Deploy
jobs:
- job: DeployToAzure
steps:
- task: AzureCLI@2
displayName: 'Deploy to Azure App Service'
inputs:
azureSubscription: $(azureSubscription)
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az webapp deployment source config-zip \
--resource-group $(resourceGroupName) \
--name $(appName) \
--src ./app.zip
The AzureCLI@2 task requests the OIDC token, exchanges it for an Entra ID token, and authenticates the CLI session without reading static secret variables.
Building and Pushing Container Images
trigger:
branches:
include:
- main
- 'release/*'
pool:
vmImage: ubuntu-latest
variables:
azureSubscription: 'my-workload-identity-connection'
containerRegistry: 'mycompanyacr.azurecr.io'
imageRepository: 'myapp'
tag: $(Build.BuildId)
stages:
- stage: BuildAndPush
jobs:
- job: Build
steps:
- task: Docker@2
displayName: 'Build and push image'
inputs:
command: buildAndPush
repository: $(imageRepository)
dockerfile: '**/Dockerfile'
containerRegistry: $(azureSubscription)
tags: |
$(tag)
latest
Reading Key Vault Secrets Without Bootstrap Credentials
Federated service connections authenticate directly to Azure Key Vault, removing the need for an initial bootstrap secret in the pipeline.
steps:
- task: AzureKeyVault@2
displayName: 'Fetch application secrets'
inputs:
azureSubscription: 'my-workload-identity-connection'
KeyVaultName: 'kv-myapp-prod'
SecretsFilter: 'DatabaseConnectionString,ApiKey,SigningCertificate'
RunAsPreJob: true
- script: |
echo "Deploying with fetched configuration..."
displayName: 'Deploy application'
Restricting Deployments with Environment Checks
Federated credentials can bind directly to Azure DevOps Environments. This ensures only pipeline executions targeting a specific environment (and satisfying its approvals or verification checks) can acquire production tokens.
stages:
- stage: Production
jobs:
- deployment: DeployProduction
environment: production
strategy:
runOnce:
deploy:
steps:
- task: AzureCLI@2
displayName: 'Deploy to Production'
inputs:
azureSubscription: 'production-workload-identity-connection'
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
az webapp deploy --resource-group rg-prod \
--name app-prod --src-path ./release.zip
Because the credential expects the production environment claim, runs triggered outside that target fail token validation at the Entra ID boundary.
Operational and Security Practices
Narrow Credential Scopes
Create distinct federated credentials for different branches and environments:
mainbranch for production releasesrefs/heads/release/*for staging deployments- Pull requests for read-only validation jobs
productionenvironment for gated release stages
Separate App Registrations Across Environments
Use distinct App Registrations for development, staging, and production. Isolating identities prevents misconfigurations in lower environments from impacting production resources.
Enforce Least Privilege
Scope role assignments to specific resource groups or resources rather than entire subscriptions.
# Resource-scoped assignment
az role assignment create \
--assignee "<object-id>" \
--role "AcrPush" \
--scope "/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.ContainerRegistry/registries/<acr-name>"
Review Sign-in Logs
Workload identity authentications appear under Microsoft Entra ID → Sign-in logs → Service principal sign-ins. Send these logs to Azure Monitor or your SIEM to track which pipelines request tokens and observe failures.
Converting Existing Service Connections
Existing Azure DevOps service connections using secret keys can be updated in place:
- Open Project Settings → Service connections.
- Select your service connection and click Edit.
- For connections created with the automatic option, click Convert to switch authentication to workload identity federation.
- For manual connections, add the federated credential in Entra ID and update the connection type in Azure DevOps settings.
Converting existing connections preserves existing role assignments while removing secret expiration alerts and rotation schedules.
Troubleshooting Common Errors
AADSTS70021: No matching federated identity record found
The subject claim in the pipeline token does not match any federated credential on the App Registration. Verify that:
- The organization name matches your Azure DevOps organization casing exactly.
- The project name matches.
- The entity type and ref match the Git reference format (such as
refs/heads/mainversusmain).
AADSTS700016: Application not found in directory
The Application (client) ID in the service connection does not match an App Registration in the targeted Entra ID tenant.
AuthorizationFailed: Client does not have authorization
The token exchange succeeded, but the App Registration lacks the required Azure RBAC role on the target resource. Check role assignments and allow a few minutes for new assignments to propagate.
Service Connection Verification Fails During Setup
Check the issuer URL format in your federated credential. Azure DevOps expects https://vstoken.dev.azure.com/<organization-id>, where the organization ID is the GUID found under Organization Settings → Overview.
Reference Links
- Microsoft: Create an Azure Resource Manager Service Connection using Workload Identity Federation
- Microsoft Entra: Workload Identity Federation Overview
- Microsoft: Configure Workload Identity in Azure Pipelines
- OpenID Connect Core Specification
- Microsoft Entra: Create a Federated Identity Credential