Help us learn about your current experience with the documentation. Take the survey.

在 GitLab CI/CD 中使用 HashiCorp Vault secrets

  • Tier: Premium, Ultimate
  • Offering: GitLab.com, GitLab Self-Managed, GitLab Dedicated

使用 CI_JOB_JWT 进行认证已在 GitLab 15.9 中弃用,并在 GitLab 17.0 中移除。 请使用 ID tokens 来认证 HashiCorp Vault, 如本页所示。

从 Vault 1.17 开始,JWT auth login 要求角色上有绑定的 audiences 当 JWT 包含 aud 声明时。aud 声明可以是单个字符串或字符串列表。

本教程演示如何从 GitLab CI/CD 中使用 HashiCorp Vault 进行认证、配置和读取 secrets。

前置条件

本教程假设您熟悉 GitLab CI/CD 和 Vault。

要跟随本教程,您必须拥有:

  • 一个 GitLab 账户。
  • 对运行中的 Vault 服务器(至少 v1.2.0)的访问权限,用于配置认证和创建角色及策略。 对于 HashiCorp Vaults,这可以是开源版或企业版。

您必须将以下示例中的 vault.example.com URL 替换为您 Vault 服务器的 URL, 并将 gitlab.example.com 替换为您 GitLab 实例的 URL。

HashiCorp Vault secrets 集成

ID tokens 是用于与第三方服务进行 OIDC 认证的 JSON Web Tokens (JWT)。 如果一个作业至少定义了一个 ID token,secrets 关键字会自动使用该 token 来认证 Vault。

JWT 中包含以下字段:

字段 时间 描述
jti 始终 此 token 的唯一标识符
iss 始终 签发者,您的 GitLab 实例的域名
iat 始终 签发时间
nbf 始终 生效前时间
exp 始终 过期时间
sub 始终 主题(作业 ID)
namespace_id 始终 使用此按 ID 限定到组或用户级别命名空间
namespace_path 始终 使用此按路径限定到组或用户级别命名空间
project_id 始终 使用此按 ID 限定到项目
project_path 始终 使用此按路径限定到项目
user_id 始终 执行作业的用户 ID
user_login 始终 执行作业的用户名
user_email 始终 执行作业的用户邮箱
pipeline_id 始终 此流水线的 ID
pipeline_source 始终 流水线来源
job_id 始终 此作业的 ID
ref 始终 此作业的 Git ref
ref_type 始终 Git ref 类型,为 branchtag
ref_path 始终 作业的完全限定 ref。例如 refs/heads/main在 GitLab 16.0 中引入
ref_protected 始终 如果此 Git ref 受保护则为 true,否则为 false
environment 作业指定了环境 作业指定的环境
groups_direct 用户是 0 到 200 个组的直接成员 用户直接所属组的路径。如果用户是超过 200 个组的直接成员,则省略此字段。(在 GitLab 16.11 中引入)。
environment_protected 作业指定了环境 如果指定环境受保护则为 true,否则为 false
deployment_tier 作业指定了环境 环境部署层级 (在 GitLab 15.2 中引入)
environment_action 作业指定了环境 作业中指定的 环境操作 (environment:action) (在 GitLab 16.5 中引入)

示例 JWT payload:

{
  "jti": "c82eeb0c-5c6f-4a33-abf5-4c474b92b558",
  "iss": "gitlab.example.com",
  "iat": 1585710286,
  "nbf": 1585798372,
  "exp": 1585713886,
  "sub": "job_1212",
  "namespace_id": "1",
  "namespace_path": "mygroup",
  "project_id": "22",
  "project_path": "mygroup/myproject",
  "user_id": "42",
  "user_login": "myuser",
  "user_email": "myuser@example.com",
  "pipeline_id": "1212",
  "pipeline_source": "web",
  "job_id": "1212",
  "ref": "auto-deploy-2020-04-01",
  "ref_type": "branch",
  "ref_path": "refs/heads/auto-deploy-2020-04-01",
  "ref_protected": "true",
  "groups_direct": ["mygroup/mysubgroup", "myothergroup/myothersubgroup"],
  "environment": "production",
  "environment_protected": "true",
  "environment_action": "start"
}

JWT 使用 RS256 编码并使用专用私钥签名。token 的过期时间 设置为作业的超时时间(如果指定),否则为 5 分钟。 用于签名的密钥可能会在没有任何通知的情况下更改。在这种情况下,重试作业 会使用当前的签名密钥生成新的 JWT。

