Use templates for security
Azure DevOps Services | Azure DevOps Server 2022 | Azure DevOps Server 2020
This article describes how templates can streamline security for Azure Pipelines. Templates can define the outer structure of your pipeline and help prevent malicious code infiltration. Templates can also automatically include steps to do tasks such as credential scanning. If multiple pipelines within your team or organization share the same structure, consider using templates.
Checks on protected resources form the fundamental security framework for Azure Pipelines. These checks apply regardless of pipeline structure, stages, and jobs. You can use templates to help enforce these checks.
Includes and extends templates
Azure Pipelines provides includes and extends templates.
Includes templates include the template's code directly in the outer file that references it, similar to
#include
in C++. The following example pipeline inserts the include-npm-steps.yml template into thesteps
section.steps: - template: templates/include-npm-steps.yml
Extends templates define the outer structure of the pipeline and offer specific points for targeted customizations. In the context of C++,
extends
templates resemble inheritance.
When you use extends
templates, you can also use includes
in both the template and the final pipeline to do common configuration pieces. For a complete reference, see the Template usage reference.
Extends templates
For the most secure pipelines, start by using extends templates. These templates define the outer structure of the pipeline and prevent malicious code from infiltrating the pipeline.
For example, the following template file is named template.yml.
parameters:
- name: usersteps
type: stepList
default: []
steps:
- ${{ each step in parameters.usersteps }}:
- ${{ step }}
The following pipeline extends the template.yml template.
# azure-pipelines.yml
resources:
repositories:
- repository: templates
type: git
name: MyProject/MyTemplates
ref: refs/tags/v1
extends:
template: template.yml@templates
parameters:
usersteps:
- script: echo This is my first step
- script: echo This is my second step
Tip
When you set up extends
templates, consider anchoring them to a particular Git branch or tag so if there are breaking changes, existing pipelines aren't affected. The preceding example uses this feature.
YAML pipeline security features
The YAML pipeline syntax includes several built-in protections. Extends template can enforce their use. To enhance pipeline security, you can implement any of the following restrictions.
Step targets
You can restrict certain steps to run in a container rather than on the host. Steps in containers don't have access to the agent's host, preventing these steps from modifying agent configuration or leaving malicious code for later execution.
For example, consider limiting network access. Without open network access, user steps can't retrieve packages from unauthorized sources or upload code and secrets to external network locations.
The following example pipeline runs steps on the agent host before running steps inside a container.
resources:
containers:
- container: builder
image: mysecurebuildcontainer:latest
steps:
- script: echo This step runs on the agent host, and it could use Docker commands to tear down or limit the container's network
- script: echo This step runs inside the builder container
target: builder
Agent logging command restrictions
You can restrict the services the Azure Pipelines agent provides to user steps. User steps request services by using logging commands, which are specially formatted strings printed to standard output. In restricted mode, most of the agent's services, such as uploading artifacts and attaching test results, are unavailable.
The following example task fails because its target
property instructs the agent not to allow publishing artifacts.
- task: PublishBuildArtifacts@1
inputs:
artifactName: myartifacts
target:
commands: restricted
In restricted
mode, the setvariable
command remains permissible, so caution is necessary because pipeline variables are exported as environment variables to subsequent tasks. If tasks output user-provided data, such as open issues retrieved via a REST API, they might be vulnerable to injection attacks. Malicious user content could set environment variables that might be exploited to compromise the agent host.
To mitigate this risk, pipeline authors can explicitly declare which variables are settable by using the setvariable
logging command. When you specify an empty list, all variable setting is disallowed.
The following example task fails because the task is only allowed to set the expectedVar
variable or a variable prefixed with ok
.
- task: PowerShell@2
target:
commands: restricted
settableVariables:
- expectedVar
- ok*
inputs:
targetType: 'inline'
script: |
Write-Host "##vso[task.setvariable variable=BadVar]myValue"
Conditional stage or job execution
You can restrict stages and jobs to run only under specific conditions. In the following example, the condition ensures that restricted code builds only for the main branch.
jobs:
- job: buildNormal
steps:
- script: echo Building the normal, unsensitive part
- ${{ if eq(variables['Build.SourceBranchName'], 'refs/heads/main') }}:
- job: buildMainOnly
steps:
- script: echo Building the restricted part that only builds for main branch
Syntax modification
Azure Pipelines templates have the flexibility to iterate over and modify YAML syntax. By using iteration, you can enforce specific YAML security features.
A template can also rewrite user steps, allowing only approved tasks to run. For example, you can prevent inline script execution.
The following example template prevents the step types bash
, powershell
, pwsh
, and script
from running. For complete lockdown of ad-hoc scripts, you could also block BatchScript
and ShellScript
.
# template.yml
parameters:
- name: usersteps
type: stepList
default: []
steps:
- ${{ each step in parameters.usersteps }}:
- ${{ if not(or(startsWith(step.task, 'Bash'),startsWith(step.task, 'CmdLine'),startsWith(step.task, 'PowerShell'))) }}:
- ${{ step }}
# The following lines replace tasks like Bash@3, CmdLine@2, PowerShell@2
- ${{ else }}:
- ${{ each pair in step }}:
${{ if eq(pair.key, 'inputs') }}:
inputs:
${{ each attribute in pair.value }}:
${{ if eq(attribute.key, 'script') }}:
script: echo "Script removed by template"
${{ else }}:
${{ attribute.key }}: ${{ attribute.value }}
${{ elseif ne(pair.key, 'displayName') }}:
${{ pair.key }}: ${{ pair.value }}
displayName: 'Disabled by template: ${{ step.displayName }}'
In the following pipeline that extends this template, the script steps are stripped out and not run.
# azure-pipelines.yml
extends:
template: template.yml
parameters:
usersteps:
- task: MyTask@1
- script: echo This step will be stripped out and not run!
- bash: echo This step will be stripped out and not run!
- powershell: echo "This step will be stripped out and not run!"
- pwsh: echo "This step will be stripped out and not run!"
- script: echo This step will be stripped out and not run!
- task: CmdLine@2
displayName: Test - Will be stripped out
inputs:
script: echo This step will be stripped out and not run!
- task: MyOtherTask@2
Type-safe parameters
Before a pipeline runs, templates and their parameters are transformed into constants. Template parameters can enhance type safety for input parameters.
In the following example template, the parameters restrict the available pipeline pool options by providing an enumeration of specific choices instead of allowing freeform strings.
# template.yml
parameters:
- name: userpool
type: string
default: Azure Pipelines
values:
- Azure Pipelines
- private-pool-1
- private-pool-2
pool: ${{ parameters.userpool }}
steps:
- script: # ... removed for clarity
When the pipeline extends the template, it has to specify one of the available pool choices.
# azure-pipelines.yml
extends:
template: template.yml
parameters:
userpool: private-pool-1
Template steps
A template can automatically include steps in a pipeline. These steps can do tasks such as credential scanning or static code checks. The following template inserts steps before and after the user steps in every job.
parameters:
jobs: []
jobs:
- ${{ each job in parameters.jobs }}:
- ${{ each pair in job }}:
${{ if ne(pair.key, 'steps') }}:
${{ pair.key }}: ${{ pair.value }}
steps:
- task: CredScan@1
- ${{ job.steps }}
- task: PublishMyTelemetry@1
condition: always()
Template enforcement
Templates are a valuable security mechanism, but their effectiveness relies on enforcement. The key control points for enforcing template usage are protected resources. You can configure approvals and checks for your agent pool or other protected resources such as repositories. For an example, see Add a repository resource check.
Required templates
To enforce the use of a specific template, configure the required template check for a resource. This check applies only when the pipeline extends from a template.
When you view the pipeline job, you can monitor the check's status. If the pipeline doesn't extend from the required template, the check fails. The run stops and notifies you of the failed check.
When you use the required template, the check passes.
The following params.yml template must be referenced in any pipeline that extends it.
# params.yml
parameters:
- name: yesNo
type: boolean
default: false
- name: image
displayName: Pool Image
type: string
default: ubuntu-latest
values:
- windows-latest
- ubuntu-latest
- macOS-latest
steps:
- script: echo ${{ parameters.yesNo }}
- script: echo ${{ parameters.image }}
The following example pipeline extends the params.yml template and requires it for approval. To demonstrate a pipeline failure, comment out the reference to params.yml.
# azure-pipeline.yml
resources:
containers:
- container: my-container
endpoint: my-service-connection
image: mycontainerimages
extends:
template: params.yml
parameters:
yesNo: true
image: 'windows-latest'