使用 HashiCorp Vault 验证和读取 secrets

本教程演示了如何使用来自 GitLab CI/CD 的 HashiCorp 的 Vault 进行身份验证、配置和读取 secrets。

note 专业版支持对 HashiCorp Vault 的读取访问,并使您能够在 CI 作业中使用 Vault secrets。

要求

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

您必须具备:

  • 极狐GitLab 上的一个账户。
  • 访问正在运行的 Vault 服务器(至少 v1.2.0)的权限,配置身份验证并创建角色和策略。对于 HashiCorp Vaults,这可以是开源或企业版本。
note 您必须将下面的 vault.example.com URL 替换为 Vault 服务器的 URL,并将 gitlab.example.com 替换为极狐GitLab 实例的 URL。

工作原理

每个作业都有 JSON Web Token (JWT) 作为 CI/CD 变量提供,名为 CI_JOB_JWT。此 JWT 可用于使用 JWT Auth 方法对 Vault 进行身份验证。

JWT 中包含以下字段:

字段 When 描述
jti Always 此令牌的唯一标识符
iss Always 发行者,您的极狐GitLab 实例的域名
iat Always 发行时间
nbf Always 生效时间
exp Always 到期时间
sub Always 主题(作业 ID)
namespace_id Always 使用它来按 ID 将范围限定为群组或用户级别的命名空间
namespace_path Always 使用它来按路径将范围限定为群组或用户级别的命名空间
project_id Always 使用它来按 ID 确定项目范围
project_path Always 使用它来按路径确定项目范围
user_id Always 执行作业的用户 ID
user_login Always 执行作业的用户的用户名
user_email Always 执行作业的用户的电子邮件
pipeline_id Always 此流水线的 ID
pipeline_source Always 流水线源
job_id Always 作业的 ID
ref Always 此作业的 Git ref
ref_type Always Git ref 类型,branchtag
ref_protected Always 如果 Git ref 受保护,则为 true,否则为 false
environment 作业指定环境 此作业指定的环境(引入于 13.9 版本)
environment_protected 作业指定环境 如果指定环境受保护,则为 true,否则为 false(引入于 13.9 版本)
deployment_tier 作业指定环境 此作业指定的环境的部署级别(引入于 15.2 版本)

示例 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_protected": "true",
  "environment": "production",
  "environment_protected": "true"
}

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

您可以使用此 JWT 和您的实例的 JWKS 端点 (https://gitlab.example.com/-/jwks),向配置为允许 JWT 身份验证方法进行身份验证的 Vault 服务器进行身份验证。

在 Vault 中配置角色时,您可以使用 bound_claims 来匹配 JWT 的声明并限制每个 CI 作业可以访问的 secret。

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

示例

caution JWT 是凭证,可以授予对资源的访问权限。小心您粘贴它们的地方!

假设您将 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/

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

$ 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:

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

还有一个用于 myproject-production 的生产:

$ vault write auth/jwt/role/myproject-production - <<EOF
{
  "role_type": "jwt",
  "policies": ["myproject-production"],
  "token_explicit_max_ttl": 60,
  "user_claim": "user_email",
  "bound_claims_type": "glob",
  "bound_claims": {
    "project_id": "22",
    "ref_protected": "true",
    "ref_type": "branch",
    "ref": "auto-deploy-*"
  }
}
EOF

此示例使用 bound_claims 指定仅允许与指定声明匹配值的 JWT 进行身份验证。

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

要对项目列表使用相同的策略,请使用 namespace_id

"bound_claims": {
  "namespace_id": ["12", "22", "37"]
}

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

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

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

"bound_claims": {
  "project_id": ["12", "22", "37"]
}

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

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

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

上表中列出的声明字段也可以使用 Vault 中 JWT auth 的访问者名称,访问 Vault 的策略路径模板mount accessor name(以下示例中的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 的 创建角色文档

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

现在,配置 JWT 身份验证方法:

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

bound_issuer 指定只有当 Issuer(即 iss 声明)的 JWT 设置为 gitlab.example.com,才可以使用此方法进行身份验证,并且应该使用 JWKS 端点(https://gitlab.example.com/-/jwks)来验证令牌。

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

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

read_secrets:
  script:
    # Check job's ref name
    - echo $CI_COMMIT_REF_NAME
    # and is this ref protected
    - echo $CI_COMMIT_REF_PROTECTED
    # Vault's address can be provided here or as CI/CD variable
    - export VAULT_ADDR=http://vault.example.com:8200
    # Authenticate and get token. Token expiry time and other properties can be configured
    # when configuring JWT Auth - https://developer.hashicorp.com/vault/api/auth/jwt#parameters-1
    - export VAULT_TOKEN="$(vault write -field=token auth/jwt/login role=myproject-staging jwt=$CI_JOB_JWT)"
    # Now use the VAULT_TOKEN to read the secret and store it in an environment variable
    - export PASSWORD="$(vault kv get -field=password secret/myproject/staging/db)"
    # Use the secret
    - echo $PASSWORD
    # This will fail because the role myproject-staging can not read secrets from secret/myproject/production/*
    - export PASSWORD="$(vault kv get -field=password secret/myproject/production/db)"
note 如果您使用 HashiCorp Cloud Platform 提供的 Vault 实例,则需要导出 VAULT_NAMESPACE 变量。它的默认值为 admin

read_secrets staging

以下作业能够使用 myproject-production 角色进行身份验证并读取 /secret/myproject/production/ 下的 secret:

read_secrets:
  image: vault:latest
  script:
    # Check job's ref name
    - echo $CI_COMMIT_REF_NAME
    # and is this ref protected
    - echo $CI_COMMIT_REF_PROTECTED
    # Vault's address can be provided here or as CI/CD variable
    - export VAULT_ADDR=http://vault.example.com:8200
    # Authenticate and get token. Token expiry time and other properties can be configured
    # when configuring JWT Auth - https://developer.hashicorp.com/vault/api-docs/auth/jwt#parameters-1
    - export VAULT_TOKEN="$(vault write -field=token auth/jwt/login role=myproject-production jwt=$CI_JOB_JWT)"
    # Now use the VAULT_TOKEN to read the secret and store it in environment variable
    - export PASSWORD="$(vault kv get -field=password secret/myproject/production/db)"
    # Use the secret
    - echo $PASSWORD

read_secrets production

限制令牌对 Vault secrets 的访问

您可以使用 Vault 保护和极狐GitLab 功能控制对 Vault 机密的 CI_JOB_JWT 访问。例如,通过以下方式限制令牌:

  • 对使用 group_claim 的特定组使用 Vault bound_claims
  • 基于特定用户的 user_loginuser_email 的 Vault bound claims 的硬编码值。
  • 根据 token_explicit_max_ttl 中指定的令牌 TTL 设置 Vault 时间限制,其中令牌在身份验证后过期。
  • 将 JWT 范围限定为仅限项目用户子集的受保护分支
  • 将 JWT 范围限定为仅限项目用户子集的受保护标签