HashiCorp Vaultによる認証とシークレットの読み取り

caution
CI_JOB_JWT での認証はGitLab 15.9で非推奨となり、トークンはGitLab 16.5で削除される予定です。このページで紹介するように、HashiCorp Vaultでの認証にはIDトークンを使用してください。

このチュートリアルでは、GitLab CI/CDからHashiCorpのVaultを使用して、認証、設定、シークレットの読み取りを行う方法を紹介します。

前提条件

このチュートリアルでは、GitLab CI/CDとVaultに精通していることを前提としています。

このチュートリアルに従うには、以下のものが必要です:

  • GitLab のアカウント。
  • 認証を設定し、ロールとポリシーを作成するために、稼働中のVaultサーバ(少なくともv1.2.0)にアクセスできること。HashiCorp Vaultsの場合、これはオープンソース版でもエンタープライズ版でもかまいません。
note
以下のvault.example.com URLをVaultサーバーのURLに、gitlab.example.com をGitLabインスタンスのURLに置き換える必要があります。

どのように動作するか

ID トークンは、サードパーティサービスとの OIDC 認証に使用される JSON Web トークン (JWT) です。ジョブに少なくとも1つの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常にジョブを実行するユーザーのEメール
pipeline_id常にパイプラインのID
pipeline_source常にパイプラインソース
job_id常にこのジョブのID
ref常にこのジョブの Git リファレンス
ref_type常にGit 参照タイプ、branch またはtag
ref_path常にジョブの完全修飾参照。例えば、refs/heads/main 。 GitLab 16.0で導入されました
ref_protected常に true このGit参照が保護されている場合、それ以外の場合はfalse
environmentジョブは環境を指定しますこのジョブが指定する環境 (GitLab 13.9 で導入)
environment_protectedジョブは環境を指定します true 指定した環境が保護されている場合、false (GitLab 13.9 で導入)
deployment_tierジョブは環境を指定しますこのジョブが指定する環境のデプロイ階層(GitLab 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_path": "refs/heads/auto-deploy-2020-04-01",
  "ref_protected": "true",
  "environment": "production",
  "environment_protected": "true"
}

JWTはRS256でエンコードされ、専用の秘密鍵で署名されます。トークンの有効期限は、指定されていればジョブのタイムアウトに、指定されていなければ5分に設定されます。このトークンに署名するために使用される鍵は予告なく変更される可能性があります。そのような場合、ジョブを再試行すると、現在の署名鍵を使って新しいJWTが生成されます。

このJWTとインスタンスのJWKSエンドポイント(https://gitlab.example.com/-/jwks)を使用して、認証にJWT認証メソッドを許可するように設定されているVaultサーバと認証することができます。

Vaultでロールを設定する場合、バインドされたクレームを使用してJWTクレームと照合し、各CI/CDジョブがアクセスできるシークレットを制限することができます。

Vault と通信するには、CLI クライアントを使用するか、API リクエスト(curl または他のクライアントを使用)を実行します。

物件例

caution
JWTはリソースへのアクセスを許可する認証情報です。貼り付ける場所には注意してください!

ステージングデータベースとプロダクションデータベースのパスワードが、http://vault.example.com:8200 で動作している Vault サーバに保存されているとしましょう。ステージング用パスワードは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/

次に、これらのシークレットの読み取りを許可するポリシーを作成します(シークレットごとに1つ):

$ 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 というステージング用のロールが必要です:

$ 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

この例では、バインドされたクレームを使って、指定されたクレームにマッチする値を持つ JWT だけが認証を許可されるように指定しています。

protected ブランチと組み合わせることで、認証できる人やシークレットを読める人を制限できます。

プロジェクトのリストに同じポリシーを使うには、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 が作成する Identity エイリアスの名前を指定します。

bound_claims_type は、bound_claims 値の解釈を設定します。glob に設定すると、値はグロ ブとして解釈され、* は任意の数の文字に一致します。

上の表に記載されているクレーム・フィールドは、Vault内のJWT認証のアクセッサ名を使用することで、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ドキュメントを参照してください。

caution
提供されるクレーム(例えば、project_idnamespace_id )のいずれかを使用して、常にロールをプロジェクトまたはネームスペースに制限してください。そうしないと、このインスタンスで生成されたJWTは、このロールを使用した認証を許可される可能性があります。

次に、JWT認証方法を設定します:

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

bound_issuer は、発行者(つまりiss claim)がgitlab.example.com に設定された JWT のみが認証にこのメソッドを使用でき、トークンの検証には JWKS エンドポイント (https://gitlab.example.com/-/jwks) を使用することを指定します。

利用可能な設定オプションの完全なリストについては、Vault のAPI ドキュメントを参照してください。

GitLab では、以下のCI/CD 変数を作成して、Vault サーバーの詳細を提供します:

  • VAULT_SERVER_URL - VaultサーバーのURL、例えばhttps://vault.example.com:8200
  • VAULT_AUTH_ROLE - オプション。認証を試みる際に使用するロール。ロールが指定されていない場合、Vault は、認証方法が設定されたときに指定されたデフォルトのロールを使用します。
  • VAULT_AUTH_PATH - オプション。認証方法がマウントされているパス。デフォルトはjwt です。
  • VAULT_NAMESPACE - オプション。シークレットの読み取りと認証に使用するVault Enterprise ネームスペース。名前空間を指定しない場合、Vault はルート (/) 名前空間を使用します。この設定は、Vault Open Source では無視されます。

以下のジョブをデフォルトのブランチで実行すると、secret/myproject/staging/ 以下のシークレットは読み取れますが、secret/myproject/production/ 以下のシークレットは読み取れません:

job_with_secrets:
  id_tokens:
    VAULT_ID_TOKEN:
      aud: https://example.vault.com
  secrets:
    STAGING_DB_PASSWORD:
      vault: secret/myproject/staging/db/password@secrets # authenticates using $VAULT_ID_TOKEN
  script:
    - access-staging-db.sh --token $STAGING_DB_PASSWORD

この例では:

  • @secrets - シークレット・エンジンが有効になっている保管庫の名前。
  • secret/myproject/staging/db - Vault内のシークレットのパス位置。
  • password 参照されたシークレット内で取得されるフィールド。

Vaultシークレットへのアクセストークンの制限

Vaultの保護とGitLabの機能を使うことで、VaultシークレットへのIDトークンのアクセスを制御することができます。例えば、以下のようにしてトークンを制限します:

  • group_claim を使って、特定のグループに対して Vaultバインドクレームを使用します。
  • 特定のユーザーのuser_loginuser_email に基づくVaultバインドクレームの値のハードコーディング。
  • token_explicit_max_ttl で指定されているように、トークンのTTLに対するVaultの時間制限を設定します。
  • プロジェクトユーザーのサブセットに制限されているGitLab保護ブランチにJWTをスコープします。
  • プロジェクトユーザーのサブセットに制限されている、GitLabで保護されたタグへのJWTのスコープ。