Skip to main content

Challenge 41: Azure DevOps permissions and service connections

Exam skills covered​

  • Implement and manage Azure DevOps service connections and personal access tokens
  • Design and implement permissions and security groups in Azure DevOps
  • Recommend appropriate access levels
  • Configure projects and teams in Azure DevOps

Scenario​

Contoso Ltd's Azure DevOps organization has grown organically over three years. All 200 users are members of the Project Administrators group because "it was easier." Every pipeline uses a single service connection named Azure-All with Contributor access to the entire production subscription. The PAT used by the CI integration was created by a former employee with full scope and no expiration. You must redesign the permission model to implement least-privilege access.

Prerequisites​

  • Azure DevOps organization with Project Collection Administrator access
  • Azure subscription with Owner role
  • Azure CLI with azure-devops extension installed
  • At least one Azure DevOps project

Tasks​

Task 1: Design security groups (per-team, per-role)​

Create a group structure that maps to Contoso's team organization.

# Install the Azure DevOps CLI extension
az extension add --name azure-devops

# Set the default organization and project
az devops configure --defaults organization=https://dev.azure.com/contoso project=ContosoWeb

# Create team-specific security groups
az devops security group create \
--name "Contoso-Backend-Developers" \
--description "Backend development team - write access to backend repos" \
--project ContosoWeb

az devops security group create \
--name "Contoso-Frontend-Developers" \
--description "Frontend development team - write access to frontend repos" \
--project ContosoWeb

az devops security group create \
--name "Contoso-DevOps-Engineers" \
--description "Platform team - manage pipelines and service connections" \
--project ContosoWeb

az devops security group create \
--name "Contoso-Release-Managers" \
--description "Approve and manage production releases" \
--project ContosoWeb

az devops security group create \
--name "Contoso-Stakeholders" \
--description "View work items and dashboards only" \
--project ContosoWeb

Remove users from Project Administrators:

# List current Project Administrators members
az devops security group membership list \
--id "vstfs:///Classification/TeamProject/<project-id>\\Project Administrators"

# Remove a user from Project Administrators
az devops security group membership remove \
--group "vstfs:///Classification/TeamProject/<project-id>\\Project Administrators" \
--member-id <user-descriptor>

# Add user to the appropriate team group instead
az devops security group membership add \
--group "vstfs:///Classification/TeamProject/<project-id>\\Contoso-Backend-Developers" \
--member-id <user-descriptor>

Task 2: Configure project-level permissions​

Set granular permissions for each group.

# Get the security namespace IDs
az devops security permission namespace list --query "[].{name:name, id:namespaceId}" -o table

# Key namespace IDs:
# Build: 33344d9c-fc72-4d6f-aba5-fa317101a7e9
# Git Repositories: 2e9eb7ed-3c0a-47d4-87c1-0ffdd275fd87
# ReleaseManagement: c788c23e-1b46-4162-8f5e-d7585343b5de

# Grant build permissions to DevOps Engineers group
az devops security permission update \
--namespace-id 33344d9c-fc72-4d6f-aba5-fa317101a7e9 \
--subject <devops-engineers-group-descriptor> \
--token "<project-id>" \
--allow-bit 1535 \
--deny-bit 0

# Restrict developers to only queue builds (not edit pipelines)
az devops security permission update \
--namespace-id 33344d9c-fc72-4d6f-aba5-fa317101a7e9 \
--subject <backend-developers-group-descriptor> \
--token "<project-id>" \
--allow-bit 128 \
--deny-bit 0

Permission matrix for Contoso:

PermissionBackend DevsFrontend DevsDevOps EngineersRelease Managers
View buildsYesYesYesYes
Queue buildsYesYesYesYes
Edit build pipelinesNoNoYesNo
Create releasesNoNoYesYes
Approve releasesNoNoNoYes
Manage service connectionsNoNoYesNo
Edit project settingsNoNoYesNo

Task 3: Set up service connections with minimum permissions​

Replace the overly permissive Azure-All connection with scoped connections.

# Create a resource group per environment
az group create --name rg-contoso-dev --location eastus
az group create --name rg-contoso-staging --location eastus
az group create --name rg-contoso-prod --location eastus

# Create service principals scoped to each environment
az ad sp create-for-rbac \
--name "sp-ado-contoso-dev" \
--role "Contributor" \
--scopes "/subscriptions/<sub-id>/resourceGroups/rg-contoso-dev"

az ad sp create-for-rbac \
--name "sp-ado-contoso-staging" \
--role "Contributor" \
--scopes "/subscriptions/<sub-id>/resourceGroups/rg-contoso-staging"