您可以使用此 JWT 来认证配置为允许 JWT 认证方法的 Vault 服务器。将您的 GitLab 实例的基础 URL (例如 https://gitlab.example.com)提供给您的 Vault 服务器作为 oidc_discovery_url。 服务器然后可以从您的实例获取用于验证 token 的密钥。

在 Vault 中配置角色时,您可以使用 bound claims 来匹配 JWT 声明并限制每个 CI/CD 作业可以访问哪些 secrets。

要与 Vault 通信,您可以使用其 CLI 客户端或执行 API 请求(使用 curl 或其他客户端)。

示例

JWT 是凭据,可以授予对资源的访问权限。请谨慎粘贴!

考虑一个场景,您将暂存和生产数据库的密码存储在 Vault 服务器中。 此场景假设您使用 KV v2 secrets 引擎。 如果您使用的是 KV v1, 请从以下策略路径中移除 /data/,并参阅 如何配置您的 CI/CD 作业

您可以使用 vault kv get 命令检索密码。

$ vault kv get -field=password secret/myproject/staging/db
pa$$w0rd

$ vault kv get -field=password secret/myproject/production/db
real-pa$$w0rd

您的暂存密码是 pa$$w0rd, 您的生产密码是 real-pa$$w0rd

要配置您的 Vault 服务器,首先启用 JWT Auth 方法:

$ vault auth enable jwt
Success! Enabled jwt auth method at: jwt/

然后创建允许您读取这些 secrets 的策略(每个 secrets 一个):

$ vault policy write myproject-staging - <<EOF
# 策略名称:myproject-staging
#
# 对 'secret/data/myproject/staging/*' 路径的只读权限
path "secret/data/myproject/staging/*" {
  capabilities = [ "read" ]
}
EOF
Success! Uploaded policy: myproject-staging

$ vault policy write myproject-production - <<EOF
# 策略名称:myproject-production
#
# 对 'secret/data/myproject/production/*' 路径的只读权限
path "secret/data/myproject/production/*" {
  capabilities = [ "read" ]
}
EOF
Success! Uploaded policy: myproject-production

您还需要将 JWT 与这些策略关联的角色。

例如,一个用于暂存的名为 myproject-staging 的角色。bound claims 配置为只允许 ID 为 22 的项目的 main 分支使用此策略:

$ vault write auth/jwt/role/myproject-staging - <<EOF
{
  "role_type": "jwt",
  "policies": ["myproject-staging"],
  "token_explicit_max_ttl": 60,
  "user_claim": "user_email",
  "bound_audiences": "https://vault.example.com",
  "bound_claims": {
    "project_id": "22",
    "ref": "main",
    "ref_type": "branch"
  }
}
EOF

另一个用于生产的名为 myproject-production 的角色。此角色的 bound_claims 部分 只允许匹配 auto-deploy-* 模式的受保护分支访问 secrets。

$ 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": "22",
    "ref_protected": "true",
    "ref_type": "branch",
    "ref": "auto-deploy-*"
  }
}
EOF

结合 受保护分支, 您可以限制谁能够认证和读取 secrets。

JWT 中包含的任何声明 都可以与 bound claims 中的值列表进行匹配。例如:

"bound_claims": {
  "user_login": ["alice", "bob", "mallory"]
}

"bound_claims": {
  "ref": ["main", "develop", "test"]
}

"bound_claims": {
  "namespace_id": ["10", "20", "30"]
}

"bound_claims": {
  "project_id": ["12", "22", "37"]
}
  • 如果只使用 namespace_id,则允许命名空间中的所有项目。不包含嵌套项目, 因此如果需要,它们的命名空间 ID 也必须添加到列表中。
  • 如果同时使用 namespace_idproject_id,Vault 首先检查项目的命名空间 是否在 namespace_id 中,然后检查项目是否在 project_id 中。

token_explicit_max_ttl 指定 Vault 在成功认证后颁发的 token 有 60 秒的硬性生命周期限制。

user_claim 指定成功登录后 Vault 创建的 Identity 别名的名称。

bound_claims_type 配置 bound_claims 值的解释。如果设置为 glob,值将被解释为 glob, 其中 * 匹配任意数量的字符。

前一个表格 中列出的声明字段也可以通过 Vault 中 JWT auth 的访问器名称用于 Vault 的策略路径模板挂载访问器名称 (以下示例中的 ACCESSOR_NAME)可以通过运行 vault auth list 获取。

使用名为 project_path 的命名元数据字段的策略模板示例:

path "secret/data/{{identity.entity.aliases.ACCESSOR_NAME.metadata.project_path}}/staging/*" {
  capabilities = [ "read" ]
}

支持上述模板策略的角色示例,通过使用 claim_mappings 配置将声明字段 project_path 映射为元数据字段:

{
  "role_type": "jwt",
  ...
  "claim_mappings": {
    "project_path": "project_path"
  }
}

有关完整选项列表,请参阅 Vault 的 Create Role 文档

始终通过使用提供的声明之一(例如 project_idnamespace_id)将您的角色限制在项目或命名空间级别。 否则,此实例生成的任何 JWT 都可能被允许使用此角色进行认证。

现在,配置 JWT 认证方法:

$ vault write auth/jwt/config \
    oidc_discovery_url="https://gitlab.example.com" \
    bound_issuer="https://gitlab.example.com"

