HashiCorp Vaultによる認証とシークレットの読み取り
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の場合、これはオープンソース版でもエンタープライズ版でもかまいません。
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
または他のクライアントを使用)を実行します。
物件例
ステージングデータベースとプロダクションデータベースのパスワードが、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ドキュメントを参照してください。
project_id
やnamespace_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_login
、user_email
に基づくVaultバインドクレームの値のハードコーディング。 -
token_explicit_max_ttl
で指定されているように、トークンのTTLに対するVaultの時間制限を設定します。 - プロジェクトユーザーのサブセットに制限されているGitLab保護ブランチにJWTをスコープします。
- プロジェクトユーザーのサブセットに制限されている、GitLabで保護されたタグへのJWTのスコープ。