原文:GitLab 文档贡献者,Use HashiCorp Vault secrets in GitLab CI/CD。原页面没有个人署名。本文为获授权的中文翻译整理,译编:未完纪;对原页中的 issuer 写法和一处 YAML 层级问题作了明确修订。
GitLab CI/CD 可以在作业执行前,通过短期 ID token 向 HashiCorp Vault 证明身份,再读取作业需要的秘密。这样可以把凭据集中在 Vault 中管理,同时用项目、命名空间、受保护引用和 audience 限制作业能够访问的范围。
本文介绍的是 GitLab 内置的 Vault secrets 集成。源文标注适用于 Premium、Ultimate,支持 GitLab.com、GitLab Self-Managed 和 GitLab Dedicated;配置 Vault 还需要相应管理权限与可访问的服务。核对日期为 2026 年 10 月 5 日,源页是持续更新的文档,未固定 GitLab 或 Runner 的单一版本。部署前应核对所用 GitLab、Runner、Vault 与 secrets engine 的兼容要求。

先配置 Vault 的 JWT 认证
在作业中使用 Vault 密钥前,需要先配置 Vault 服务。示例中的 vault.example.com 应换成自己的 Vault 地址,gitlab.example.com 应换成自己的 GitLab 实例地址。
先启用 JWT 认证方法,再提供 GitLab 的 OIDC Discovery URL,让 Vault 能取得公开签名密钥并验证 JSON Web Token。下面是针对标准 GitLab ID token 的修订示例,属于管理员配置命令:
vault auth enable jwt
vault write auth/jwt/config \
oidc_discovery_url="https://gitlab.example.com" \
bound_issuer="https://gitlab.example.com"
与原文的差异:主页面仍把 bound_issuer 写为 gitlab.example.com,省略了协议。GitLab 的 ID token 迁移教程明确要求,从旧 CI_JOB_JWT 迁移时更新 issuer;标准 ID token 的 iss 包含 https://。因此本稿将它修正为带协议的值。实际配置必须与可信 token 的真实 iss 精确对应,不能为解决不匹配而关闭 issuer 校验。
这些命令会修改 Vault 的认证配置。已有 jwt 挂载点时,应检查现有配置和使用者,不应在不了解影响范围的情况下重建或覆盖。本次没有连接 Vault,也没有执行命令。
用策略限制可读取的路径
Vault 策略决定令牌能够对哪些路径执行哪些操作。原文下面的策略允许读取生产环境路径下的秘密:
vault policy write myproject-production - <<'EOF'
path "ops/data/production/*" {
capabilities = ["read"]
}
EOF
路径中的 ops 是 secrets engine 的挂载名;对于 KV v2,策略使用的 API 数据路径含有 /data/。read 不授予写入权限,但结尾的 * 会覆盖整个子树。如果作业只需要单个秘密,应把策略缩小到实际所需路径。
编辑说明:本稿把原文未加引号的 here-document 分隔符 EOF 改为 'EOF',防止 shell 对输入内容做变量和命令替换。当前示例不含需要展开的变量,因此不改变它的预期策略内容。这并不能替代对动态路径和外部输入的审查。
把角色约束到项目和受保护的发布引用
作业认证时会指定一个 Vault 角色。角色组合所需策略,并为认证成功后签发的 Vault token 设置约束。Bound claims 是预先声明的匹配条件:它们与 JWT 的声明值比较;配置了多个条件时,必须全部满足。
下面的角色只允许项目 42 中、受保护且名称匹配 auto-deploy-* 的标签作业认证,并把 Vault token 的显式最大 TTL 限制为 60 秒:
vault write auth/jwt/role/myproject-production - <<'EOF'
{
"role_type": "jwt",
"policies": ["myproject-production"],
"token_explicit_max_ttl": 60,
"user_claim": "user_email",
"bound_audiences": "https://vault.example.com",
"bound_claims_type": "glob",
"bound_claims": {
"project_id": "42",
"ref_protected": "true",
"ref_type": "tag",
"ref": "auto-deploy-*"
}
}
EOF
这里 bound_claims_type: "glob" 使标签规则按 glob 模式匹配。项目编号、受保护状态、引用类型和名称共同构成边界,不能只保留其中的标签名称模式。应始终使用 project_id、namespace_id 等声明把角色限制到预期项目或命名空间;否则,该 GitLab 实例签发的其他 JWT 也可能有机会使用这个角色。
GitLab 用户角色、受保护分支或标签与 Vault 的声明匹配需要一起设计。受保护标签上的作业并不会自动让所有相关脚本可信,还需要审查谁能修改流水线、触发作业和使用 Runner。Vault 角色也能设置 token 的存活时间、来源 IP 范围、使用次数等属性,完整参数以 JWT 角色 API 为准。
bound_audiences 要与下文作业 ID token 的 aud 匹配。补充文档指出,Vault 1.17 及以后对含 audience 的 JWT 要求角色配置 bound audience,且至少匹配一个值。不要把 issuer、audience 与项目约束混为一谈:issuer 表示签发者,audience 表示预期接收方,项目等声明决定哪类作业可以通过。
提供 Runner 需要的 Vault 参数
接着在 GitLab 中配置相关 CI/CD 变量:
| 变量 | 含义与默认行为 |
|---|---|
VAULT_SERVER_URL |
Vault 服务 URL,例如 https://vault.example.com:8200。 |
VAULT_AUTH_ROLE |
可选,认证使用的角色。未指定时,Vault 使用认证方法配置中的默认角色。生产配置宜显式指定经过审查的角色。 |
VAULT_AUTH_PATH |
可选,认证方法的挂载路径,默认 jwt。 |
VAULT_NAMESPACE |
可选,读取秘密与认证使用的 Vault Enterprise 命名空间。未指定时通常使用根命名空间 /;Vault 开源版本忽略该设置。HCP Vault 需要指定命名空间,默认根命名空间为 admin,例如 VAULT_NAMESPACE=admin。 |
这些是服务位置和认证上下文,不应把长期 Vault 管理令牌硬编码在 .gitlab-ci.yml 中。作业所需的认证材料由 GitLab ID token 流程提供。
在 CI/CD 作业中读取一个秘密字段
作业至少定义一个 ID token 后,secrets 集成可以使用该 token 向 Vault 认证。下面显式指定使用哪个 token,避免多个 ID token 时产生歧义:
job_using_vault:
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.example.com
secrets:
DATABASE_PASSWORD:
vault: production/db/password@ops
token: $VAULT_ID_TOKEN
这是合并到既有作业中的认证与秘密配置片段,省略了业务 script 或其他执行定义,不是独立完整的部署流水线。其中:
production/db是秘密的逻辑路径。password是要读取的字段。ops是 secrets engine 的挂载路径。- 使用默认 KV v2 时,
production/db/password@ops对应读取ops/data/production/db,再取得其中的password字段。 token: $VAULT_ID_TOKEN指定认证所用 ID token。
默认情况下,GitLab 读取秘密后把值保存到临时文件,再把文件路径放进名为 DATABASE_PASSWORD 的 CI/CD 变量。这与 file 类型变量类似;直接把变量内容当成数据库密码,会把路径误当成秘密值。
如果应用要求环境变量中直接是秘密值,可以使用 file: false:
job_using_vault:
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.example.com
secrets:
DATABASE_PASSWORD:
vault: production/db/password@ops
file: false
token: $VAULT_ID_TOKEN
与原文的差异:源页的第二个 YAML 片段把 id_tokens 放在了 secrets 之下。id_tokens 应是作业级关键字,与 secrets 同级;上面已按第一段配置及 GitLab ID token 文档修正层级。没有对 GitLab 实例运行 CI Lint。
秘密暴露边界:file: false 会使变量直接包含秘密,业务脚本应避免输出它、启用 shell 跟踪或把它放进命令行参数。文件模式也不代表绝对安全:不要把临时文件或其内容收入日志、缓存和构建产物,且应限制运行相同作业环境的主体。
选择不同的 secrets engine
默认使用 KV v2。Runner 还支持下列 engine 名称,具体可用性要与部署中的 Runner 和 Vault 插件版本核对:
| Secrets engine | 配置中的 engine name | 说明 |
|---|---|---|
| KV v2 | kv-v2 |
没有显式指定时使用的默认引擎。 |
| KV v1 | kv-v1 或 generic |
路径结构与 KV v2 不同。 |
| AWS secrets engine | generic |
通过通用 engine 配置读取。 |
| HashiCorp Vault Artifactory Secrets Plugin | generic |
与 JFrog Artifactory 5.0.0 或更新服务交互,动态提供具有指定范围的访问令牌。 |
要使用其他引擎,在 vault 下写明 engine。以下是原文 Artifactory 示例;本稿增加显式 token 引用,其余字段保持原意:
job_using_vault:
id_tokens:
VAULT_ID_TOKEN:
aud: https://vault.example.com
secrets:
JFROG_TOKEN:
vault:
engine:
name: generic
path: artifactory
path: production/jfrog
field: access_token
file: false
token: $VAULT_ID_TOKEN
这个配置从 artifactory/production/jfrog 取得 access_token 字段。不要把 KV v2 自动增加的 /data/ 路径规则机械套用到所有引擎。
排查证书信任问题
Vault 使用未被 Runner 信任的自签名证书时,作业可能在初始化 Vault 客户端或检查服务健康状态时失败,并出现:
ERROR: Job failed (system failure): resolving secrets: initializing Vault service: preparing authenticated client: checking Vault server health: ... x509: certificate signed by unknown authority
上面是原文错误信息的关键部分,省略了冗长的健康检查查询参数,不是本次运行日志。原文给出两种修复方式:把证书加入 GitLab Runner 服务器的 CA 信任存储;或使用 VAULT_CACERT 让 Runner 指向应信任的证书文件。若通过 Helm chart 部署,修改镜像的 CA 存储可能需要自定义 Runner 镜像。
使用 systemd 管理 Runner 时,应通过 Runner 服务的环境变量配置方式提供 VAULT_CACERT。若使用 Helm,先按 GitLab Runner 的自定义证书方式创建 Secret,但放入的是 Vault 证书,而不是误用 GitLab 证书;如果两个服务都用自签名证书,可以把两者放在同一个 Secret 中。然后在 values.yaml 中设置:
# 把两个占位值替换为已创建的 Secret 名称与证书文件名。
certsSecretName: <SECRET_NAME>
envVars:
- name: VAULT_CACERT
value: "/home/gitlab-runner/.gitlab-runner/certs/<VAULT_CERTIFICATE>"
通过 GitLab Development Kit(GDK)在本地使用 Vault 开发模式时,也可能出现同类错误,需要让系统信任相应的本地证书。这里的目标是建立正确的 CA 信任链,不能用关闭 TLS 校验来替代。
排查“secret not found”
当 GitLab 找不到秘密时,可能出现:
ERROR: Job failed (system failure): resolving secrets: secret not found: MY_SECRET
首先检查作业中的 vault 配置,逐项核对挂载点、命名空间、逻辑路径、字段名和引擎类型。原文展示用 Vault CLI 确认字段是否可以读取:
vault kv get -field=password -namespace=admin -mount=ops "production/db"
这个命令会把秘密值写到标准输出。原文以 this-is-a-password 演示输出;本稿没有读取任何秘密,也不复制真实密码。需要排错时,只应在获授权的安全终端执行,并避免终端录屏、共享日志或流水线日志保留秘密。管理员 CLI 读得到,并不能证明作业的受限角色也能读得到;还需核对作业 token、策略和命名空间。
本稿的静态审查结论
已对照源文全文及 GitLab 的 ID token 说明核查:示例地址、项目号、挂载点和路径都是占位或演示配置,没有真实硬编码凭据。需要实际注意的是 issuer 精确匹配、角色必须绑定项目或命名空间、audience 匹配、生产路径通配范围、环境变量和日志泄密,以及正确处理私有 CA。
本稿修正了 file: false 示例的 YAML 层级和标准 ID token 的 issuer,给 here-document 加上引号,并为 Artifactory 示例补上显式 token 引用。其余安全提醒作为编辑补充标明。所有代码仅作静态审阅;未执行 Vault 管理命令、未读取秘密、未运行 GitLab 作业或 CI Lint。没有发现其他问题,不等于这些配置在任意部署中无漏洞。
版权与许可:原文属于 GitLab 文档贡献者,源站页脚链接到 Creative Commons Attribution-ShareAlike 4.0(CC BY-SA 4.0)。本中文翻译及整理按 CC BY-SA 4.0 提供,并保留原文来源与上述变更说明;原创示意图也按 CC BY-SA 4.0 提供。












暂无评论内容