Use service connection approvals to elevate Azure DevOps deployment security

In DevOps, the focus is primarily on automation and having dependable deployments. However, sometimes human intervention is necessary, particularly for deployments to critical environments like production or other business-critical systems. Azure DevOps has some measures that you can configure to implement these controls through approvals and checks. Azure DevOps has some measures that you can configure to implement these controls through approvals and checks.

These approvals and checks can be configured in multiple places within an Azure DevOps organisation, such as in environments and on service connections. The latter is the way to go.

In this blog, you will learn about how to configure approvals and checks for service connections and the security benefits over using environments.

Approval and checks

When approvals are configured, the deployment is paused at each point where the approval is configured and only continues when the assigned approver or approver group grants approval. An approval request can also be rejected so that the release stops.

In Azure DevOps, it is possible to configure approvals and checks on:

  • An environment
  • A service connection

In the Azure Pipeline, the environment is configured directly in the pipeline (as code) with the environment property:

- stage: deploy
jobs:
- deployment: DeployMyApp
displayName: deploy MyApp
pool:
vmImage: 'windows-latest'
environment:
name: 'my-environment-name'
view raw environment.yml hosted with ❤ by GitHub

For service connections, it is simpler. Let’s see why!

What are service connections?

In Azure DevOps, service connections are used to connect to external services, like Azure or other external services. These service connections have an authentication context, so they are connected and authorised to an external service. A service connection can handle the deployment and configuration of resources in Azure or any other external service, depending on the permissions within Azure.

Configure approvals on service connections

Let’s configure an approval on a service connection in Azure DevOps.

Permissions (project level)

To configure an approval on a service connection you need the following permission:

Step-by-step configuration

Let’s configure an approval on a service connection. Go to your Azure DevOps project settings, on this screen:

  1. Go to Pipelines > Service connections
  2. On the right, the service connections will appear. Go to the service connection you want to configure approvals and checks on.

3. After selecting the service connection go to the approvals and checks tab

4. Select approvals

5. On the Approval configuration pane add either a specific user of an Azure Entra ID security group. You can add instructions and make sure to set Allow approvers to approve their own runs to false. At last, click on the create button.

These are the steps that have to be taken to configure approvals on service connections. Now, each time the service connection is used Azure DevOps asks for an approval of the specified user or security group.

Using the service connection

Let’s see how the approval works in practice. You are probably wondering what happens when the service connection is used multiple times or what happens when multiple service connections with approvals are used in a pipeline.

You will see the following scenarios:

  • Scenario when a service connection is used over multiple jobs.
  • Scenario when multiple “approval configured” service connections are used within a job.
  • Scenario when a service connection is used over multiple stages.

Scenario 1: Multiple jobs and tasks

In this scenario, we have a pipeline that has two jobs with each one AzureCLI task. The service connection configured is azure-sc-prod and requires an approval when used. This service connection is configured on each AzureCLI task:

trigger: none
pool:
vmImage: "windows-latest"
jobs:
- job: 'JobOne'
displayName: 'JobOne'
steps:
- task: AzureCLI@2
inputs:
azureSubscription: "azure-sc-prod"
scriptType: "ps"
scriptLocation: "inlineScript"
inlineScript: "az --version"
displayName: "Azure CLI Task"
- job: 'JobTwo'
dependsOn: 'JobOne'
displayName: 'JobTwo'
steps:
- task: AzureCLI@2
inputs:
azureSubscription: "azure-sc-prod"
scriptType: "ps"
scriptLocation: "inlineScript"
inlineScript: "az --version"
displayName: "Azure CLI Task2"
view raw scenario1.yml hosted with ❤ by GitHub

When the pipeline triggers it asks for an approval before the run can continue:

When you press the view button the approval pane pops up. Here you can give a comment, which is optional, and there are two buttons: approval and reject. Also, Azure DevOps doesn’t ask for two approvals. In this context, the approval only needs to happen once even if you use the service connection multiple times:

Scenario 2: Multiple “approval configured” service connections

In this scenario, there is a pipeline that has two jobs and each task within the jobs has its own purpose: one for infrastructure deployment and one for deployments to Azure Entra ID. Each AzureCLI task uses its specified service connection: azure-sc-prod and azure-sc-prod-entra. Both of the service connections are configured with approvals.

trigger: none
pool:
vmImage: "windows-latest"
jobs:
- job: 'JobOne'
displayName: 'Infrastructure deployment'
steps:
- task: AzureCLI@2
inputs:
azureSubscription: "azure-sc-prod"
scriptType: "ps"
scriptLocation: "inlineScript"
inlineScript: "az --version"
displayName: "Azure CLI Task"
- job: 'JobTwo'
dependsOn: 'JobOne'
displayName: 'Entra ID deployment'
steps:
- task: AzureCLI@2
inputs:
azureSubscription: "azure-sc-prod-entra"
scriptType: "ps"
scriptLocation: "inlineScript"
inlineScript: "az --version"
displayName: "Azure CLI Task2"
view raw scenario2.yml hosted with ❤ by GitHub