bound_issuer 指定只有签发者(即 iss 声明)设置为 gitlab.example.com 的 JWT 才能使用此方法进行认证,并且应该使用 oidc_discovery_url (https://gitlab.example.com) 来验证 token。

有关可用配置选项的完整列表,请参阅 Vault 的 API 文档

在 GitLab 中,创建以下 CI/CD variables 来提供有关您的 Vault 服务器的详细信息:

  • VAULT_SERVER_URL - 您的 Vault 服务器的 URL,例如 https://vault.example.com:8200
  • VAULT_AUTH_ROLE - 可选。尝试认证时使用的 Vault JWT Auth 角色名称。在本教程中, 我们已经创建了两个名称为 myproject-stagingmyproject-production 的角色。如果未指定角色, Vault 使用配置认证方法时指定的 默认角色
  • VAULT_AUTH_PATH - 可选。认证方法挂载的路径。 默认为 jwt
  • VAULT_NAMESPACE - 可选。用于读取 secrets 和认证的 Vault Enterprise 命名空间。 如果未指定命名空间,Vault 使用根 (/) 命名空间。 此设置被 Vault Open Source 忽略。

使用 Hashicorp Vault 自动 ID token 认证

以下作业在为默认分支运行时,可以读取 secret/myproject/staging/ 下的 secrets, 但不能读取 secret/myproject/production/ 下的 secrets:

job_with_secrets:
  id_tokens:
    VAULT_ID_TOKEN:
      aud: https://vault.example.com
  secrets:
    STAGING_DB_PASSWORD:
      vault: myproject/staging/db/password@secret  # 翻译为路径 'secret/myproject/staging/db' 和字段 'password'。使用 $VAULT_ID_TOKEN 进行认证。
  script:
    - access-staging-db.sh --token $STAGING_DB_PASSWORD

在此示例中:

  • id_tokens - 用于 OIDC 认证的 JSON Web Token (JWT)。aud 声明 设置为匹配用于 Vault JWT 认证方法的 rolebound_audiences 参数。
  • @secret - 您启用 Secrets Engines 的 vault 名称。
  • myproject/staging/db - Vault 中 secrets 的路径位置。
  • password 要在引用的 secret 中获取的字段。

如果定义了多个 ID token,请使用 token 关键字指定应使用哪个 token。例如:

job_with_secrets:
  id_tokens:
    FIRST_ID_TOKEN:
      aud: https://first.service.com
    SECOND_ID_TOKEN:
      aud: https://second.service.com
  secrets:
    FIRST_DB_PASSWORD:
      vault: first/db/password
      token: $FIRST_ID_TOKEN
    SECOND_DB_PASSWORD:
      vault: second/db/password
      token: $SECOND_ID_TOKEN
  script:
    - access-first-db.sh --token $FIRST_DB_PASSWORD
    - access-second-db.sh --token $SECOND_DB_PASSWORD

手动 ID Token 认证

您可以手动使用 ID tokens 来认证 HashiCorp Vault。例如:

manual_authentication:
  variables:
    VAULT_ADDR: http://vault.example.com:8200
  image: vault:latest
  id_tokens:
    VAULT_ID_TOKEN:
      aud: http://vault.example.com
  script:
    - export VAULT_TOKEN="$(vault write -field=token auth/jwt/login role=myproject-example jwt=$VAULT_ID_TOKEN)"
    - export PASSWORD="$(vault kv get -field=password secret/myproject/example/db)"
    - my-authentication-script.sh $VAULT_TOKEN $PASSWORD

限制 token 对 Vault secrets 的访问

您可以通过使用 Vault 保护 和 GitLab 功能来控制 ID token 对 Vault secrets 的访问。例如,通过以下方式限制 token:

  • 使用 Vault bound audiences 用于特定的 ID token aud 声明。
  • 使用 Vault bound claims 用于使用 group_claim 的特定组。
  • 基于 user_loginuser_email 对特定用户进行硬编码值用于 Vault bound claims。
  • 设置 Vault 时间限制用于 token 的 TTL,如 token_explicit_max_ttl 所指定, token 在认证后过期。
  • 将 JWT 限定到 GitLab 受保护分支, 这些分支限制为部分项目用户。
  • 将 JWT 限定到 GitLab 受保护标签, 这些标签限制为部分项目用户。

故障排除

找不到 secrets provider。请检查您的 CI/CD 变量并重试。 消息

当尝试启动配置为访问 HashiCorp Vault 的作业时,您可能会收到此错误:

The secrets provider can not be found. Check your CI/CD variables and try again.

作业无法创建,因为未定义必需的变量:

  • VAULT_SERVER_URL

api error: status code 400: missing role 错误

当尝试启动配置为访问 HashiCorp Vault 的作业时,您可能会收到 missing role 错误。 错误可能是因为未定义 VAULT_AUTH_ROLE 变量,因此作业无法认证 vault 服务器。

audience claim does not match any expected audience 错误

如果 YAML 文件中指定的 ID token 的 aud: 声明值与用于 JWT 认证的 rolebound_audiences 参数不匹配, 您可能会收到此错误:

invalid audience (aud) claim: audience claim does not match any expected audience

请确保这些值相同。