使用模板实现安全性

Azure DevOps Services | Azure DevOps Server 2022 | Azure DevOps Server 2020

本文介绍了模板如何简化 Azure Pipelines 的安全性。 模板可以定义管道的外部结构,并帮助防止恶意代码渗透。 模板还可以自动包括执行凭据扫描等任务的步骤。 如果团队或组织内的多个管道共享相同的结构,请考虑使用模板。

检查受保护的资源 构成了 Azure Pipelines 的基本安全框架。 无论管道结构、阶段和作业如何,这些检查都适用。 可以使用模板来帮助强制实施这些检查。

包括和扩展模板

Azure Pipelines 提供扩展模板。

  • 包括模板直接在引用模板的外部文件中包括模板的代码,类似于 #include C++。 以下示例管道将 include-npm-steps.yml 模板插入该 steps 部分。

      steps:
      - template: templates/include-npm-steps.yml 
    
  • 扩展模板定义管道的外部结构,并为目标自定义项提供特定点。 在C++上下文中, extends 模板类似于继承。

使用 extends 模板时,还可以在 includes 模板和最终管道中使用以执行常见配置部分。 有关完整参考,请参阅 模板使用情况参考

扩展模板

对于最安全的管道,首先使用扩展模板。 这些模板定义管道的外部结构,并防止恶意代码渗透管道。

例如,以下模板文件命名 为template.yml

parameters:
- name: usersteps
  type: stepList
  default: []
steps:
- ${{ each step in parameters.usersteps }}:
  - ${{ step }}

以下管道扩展 template.yml 模板。

# 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

提示

设置 extends 模板时,请考虑将它们定位到特定的 Git 分支或标记,因此,如果有中断性变更,现有管道不会受到影响。 前面的示例使用此功能。

YAML 管道安全功能

YAML 管道语法包括多个内置保护。 扩展模板可以强制使用。 若要增强管道安全性,可以实施以下任何限制。

步骤目标

可以限制在容器中运行的某些步骤,而不是在主机上运行。 容器中的步骤无法访问代理的主机,阻止这些步骤修改代理配置或留下恶意代码供以后执行。

例如,请考虑限制网络访问。 如果没有开放网络访问权限,用户步骤无法从未经授权的源检索包,也无法将代码和机密上传到外部网络位置。

以下示例管道在容器中运行步骤之前,在代理主机上运行步骤。

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

代理日志记录命令限制

可以限制 Azure Pipelines 代理提供给用户步骤的服务。 用户步骤使用 日志记录命令请求服务,这些命令是打印到标准输出的特制字符串。 在受限模式下,大多数代理的服务(如上传项目和附加测试结果)都不可用。

下面的示例任务失败,因为它 target 的属性指示代理不允许发布项目。

- task: PublishBuildArtifacts@1
  inputs:
    artifactName: myartifacts
  target:
    commands: restricted

restricted 模式下, setvariable 命令仍然允许,因此需要谨慎,因为管道变量作为环境变量导出到后续任务。 如果任务输出用户提供的数据(例如通过 REST API 检索的开放问题),则它们可能容易受到注入攻击。 恶意用户内容可以设置可能被利用的环境变量来入侵代理主机。

为了缓解此风险,管道作者可以使用日志记录命令显式声明哪些变量是可设置的 setvariable 。 指定空列表时,不允许所有变量设置。

下面的示例任务失败,因为仅允许该任务设置变量 expectedVar 或前缀为 ok变量的变量。

- task: PowerShell@2
  target:
    commands: restricted
    settableVariables:
    - expectedVar
    - ok*
  inputs:
    targetType: 'inline'
    script: |
      Write-Host "##vso[task.setvariable variable=BadVar]myValue"

条件阶段或作业执行

可以限制阶段和作业仅在特定条件下运行。 在下面的示例中,该条件可确保限制的代码仅针对主分支生成。

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

语法修改

Azure Pipelines 模板可以灵活地循环访问和修改 YAML 语法。 通过使用迭代,可以强制实施特定的 YAML 安全功能。

模板还可以重写用户步骤,仅允许已批准的任务运行。 例如,可以阻止内联脚本执行。

以下示例模板阻止步骤类型、powershellpwsh步骤script类型和bash运行。 若要完全锁定即席脚本,还可以阻止 BatchScriptShellScript锁定。

# 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 }}'

在扩展此模板的以下管道中,脚本步骤将剥离出来,且未运行。

# 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

类型安全参数

在管道运行之前,模板及其参数将转换为常量。 模板参数 可以增强输入参数的类型安全性。

在以下示例模板中,参数通过提供特定选项的枚举来限制可用的管道池选项,而不是允许任意多边形字符串。

# 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

管道扩展模板时,必须指定可用池选项之一。

# azure-pipelines.yml
extends:
  template: template.yml
  parameters:
    userpool: private-pool-1

模板步骤

模板可以自动在管道中包含步骤。 这些步骤可以执行凭据扫描或静态代码检查等任务。 以下模板在每个作业中的用户步骤之前和之后插入步骤。

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()

模板强制实施

模板是一种有价值的安全机制,但其有效性依赖于强制实施。 强制实施模板使用的关键控制点是 受保护的资源。 可以配置代理池或其他受保护资源(例如存储库)的审批和检查。 有关示例,请参阅添加存储库资源检查

所需的模板

若要强制使用特定模板,请配置 资源的所需模板检查 。 此检查仅适用于管道从模板扩展时。

查看管道作业时,可以监视检查的状态。 如果管道未从所需的模板进行扩展,则检查将失败。 运行会停止并通知失败的检查。

显示审批检查失败的屏幕截图。

使用所需的模板时,检查将通过。

显示已通过审批检查的屏幕截图。

必须在任何扩展模板的管道中引用以下 params.yml 模板。

# 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 }}

以下示例管道扩展 params.yml 模板,并要求其批准。 若要演示管道故障,请注释掉对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'