adaptive.run TECH BLOG

Cloud can be tricky sometimes. Find out what scenarios we've ran into that are worth being mentioned and explained.

Configuring Azure DevOps Workload Identity Federation Using Azure Bicep

Level: 300
Publishing date: 24-Jan-2025 
Author: Catalin Popa

Integrating Azure DevOps service connections with workload identity federation significantly enhances security by eliminating the need for client secrets while enabling automated authentication through managed identities. Using Azure Bicep, this setup can be fully automated and easily maintained.

This guide covers:

      • How workload identity federation simplifies authentication
      • Overview of the deployment process using Bicep
      • Prerequisites for successful deployment
      • Detailed breakdown of the Bicep template, including deployment scripts
      • Enhancements for flexibility and scalability
_______________________________________________________________________________

Understanding Workload Identity Federation

Workload Identity Federation replaces client secret-based authentication with a trusted identity exchange mechanism using OpenID Connect (OIDC). Instead of relying on static secrets, Azure DevOps authenticates using a managed identity, which is:

More secure – No manual secret rotation required.
Easier to manage – No need to store credentials in pipelines.
Native to Azure AD – Uses OpenID Connect (OIDC) for token-based authentication.

For more details, check Microsoft’s documentation: Workload Identity Federation.
_______________________________________________________________________________

Deployment Overview: Setting Up Azure DevOps Service Connections via Bicep

Mobirise
azure.microsoft.com

The diagram illustrates the orchestration process for configuring workload identity federation through Azure Bicep using OpenID Connect. The workflow follows these steps:

      1. Deployment Initialization – An Azure Pipeline executes the deployment.
      2. Azure DevOps Metadata Retrieval – A deployment script gathers the instance ID of the                    Azure DevOps organization and project.
      3. Managed Identity Creation – A user-assigned managed identity is provisioned,                                incorporating the retrieved metadata.
      4. Role Assignment – The managed identity is granted Contributor permissions at the                          subscription level.
      5. Service Connection Creation – Another deployment script registers the managed identity                as a service connection in Azure DevOps.

These steps are automated through a Bicep template for streamlined configuration.
_______________________________________________________________________________

Prerequisites for Deployment

1. Injecting System.AccessToken into the Pipeline

The System.AccessToken allows the pipeline to authenticate against Azure DevOps securely. It must be explicitly mapped within the pipeline.

Pipeline Configuration

YAML

steps:
- task: AzureCLI@2
inputs:
azureSubscription: ''
scriptType: 'pscore'
scriptLocation: 'inlineScript'
inlineScript: "az deployment group create --template-file --parameters --resource-group "
env:
SYSTEM_ACCESSTOKEN: $(System.AccessToken) 

This loads the token as an environment variable, which is later retrieved in the Bicep template using:

Bicep

param parAzureDevOpsSystemAccessToken = readEnvironmentVariable('SYSTEM_ACCESSTOKEN')

2. Configuring Permissions in Azure DevOps

The Azure DevOps build service requires Endpoint Administrator permissions to create service connections.

This allows:

✔ Creation and management of service connections
✔ Enabling “Grant access permission to all pipelines”

These permissions must be configured before running the deployment.
_______________________________________________________________________________

Implementation

Main Bicep Template

@secure()
param parAzureDevOpsToken string
param parOrgName string
param parProjectName string
param parServiceConnName string
param parLocation string = 'westeurope'
param parDeploymentTime string = utcNow('yyyy-MM-dd-HH-mm-ss')

// Get Azure DevOps Information
resource resDevOpsInfo 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
name: 'get-devops-details'
location: parLocation
kind: 'AzurePowerShell'
properties: {
azPowerShellVersion: '11.0'
retentionInterval: 'P1D'
forceUpdateTag: parDeploymentTime
environmentVariables: [
{
name: 'DevOpsToken'
secureValue: parAzureDevOpsToken
  }
]
arguments: '-OrgName ${parOrgName} -ProjectName ${parProjectName} -ServiceConnName ${parServiceConnName}'
scriptContent: loadTextContent('scripts/get-devops-info.ps1')
  }
}

// Create Managed Identity
resource resManagedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: parServiceConnName
location: parLocation

