Setup managed identities for workload federation in Azure DevOps with the Azure CLI and PowerShell

This post will show how to create a user assigned managed identity, assign roles to it and configure it for workload identity federation using the Azure CLI in PowerShell.

Philipp Bauknecht
Published in
4 min readJan 17, 2024

--

Workload identity federation is the new recommended way of authorizing service connections in Azure DevOps to access Azure resources in pipelines. This approach offers increased security and reliabilty as the authentication is done using a user assigned managed identity.

Dialogue to create a new service connection in Azure DevOps for ARM

While it’s ok to use the automatic option to let Azure take care of the setup of the identity there are scenarios where you want to go the manual route. Either because you want full control of the identity setup (name, assigned roles etc.) or you simply have too many subscriptions so the automatic way doesn’t work.

First step is to create a new service connection in Azure DevOps, choose “Azure Resource Manager” ➡️ “Workload Identity federation (manual)” ➡️ choose any connection name and click “next”.

Leave the dialogue open at this point, we will complete the setup after we have created the identity. Take note of the issuer and subject identifier values.

Next up let’s switch to PowerShell. Make sure you have the Azure CLI installed and you are logged into your Azure subscription. First up we need to define some variables:

$subscriptionId = "{YOUR_SUBSCRIPTION_ID}"
$resourceGroupName = "{YOUR_RESOURCE_GROUP_NAME}"
$resourceGroupScope = "/subscriptions/$($subscriptionId)/resourcegroups/$($resourceGroupName)"
$identityName = "id-azuredevops"
$federatedCredentialName = "AzureDevOps"
$audience = "api://AzureADTokenExchange"
$issuerUrl = "{copy this value from the service connection draft in Azure DevOps}"
$subjectIdentifier = "{copy this value from the service connection draft in Azure DevOps}"

Based on these variables we can create a new used assigned managed identity and save the result of the operation as object:

$identity = az identity create --name $identityName --resource-group $resourceGroupName | ConvertFrom-Json

Note: A user assigned identity is created in a specific resource group rather than in Entra ID like a service principal

The identity needs to have permission to make changes to Azure. To create new Azure services in a specific resource group we need to add the “Contributor” role with scope for a resource group to the identity:

$contributorRoleId = "b24988ac-6180-42a0-ab88-20f7382dd24c"
az role assignment create --assignee $identity.principalId --role $contributorRoleId --scope $resourceGroupScope

Note: A full list of all built in roles in Azure with their ids can be found here: https://learn.microsoft.com/en-us/azure/role-based-access-control/built-in-roles

A common use case in my pipelines is that I run Bicep to deploy and configure my Azure resources and as part of that I’m doing role assignments in Bicep e.g. to grant an App Service access to a Key Vault. For this use case the “Contributor” role we just assigned to the identity is not enough but we also need the role “User Access Administrator”.

This role supports conditions to further restrict access and enforce security. Learn more about role assignment conditions here: What is Azure attribute-based access control (Azure ABAC)? | Microsoft Learn.

So let’s create a new role assignment condition where we restrict the role “User Access Administrator” to be only allowed to assign certain roles to other identities and assign the role to the identity:

$userAccessAdministratorRoleId = "18d7d88d-d35e-4fb5-a5c3-7773c20a72d9"

# Allowed roles to be managed (e.g. assigned) by the pipeline identity to other managed identities e.g. other Azure resource
$allowedRoles = @(
"4633458b-17de-408a-b874-0445c86b69e6", # Key Vault Secrets User > Various Azure resources need to read secrets
"ba92f5b4-2d11-453d-a403-e96b0029c9fe" # Storage Blob Data Contributor > Various Azure resources need to access blob storage
)

# Condition to make sure identity can only assign the allowed roles to other service principals
$userAccessAdministratorRoleCondition = ("
(
(
!(ActionMatches{'Microsoft.Authorization/roleAssignments/write'})
)
OR
(
@Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {$($allowedRoles -join ", ")}
AND
@Request[Microsoft.Authorization/roleAssignments:PrincipalType] ForAnyOfAnyValues:StringEqualsIgnoreCase {'ServicePrincipal'}
)
)
AND
(
(
!(ActionMatches{'Microsoft.Authorization/roleAssignments/delete'})
)
OR
(
@Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {$($allowedRoles -join ", ")}
AND
@Resource[Microsoft.Authorization/roleAssignments:PrincipalType] ForAnyOfAnyValues:StringEqualsIgnoreCase {'ServicePrincipal'}
)
)
").replace("`n", "")
az role assignment create --assignee $identity.principalId --role $userAccessAdministratorRoleId --scope $resourceGroupScope --condition $userAccessAdministratorRoleCondition

The final configuration is the federated credential in the identity to allow the service connection in Azure DevOps to use the identity:

$federatedCredentialName = "AzureDevOps"
$audience = "api://AzureADTokenExchange"
$issuerUrl = "{copy this value from the service connection draft in Azure DevOps}"
$subjectIdentifier = "{copy this value from the service connection draft in Azure DevOps}"
az identity federated-credential create --name $federatedCredentialName --identity-name $identityName --resource-group $resourceGroupName --issuer $issuerUrl --subject $subjectIdentifier --audiences $audience

The user assigned managed identity is now ready to be used. We need some information of the identity to complete the setup dialogue in Azure DevOps. For convenience we can output all the required information in PowerShell:

$account = az account show | ConvertFrom-Json
$serviceConnection = New-Object -TypeName psobject
$serviceConnection | Add-Member NoteProperty -Name SubscriptionId -Value $account.id
$serviceConnection | Add-Member NoteProperty -Name SubscriptionName -Value $account.name
$serviceConnection | Add-Member NoteProperty -Name ServicePrincipalId -Value $identity.clientId
$serviceConnection | Add-Member NoteProperty -Name TenantId -Value $identity.tenantId
$serviceConnection | ConvertTo-Json

Copy & paste these values back into the dialogue and we’re done!

Conclusion

With this script we have repeatable (e.g. different environments), secure and robust way of settings up a connection between Azure DevOps and Azure Resource Manager for deployments. This connection has fine grained access control using explicit role assignments and role condition assignments.

--

--

Philipp Bauknecht

CEO @ medialesson. Microsoft Regional Director & MVP Windows Development. Father of identical twins. Passionate about great User Interfaces, NYC & Steaks