When the pipeline triggers it asks for 2 approvals, because each service connection requires an approval:

When you press the view button the approval pane pops up. Azure DevOps detects both service connections and combines the approval, so only one approval using the approval all button for both deployments is enough:

Scenario 3: Multiple stages

In this scenario, there is a pipeline which has two stages. These stages both use different service connections with each containing one or more jobs using the AzureCLI task. The defined service connections are configured with approvals.

trigger: none
pool:
vmImage: "windows-latest"
stages:
- stage: StageOne
displayName: "sc-prod"
jobs:
- job: "JobOne"
displayName: "JobOne"
steps:
- task: AzureCLI@2
inputs:
azureSubscription: "azure-sc-prod"
scriptType: "ps"
scriptLocation: "inlineScript"
inlineScript: "az --version"
displayName: "Azure CLI Task"
- job: "JobTwo"
dependsOn: "JobOne"
displayName: "JobTwo"
steps:
- task: AzureCLI@2
inputs:
azureSubscription: "azure-sc-prod"
scriptType: "ps"
scriptLocation: "inlineScript"
inlineScript: "az --version"
displayName: "Azure CLI Task2"
- stage: StageTwo
displayName: "sc-prod-entra"
jobs:
- job: "JobThree"
displayName: "JobThree"
steps:
- task: AzureCLI@2
inputs:
azureSubscription: "azure-sc-prod-entra"
scriptType: "ps"
scriptLocation: "inlineScript"
inlineScript: "az --version"
displayName: "Azure CLI Task2"
view raw scenario3.yml hosted with ❤ by GitHub

When the pipeline triggers each stage requires an approval:

When deploying stages in parallel the approvals must also be given for each stage:

Security benefits over using environments

Using approvals and checks on service connections is a recommended approach, mostly because of security reasons. When using Azure DevOps environments, you configure the approvals and checks within the Azure Pipeline. This is where the problem is because it is possible to remove the environment from the code in a feature branch and bypass the approvals and checks. This way it is possible to have faulty or malicious code being deployed or run into your production environment. Moreover, it is harder to monitor that this bypass happens.

By using approvals and checks on service connections you can overcome these security issues and also implement an audit.

The image below illustrates the approval flow when the environment property is used in an Azure DevOps pipeline. It shows what happens via the main branch, where the environment property is present and it shows the flow of what happens when a user creates a feature branch and removes the environment property from the pipeline. As you can see potential unwanted or malicious code bypasses the approval step completely.

The approval flow when the user removes the environment property

Let’s see how the approval flow goes in the image below when using approvals on service connections. In the image below, you see the same two branches and both trigger the pipeline to the production Azure environment. The difference is that the approval flow from the service connection is used instead and the user is not able to deploy without an approval from the feature branch.

The approval flow when approval and checks are configured on service connections

Audit removal of approvals and checks

Approvals and checks are part of keeping your production environment secure, thus having an audit is important, especially when approvals and checks are either disabled or deleted. This means that unwanted production deployments can happen. Azure DevOps has a built-in audit stream that can be pushed to a Log Analytics Workspace, Splunk Instance, or an Azure Event Grid. This opens up possibilities to couple audit logs to a Security Information and Event Management system like Azure Sentinel.

Here is an example of an audit record in Azure DevOps regarding a removal action of an approval:

Audit log record in Azure DevOps

Log Analytics Workspace DevOps Table

The Log Analytics Workspace has a specific table for Azure DevOps AzureDevOpsAuditing. This table holds the auditing data generated in your Azure DevOps organisation.

The Kusto query below queries the AzureDevOpsAuditing table for either removed or disabled approvals and checks on service connections:

AzureDevOpsAuditing
| where Area == "Checks"
| where Category == "Remove" or Category == 'Disabled'
| project TimeGenerated, ActorDisplayName, Category, ProjectName, Details, Data
| order by TimeGenerated desc

This (can, when logged) generate the following output:

Output of the KQL query

Conclusion

In conclusion, using service connections for approval flows in Azure DevOps has security benefits. By configuring it on service connections it ensures that deployments to production or other sensitive environments are monitored, and controlled and cannot be bypassed. It also prevents unauthorised or accidental deployments that could disrupt services or expose sensitive data.

If approvals on service connections fit your use-case I highly recommend to setup service connection approvals to enhance the safety and reliability of your deployments.

Microsoft documentation regarding this topic

One thought on “Use service connection approvals to elevate Azure DevOps deployment security

Leave a comment