{{< details >}}

  • Tier: 专业版, 旗舰版
  • Offering: JihuLab.com, 私有化部署

{{< /details >}}

{{< alert type=”warning” >}}

使用 CI_JOB_JWT 认证在极狐GitLab 15.9 中被弃用而且在极狐GitLab 17.0 中被移除。使用 HashiCorp Vault ID 令牌来认证取而代之。

{{< /alert >}}

{{< alert type=”note” >}}

从 Vault 1.17 开始,当 JWT 包含 aud 声明时,JWT 认证登录需要在角色上绑定受众。aud 声明可以是单个字符串或字符串列表。

{{< /alert >}}

这篇教程展示了如何通过极狐GitLab CI/CD 从 HashiCorp 的 Vault 进行身份验证、配置和读取密钥。

前提条件

这篇教程假设你已经熟悉极狐GitLab CI/CD 和 Vault。

为了跟进,你必须拥有:

  • 一个极狐GitLab 账号。
  • 访问运行的 Vault 服务器(至少 v1.2.0),以配置身份验证并创建角色和策略。 对于 HashiCorp Vault,可以是开源版或企业版。

{{< alert type=”note” >}}

你必须将下面的 vault.example.com URL 替换为你的 Vault 服务器的 URL, 并将 gitlab.example.com 替换为你的极狐GitLab 实例的 URL。

{{< /alert >}}

工作原理

ID 令牌是用于与第三方服务进行 OIDC 身份验证的 JSON Web Tokens (JWTs)。 如果一个任务至少定义了一个 ID 令牌,secrets 关键字会自动使用该令牌与 Vault 进行身份验证。

JWT 中包含以下字段:

字段 何时 描述
jti 始终 此令牌的唯一标识符
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 负载示例:

{
  "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 编码并使用专用私钥签名。令牌的过期时间设置为任务的超时,如果指定的话,或者如果没有指定则为 5 分钟。用于签署此令牌的密钥可能会在没有任何通知的情况下更改。在这种情况下,重试任务会使用当前签名密钥生成新的 JWT。

你可以使用此 JWT 与配置为允许 JWT 身份验证方法的 Vault 服务器进行身份验证。将你的极狐GitLab 实例的基本 URL(例如 https://gitlab.example.com)提供给你的 Vault 服务器作为 oidc_discovery_url。服务器然后可以从你的实例检索密钥以验证令牌。

在 Vault 中配置角色时,你可以使用绑定声明来匹配 JWT 声明,并限制每个 CI/CD 任务可以访问的密钥。

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

示例

{{< alert type=”warning” >}}

JWT 是凭证,可以授予资源访问权限。注意你粘贴它们的地方!

{{< /alert >}}

假设你的 staging 和生产数据库的密码存储在运行在 http://vault.example.com:8200 的 Vault 服务器中。你的 staging 密码是 pa$$w0rd,生产密码是 real-pa$$w0rd

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

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

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

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

然后创建允许你读取这些密钥的策略(每个密钥一个):

$ vault policy write myproject-staging - <<EOF
# Policy name: myproject-staging
#
# Read-only permission on 'secret/myproject/staging/*' path
path "secret/myproject/staging/*" {
  capabilities = [ "read" ]
}
EOF
Success! Uploaded policy: myproject-staging

$ vault policy write myproject-production - <<EOF
# Policy name: myproject-production
#
# Read-only permission on 'secret/myproject/production/*' path
path "secret/myproject/production/*" {
  capabilities = [ "read" ]
}
EOF
Success! Uploaded policy: myproject-production

你还需要链接 JWT 和这些策略的角色。

例如,一个名为 myproject-staging 的 staging 角色。绑定声明 配置为只允许策略用于项目 ID 为 22main 分支:

$ 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-* 模式的受保护分支访问密钥。

$ 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

结合受保护分支,你可以限制能够进行身份验证和读取密钥的人。

可以在绑定声明中匹配 JWT 包含的声明 中的任何声明字段,以限制每个 CI/CD 任务可以访问的密钥。例如:

"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 在成功认证后发放的令牌具有 60 秒的硬生命时限。

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

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

通过使用 Vault 中 JWT 身份验证的访问者名称,声明字段列表中表中的字段也可以用于 Vault 的策略路径模板目的。可以通过运行 vault auth list 来检索 mount 访问者名称(下面示例中的 ACCESSOR_NAME)。

使用名为 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 的创建角色文档。

{{< alert type=”warning” >}}

始终使用提供的声明(例如 project_idnamespace_id)将角色限制为项目或命名空间。否则任何由此实例生成的 JWT 都可能被允许使用此角色进行身份验证。

{{< /alert >}}

现在,配置 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_urlhttps://gitlab.example.com)应该用于验证令牌。

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

在极狐GitLab 中,创建以下 CI/CD 变量 来提供关于你的 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 - 可选。用于读取密钥和身份验证的 Vault 企业命名空间。如果没有指定命名空间,Vault 使用根 (/) 命名空间。Vault 开源版忽略该设置。

使用 Hashicorp Vault 的自动 ID 令牌身份验证

以下任务在默认分支运行时,可以读取 secret/myproject/staging/ 下的密钥,但不能读取 secret/myproject/production/ 下的密钥:

job_with_secrets:
  id_tokens:
    VAULT_ID_TOKEN:
      aud: https://vault.example.com
  secrets:
    STAGING_DB_PASSWORD:
      vault: myproject/staging/db/password@secret  # translates to a path of 'secret/myproject/staging/db' and field 'password'. Authenticates using $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 - 启用了你的密钥引擎的 vault 名称。
  • myproject/staging/db - Vault 中密钥的路径位置。
  • password - 要获取的引用密钥中的字段。

如果定义了多个 ID 令牌,请使用 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 令牌身份验证

你可以使用 ID 令牌手动进行 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

限制令牌对 Vault 密钥的访问

你可以通过使用 Vault 保护和极狐GitLab 功能来控制 ID 令牌访问 Vault 密钥。例如,通过以下方式限制令牌:

  • 使用 Vault 绑定受众为特定 ID 令牌 aud 声明。
  • 使用 Vault 绑定声明为特定群组使用 group_claim
  • 根据特定用户的 user_loginuser_email 硬编码 Vault 绑定声明的值。
  • token_explicit_max_ttl 中指定的令牌时间限制,令牌在身份验证后过期。
  • 将 JWT 范围限定为 极狐GitLab 受保护分支,这些分支仅限于项目用户的子集。
  • 将 JWT 范围限定为 极狐GitLab 受保护标签,这些标签仅限于项目用户的子集。

故障排除

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

你可能会在尝试启动配置为访问 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 令牌 aud: 声明值与用于 JWT 身份验证的 rolebound_audiences 参数之间的值不匹配,则可能会收到此错误:

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

确保这些值是相同的。