az ad sp create-for-rbac \
--name "sp-ado-contoso-prod" \
--role "Contributor" \
--scopes "/subscriptions/<sub-id>/resourceGroups/rg-contoso-prod"

Create service connections in Azure DevOps:

# Create the development service connection
az devops service-endpoint azurerm create \
--name "Azure-Dev" \
--azure-rm-service-principal-id <dev-sp-app-id> \
--azure-rm-subscription-id <subscription-id> \
--azure-rm-subscription-name "Contoso Dev" \
--azure-rm-tenant-id <tenant-id>

# Create staging and production connections similarly
az devops service-endpoint azurerm create \
--name "Azure-Staging" \
--azure-rm-service-principal-id <staging-sp-app-id> \
--azure-rm-subscription-id <subscription-id> \
--azure-rm-subscription-name "Contoso Staging" \
--azure-rm-tenant-id <tenant-id>

az devops service-endpoint azurerm create \
--name "Azure-Prod" \
--azure-rm-service-principal-id <prod-sp-app-id> \
--azure-rm-subscription-id <subscription-id> \
--azure-rm-subscription-name "Contoso Production" \
--azure-rm-tenant-id <tenant-id>

Task 4: Federated service connections (workload identity)​

Create a service connection using workload identity federation to eliminate secrets.

# Create the app registration
az ad app create --display-name "sp-ado-contoso-federated"

APP_OBJECT_ID=$(az ad app list --display-name "sp-ado-contoso-federated" --query "[0].id" -o tsv)
APP_CLIENT_ID=$(az ad app list --display-name "sp-ado-contoso-federated" --query "[0].appId" -o tsv)

# Create the federated credential for the service connection
az ad app federated-credential create \
--id $APP_OBJECT_ID \
--parameters '{
"name": "ado-contoso-prod-connection",
"issuer": "https://vstoken.dev.azure.com/<org-guid>",
"subject": "sc://contoso/ContosoWeb/Azure-Prod-Federated",
"audiences": ["api://AzureADTokenExchange"]
}'

# Create service principal and assign role
az ad sp create --id $APP_CLIENT_ID
az role assignment create \
--assignee $APP_CLIENT_ID \
--role "Contributor" \
--scope "/subscriptions/<sub-id>/resourceGroups/rg-contoso-prod"

In Azure DevOps, create the service connection using "Workload Identity federation (manual)" and provide the app registration details.

Task 5: Pipeline permissions (restrict service connection usage)​

Restrict which pipelines can use each service connection.

# Get the service endpoint ID
ENDPOINT_ID=$(az devops service-endpoint list \
--query "[?name=='Azure-Prod'].id" -o tsv)

# Disable "Grant access permission to all pipelines"
az devops service-endpoint update \
--id $ENDPOINT_ID \
--enable-for-all false

Use environments with approval checks in the pipeline:

# azure-pipelines.yml
trigger:
branches:
include:
- main

stages:
- stage: Build
jobs:
- job: BuildApp
pool:
vmImage: 'ubuntu-latest'
steps:
- script: echo "Building application"

- stage: DeployProd
dependsOn: Build
jobs:
- deployment: DeployToProd
pool:
vmImage: 'ubuntu-latest'
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: AzureCLI@2
inputs:
azureSubscription: 'Azure-Prod-Federated'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
az group show --name rg-contoso-prod

Configure environment approvals:

  1. Pipelines > Environments > production > Approvals and checks
  2. Add "Approvals" with the Contoso-Release-Managers group
  3. Add "Branch control" to restrict to refs/heads/main only
  4. Add "Business hours" to prevent deployments outside working hours

Task 6: Access levels (Basic, Stakeholder, Visual Studio)​

Access levelCostKey capabilitiesBest for
StakeholderFreeView backlogs, create work items, view dashboards, view wikiProduct owners, managers, executives
BasicPaid (first 5 free)Full Boards, Repos, Pipelines, Test Plans (limited)Developers, testers, DevOps engineers
Basic + Test PlansPaid add-onFull Test Plans, test case managementDedicated QA engineers
Visual Studio subscriptionIncludedSame as Basic, included with VS Enterprise/ProfessionalDevelopers with VS subscriptions
# List current access level assignments
az devops user list --query "items[].{name:user.displayName, level:accessLevel.accountLicenseType}" -o table

# Change a user's access level to Stakeholder
az devops user update \
--user user@contoso.com \
--license-type stakeholder

