PAYATHON 2026

如何在本地 Azure DevOps Server 流水线中接入 HashiCorp Vault?

数据阿明

可以。在本地 Azure DevOps Server 2022.2 的流水线中,执行任务的 agent 可以通过 Vault HTTP API 或 CLI 完成认证并读取 secrets,再把所需的值提供给后续任务。这个过程可以用脚本实现,无需依赖所提到的第三方扩展。

可以先尝试使用 AppRole 登录 Vault,获取短期 Vault token,读取指定路径的密钥,再通过 Azure Pipelines 的秘密变量传递给同一 job 中的后续步骤。AppRole 用于机器和应用认证,默认通过 role_id 和 secret_id 换取 token。Vault AppRole 文档

为什么不能直接照搬云端指南?

问题中引用的指南依赖 Azure DevOps service connection 的 workload identity federation,并使用相应身份向 Vault 认证。如果本地环境无法完成这些配置,可以选择 AppRole 等认证方式。

接入时要分别解决两个问题:agent 如何向 Vault 证明身份,以及认证成功后允许读取哪些 secrets。Vault 的认证方法和访问策略负责这两部分,流水线脚本负责调用接口和传递结果。云端指南中的 WIF 流程只是其中一条接入路径。HashiCorp 集成指南

将现有 Library 的名称或地址改成 Vault,变量组也不会自动读取 Vault。使用脚本方案时,需要显式增加读取步骤。

接入前需要准备什么?

先确认以下条件:

  1. 执行流水线的 agent 能访问 Vault,且信任 Vault 的 HTTPS 证书。如果使用内部 CA,应将其加入 agent 的信任配置。
  2. Vault 管理员已启用 AppRole,并为对应流水线建立角色和访问策略。
  3. 已确定密钥所在的 secrets engine、挂载路径、密钥路径及字段名。
  4. 已安排 AppRole 初始凭据的安全交付与轮换方式。

例如,假设使用 KV v2,挂载点为 secret,密钥路径为 ci/myapp,其中有一个 password 字段。只允许读取该条密钥的策略可以写成:

path "secret/data/ci/myapp" {
  capabilities = ["read"]
}

将该策略绑定到流水线使用的 AppRole。KV v2 的数据读取 API 路径包含 /data/;KV v1 的路径和响应结构不同,不能直接套用下面的示例。上述策略授权读取整条密钥记录,并不会把权限进一步限制到 password 这个字段。KV v2 API 文档

如何在流水线中读取并传递 secrets?

下面的示例使用 Linux agent、Bash 和 Python 3 标准库,不需要安装 Vault CLI。示例假设:

  • agent pool 名称为 SelfHostedLinux,需要替换为实际名称。
  • 已建立变量组 vault-bootstrap,并授权目标流水线使用。
  • 变量组包含 VAULT_ADDR、VAULT_ROLE_ID、VAULT_SECRET_ID,其中 VAULT_SECRET_ID 必须设为秘密变量。
  • VAULT_ADDR 类似 https://vault.example.internal:8200,不包含 /v1。
  • AppRole 挂载在默认的 auth/approle,未使用 Vault namespace。
  • 仓库中的 deploy.sh 会从环境变量 APP_PASSWORD 读取密码;实际使用时替换为项目自己的任务。
pool:
  name: SelfHostedLinux

variables:
- group: vault-bootstrap

steps:
- bash: |
    set +x
    set -euo pipefail
    python3 - <<'PY'
    import json
    import os
    import urllib.request

    base = os.environ["VAULT_ADDR"].rstrip("/")
    if not base.startswith("https://"):
        raise ValueError("VAULT_ADDR must use HTTPS")

    def request_json(method, path, payload=None, token=None):
        headers = {"Content-Type": "application/json"}
        if token is not None:
            headers["X-Vault-Token"] = token
        body = None if payload is None else json.dumps(payload).encode("utf-8")
        request = urllib.request.Request(
            base + "/v1/" + path,
            data=body,
            headers=headers,
            method=method,
        )
        with urllib.request.urlopen(request, timeout=30) as response:
            return json.load(response)

    login = request_json("POST", "auth/approle/login", {
        "role_id": os.environ["VAULT_ROLE_ID"],
        "secret_id": os.environ["VAULT_SECRET_ID"],
    })
    token = login["auth"]["client_token"]
    result = request_json("GET", "secret/data/ci/myapp", token=token)
    password = result["data"]["data"]["password"]
    if not isinstance(password, str) or not password:
        raise ValueError("Expected a non-empty password string")

    escaped = password.replace("%", "%AZP25")
    escaped = escaped.replace("\r", "%0D").replace("\n", "%0A")
    print("##vso[task.setvariable variable=APP_PASSWORD;issecret=true]" + escaped)
    PY
  displayName: Read secret from Vault
  env:
    VAULT_ADDR: $(VAULT_ADDR)
    VAULT_ROLE_ID: $(VAULT_ROLE_ID)
    VAULT_SECRET_ID: $(VAULT_SECRET_ID)

- bash: |
    set +x
    set -euo pipefail
    ./deploy.sh
  displayName: Deploy
  env:
    APP_PASSWORD: $(APP_PASSWORD)

登录使用 POST /v1/auth/approle/login,返回的 auth.client_token 用于后续请求。AppRole API 文档

读取步骤通过 task.setvariable 将值标记为秘密变量。秘密变量不会自动成为每个任务的环境变量,后续任务需要通过 env 显式接收。示例还对日志命令中的百分号和换行符做了转义。Azure Pipelines logging commands

使用 Vault 后,还需要在 Library 中保存凭据吗?

如果采用上面的 AppRole 方案,并通过变量组交付 secret_id,仍需要保存一份用于首次认证的凭据。初始信任问题需要单独处理。

业务密码可以迁移到 Vault,Library 暂时只保存受限的认证凭据。后续可根据基础设施条件,改为由可信系统按次交付 SecretID,或使用短期 response-wrapping token。Response wrapping 能减少 SecretID 在交付过程中的直接暴露,但仍需要安排谁签发、谁交付以及何时交付。AppRole 凭据交付说明

不要为了省去这一步,把 root token 或高权限长期 token 放进流水线。

多条流水线接入时有哪些注意事项?

  • 按项目和环境划分 AppRole、策略与 agent 使用权限,避免所有流水线共享能够读取全部 secrets 的身份。
  • 为 Vault token 和 SecretID 分别配置合适的有效期与使用次数。一次性 SecretID 需要每次登录前重新交付,不能长期放在变量组中重复使用。示例未主动撤销 token,因此必须设置适当的 token TTL。
  • 示例变量用于同一 job 的后续步骤。跨 job 或 stage 时,需要配置输出变量及依赖关系,或者让每个 job 分别向 Vault 认证并读取所需值。
  • 禁止脚本跟踪和直接打印 secrets,也不要把它们写入构建产物。日志脱敏不能遮盖所有派生值或子字符串;能修改流水线脚本的人,也可能利用脚本读取任务可访问的密钥。
  • 如果使用动态数据库凭据等带租约的 secrets,还需要安排租约续期、撤销及使用时长。读取 KV 静态密码的示例没有覆盖这些生命周期操作。

可以先选一条流水线和一条受限密钥完成接入,再把认证、读取及变量设置步骤整理成共享 YAML 模板,供其他流水线复用。

备注:内容仅供参考。