resource resFederation 'federatedIdentityCredentials' = {
name: 'federation-config'
properties: {
issuer: resDevOpsInfo.properties.outputs.issuer
subject: resDevOpsInfo.properties.outputs.subjectIdentifier
audiences: ['api://AzureADTokenExchange']
    }
  }
}

// Configure Service Connection
} resource resConfigureConnection 'Microsoft.Resources/deploymentScripts@2020-10-01' = {
name: 'setup-service-connection'
location: parLocation
kind: 'AzurePowerShell'
properties: {
azPowerShellVersion: '11.0'
retentionInterval: 'P1D'
forceUpdateTag: parDeploymentTime
environmentVariables: [
{
name: 'DevOpsToken'
secureValue: parAzureDevOpsToken
  }
]
arguments: '-OrgName ${parOrgName} -ProjectName ${parProjectName} -ServiceConnName ${parServiceConnName} -TenantId ${tenant().tenantId} -SubId ${subscription().subscriptionId} -SubName ${subscription().displayName} -ManagedIdentityId ${resManagedIdentity.properties.clientId}'
scriptContent: loadTextContent('scripts/configure-connection.ps1')

PowerShell Scripts

Create two separate script files for better maintainability:
scripts/get-devops-info.ps1:

param (
[string] $OrgName,
[string] $ProjectName,
[string] $ServiceConnName
)

$auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($env:DevOpsToken)"))
$headers = @{ Authorization = "Basic $auth" }

$orgInfo = Invoke-RestMethod -Uri "https://dev.azure.com/$OrgName/_apis/connectiondata?api-version=5.0-preview.1" -Headers $headers
$orgId = $orgInfo.instanceId

$deploymentScriptOutputs = @{
issuer = "https://vstoken.dev.azure.com/$orgId"
subjectIdentifier = "sc://$OrgName/$ProjectName/$ServiceConnName"

scripts/configure-connection.ps1:

param (
[string] $OrgName,
[string] $ProjectName,
[string] $ServiceConnName,
[string] $TenantId,
[string] $SubId,
[string] $SubName,
[string] $ManagedIdentityId
)

$auth = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($env:DevOpsToken)"))
$headers = @{ Authorization = "Basic $auth" }

# Create service connection configuration
$config = @{
authorization = @{
parameters = @{
serviceprincipalid = $ManagedIdentityId
tenantid = $TenantId
}
scheme = "WorkloadIdentityFederation"
}
data = @{
environment = "AzureCloud"
scopeLevel = "Subscription"
subscriptionId = $SubId
subscriptionName = $SubName
}
name = $ServiceConnName
type = "AzureRM"
url = "https://management.azure.com/"
}

# Deploy service connection
$endpoint = Invoke-RestMethod -Uri "https://dev.azure.com/$OrgName/_apis/serviceendpoint/endpoints?api-version=7.1-preview.4" -Headers $headers -Method Post -Body ($config | ConvertTo-Json -Depth 10)

# Configure pipeline permissions
$permissions = @{
allPipelines = @{ authorized = $true }
pipelines = @()
resource = @{
id = $endpoint.id
type = "endpoint"
  }
}

Invoke-RestMethod -Uri "https://dev.azure.com/$OrgName/$ProjectName/_apis/pipelines/pipelinePermissions/endpoint/$($endpoint.id)?api-version=7.1-preview.1" -Headers $headers -Method Patch -Body ($permissions | ConvertTo-Json) 

Usage

      1. Deploy the Bicep template using an Azure Pipeline
      2. The deployment creates:
          o A managed identity with federation configuration
          o An Azure DevOps service connection using workload identity federation
          o Necessary role assignments

Security Considerations

      • The System.AccessToken is handled securely through environment variables
      • No client secrets are stored or need rotation
      • Service principal permissions are managed through Azure RBAC
      • Cross-project access should be carefully considered when granting permissions

Extensibility

The solution can be enhanced to:

      • Support multiple subscriptions
      • Configure service connections across different projects
      • Customize role assignments and permission levels
      • Integrate with existing infrastructure-as-code solutions

Remember to adapt the scripts and configurations to match your organization's security requirements and naming conventions.

Mobirise
adaptive.run

Transform your business.
Run adaptive.

Contact

Phone: +40 73 523 0005
Email: hello@adaptive.run

Mobirise Website Builder
Mobirise Website Builder

© Copyright  2019-2025 adaptive.run- All Rights Reserved