# Add a new user with Basic access
az devops user add \
--email-id newdev@contoso.com \
--license-type express \
--send-email-invite true

Task 7: PAT lifecycle management and policies​

Configure organization-level PAT policies:

  1. Organization Settings > Policies (via web portal):
    • Restrict creation of full-scoped PATs: Enabled
    • Restrict creation of global PATs: Enabled (force project-scoped)
    • Enforce maximum PAT lifetime: 90 days
    • Restrict creation of PATs with admin scope: Enabled

PAT audit and revocation:

# Via REST API: List all PATs for auditing
curl -u :$PAT \
"https://vssps.dev.azure.com/contoso/_apis/tokens/pats?api-version=7.1-preview.1" \
| jq '.patTokens[] | {displayName, scope, validTo}'

# Revoke a specific PAT
curl -X DELETE -u :$ADMIN_PAT \
"https://vssps.dev.azure.com/contoso/_apis/tokens/pats?authorizationId=<pat-auth-id>&api-version=7.1-preview.1"

Create a scheduled pipeline that audits PAT usage:

# azure-pipelines.yml
schedules:
- cron: "0 8 * * 1"
displayName: Weekly PAT audit
branches:
include:
- main

pool:
vmImage: 'ubuntu-latest'

steps:
- task: PowerShell@2
inputs:
targetType: 'inline'
script: |
$headers = @{
Authorization = "Basic $([Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$(PAT_AUDIT_TOKEN)")))"
}
$uri = "https://vssps.dev.azure.com/contoso/_apis/tokens/pats?api-version=7.1-preview.1"
$response = Invoke-RestMethod -Uri $uri -Headers $headers
$expiringSoon = $response.patTokens | Where-Object {
$_.validTo -lt (Get-Date).AddDays(14) -and $_.validTo -gt (Get-Date)
}
if ($expiringSoon) {
Write-Host "##vso[task.logissue type=warning]PATs expiring within 14 days:"
$expiringSoon | ForEach-Object {
Write-Host " - $($_.displayName): expires $($_.validTo)"
}
}

Break & fix​

Break scenario 1: Pipeline fails with resource authorization error​

A developer creates a new pipeline and references the Azure-Prod service connection. The pipeline fails with "There was a resource authorization problem."

Cause: The service connection has "Grant access permission to all pipelines" disabled, and this pipeline has not been explicitly authorized.

Diagnosis: Check the pipeline run log for the authorization error. The UI will show a "Permit" button if you have sufficient permissions.

Show solution

Fix:

  1. Navigate to Project Settings > Service connections > Azure-Prod > Security
  2. Under "Pipeline permissions," click the "+" button and add the specific pipeline
  3. Alternatively, when the pipeline run shows the authorization prompt, click "Permit"

Break scenario 2: User cannot view work items after group restructuring​

After moving users from Project Administrators to team-specific groups, users report they cannot view work items.

Cause: The custom groups do not inherit from the "Contributors" built-in group, which has default read access to work items.

Show solution

Fix:

# Make the custom group a member of Contributors (inherits basic permissions)
az devops security group membership add \
--group "vstfs:///Classification/TeamProject/<project-id>\\Contributors" \
--member-id <custom-group-descriptor>

Knowledge check​

1. Contoso wants to ensure that only the Production-Deploy pipeline can use the 'Azure-Prod' service connection. What should you configure?

2. Contoso has 200 users: 150 developers, 30 product managers who only need work item access, and 20 executives who view dashboards. What is the most cost-effective access level assignment?

3. An organization policy requires all PATs to be project-scoped with a maximum lifetime of 90 days. Where should you configure these restrictions?

4. A team needs to deploy to both development and production environments from one pipeline. Production requires release manager approval. What is the correct configuration?

Cleanup​

# Delete service connections
az devops service-endpoint delete --id <endpoint-id> --yes

# Delete security groups
az devops security group delete --id <group-descriptor>

# Delete service principals
az ad sp delete --id $(az ad sp list --display-name "sp-ado-contoso-dev" --query "[0].id" -o tsv)
az ad sp delete --id $(az ad sp list --display-name "sp-ado-contoso-staging" --query "[0].id" -o tsv)
az ad sp delete --id $(az ad sp list --display-name "sp-ado-contoso-prod" --query "[0].id" -o tsv)
az ad app delete --id $APP_OBJECT_ID

# Delete resource groups
az group delete --name rg-contoso-dev --yes --no-wait
az group delete --name rg-contoso-staging --yes --no-wait
az group delete --name rg-contoso-prod --yes --no-wait