株式会社ネットワールドのエンジニアがお届けする技術情報ブログです。
各製品のエキスパートたちが旬なトピックをご紹介します。

GitLabとHashiCorp Vaultを連携させて、CI/CDパイプラインでシークレットを取得する方法

皆様こんにちは。SEの小池と申します。

今回のブログは、 GitLabとHashiCorp Vaultを連携させて、CI/CDパイプラインでシークレットを取得する方法 を紹介いたします。

今回はJSON Web Token (以降JWTと記載) を有効期限1分のアクセストークンとして使用するため、GitLab側でアクセストークンを変数で設定しておく必要がありません

本記事の対象の方

  • GitLabのCI/CDパイプライン処理でHashiCorp Vaultに登録済のシークレットを使用なさりたい方。

今回のブログのゴール

このブログのゴールはこちらです。

今回のゴール
  • GitLabのCI/CDパイプライン処理で、HashiCorp Vaultに登録済のシークレットをJWT認証で取得する。

事前ご連絡事項

  • 本記事はSelf-Managed版 (オンプレミス) の Enterprise Edition 16.1.2-ee における仕様をベースに記載しております。それ以外のエディションやバージョンではこの記事に記載の通りではない可能性がございます。
  • 本記事の操作説明と画面ショットはGitLabのローカライズを日本語にした状態で説明しております。それ以外の言語をご利用の方は適宜読み替えてください。
  • 本記事ではGitLab RunnerとHashiCorp Vaultが必要です。事前にご用意をお願い致します。
  • 本記事ではHashiCorp Vaultから各種シークレットを取得する際にJWT (JSON Web Token) 認証を用います。この認証方法の処理の流れは本ブログでは触れません。概要についてはこちらのGitLab Docsをご参照ください。
  • 本記事に掲載されている情報は正確性・安全性を保証するものではありません。本記事の情報を利用することによって発生した損失や損害については、一切の責任を負いかねます。

このブログで実装する処理の概要

このブログでは、以下の要件で各種製品を設定していきます。

【実装する処理の概要】
  • GitLabのCI/CDパイプラインでAWSにS3バケットを作成する。ただし、S3バケットはブランチ main にコミットが発生する時のみ作成する。
  • S3バケットはAWS CLIで作成する。
  • ブランチ main にコミットが発生しているか否かはCI/CDパイプライン上で判定せず、HashiCorp Vaultのポリシーで制御する。(※)
  • AWSのアクセスキーIDとシークレットアクセスキーはHashiCorp Vaultに保存しているものを使う。
  • HashiCorp Vaultの認証方式は jwt を使用する。

なお、上記の※については、通常はCI/CDパイプライン上で制御しますが、今回はHashiCorp Vaultのポリシーでも制御可能であることを確認するため、あえてこの実装と致します。

本ブログで紹介する実装例にて使用する製品やサービスは以下の通りです。

本ブログで使用するサービス
  • GitLab
  • Self-Managed版v16.1.2-ee を使用しています。
    サーバー証明書は個人的に所有するドメインのドメイン証明書をLet's Encryptで発行し、それを実装しています。 本ブログに記載された範囲はプラン Premium 以上であることが必須です。無償版では本ブログの処理を実装できないのでご注意ください。

  • Amazon Web Service
  • GitLabのCI/CDパイプラインで、任意のリージョンにS3バケットを作成します。
    S3バケットを作成可能なIAMユーザの アクセスキーIDシークレットアクセスキー が必要になります。

  • GitLab Runner
  • 本ブログではバージョン 16.1.0、エクゼキュータータイプ docker のRunnerを使用しています。

  • HashiCorp Vault
  • 1.14.0、無償版を使用しています。
    サーバー証明書は個人的に所有するドメインのドメイン証明書をLet's Encryptで発行し、それを実装しています。

GitLab, GitLab Runner, HashiCorp Vault 間の処理の概要はこちらのGitLab Docsをご参照ください。

実装手順

実装の流れは以下の通りです。
GitLab, GitLab Runner, HashiCorp Vault, AWSのIAMユーザー は既に準備できている前提で手順を紹介しております。

  1. HashiCorp VaultにAWSのアクセスキーID等を登録する
  2. HashiCorp VaultでJWT認証を有効にする
  3. HashiCorp Vaultでポリシーとロールを作成する
  4. GitLabでCI/CDパイプラインを作成する

本記事に記載された手順を実施することで発生し得る外部サービスの課金について
このブログに記載した設定を実装することで外部サービスで課金が発生する可能性があります。
必要に応じて該当箇所の処理だけコメントアウトするなどの対応をなさってください。

Step1: HashiCorp VaultにAWSのアクセスキーID等を登録する

HashiCorp Vaultに、今回使用するAWSのアクセスキーIDとシークレットアクセスキーを登録します。
ここでは例として、以下の表の内容でシークレットを登録します。

表1. HashiCorp Vaultに登録するシークレット
マウントポイントフォルダーキーvalue
secrets aws aws_access_key_id AKIから始まるAWSのアクセスキーID。
ex.) AKIXXXXXXXXXXXXXXXXX
secrets aws aws_secret_access_key AWSのシークレットアクセスキー。

まず、HashiCorp Vaultで以下のコマンドを実行し、マウントポイントを作成します。
ここでは例としてシークレットエンジンv2で作成します。

# vault secrets enable -version=2 -path=secrets kv

以下のコマンドで、キーaws_access_key_idaws_secret_access_keyを登録します。
このコマンドでは、フォルダーawsも一緒に作成しています。

# vault kv put secrets/aws aws_access_key_id="AKIXXXXXXXXXXXXXXXXX" \
   aws_secret_access_key="xxxxxxxxxxxxxxxxxxxxxxxxxxxx"

Step1の手順は以上です。

Step2: HashiCorp VaultでJWT認証を有効にする

HashiCorp Vaultで以下のコマンドを実行し、JWT認証を有効にします。

# vault auth enable jwt

続けて、JWT認証の設定をします。
この時、引数にjwks_urlbound_issuerを指定します。これらの内容は以下の表をご参照ください。

表2. JWT認証の設定コマンドに必要な引数
引数名
jwks_url GitLabインスタンスのJWTエンドポイントのURL。<GitLabの公開URL>/-/jwks
で指定。
ex.) https://gitlab-sample.com/-/jwks
bound_issuer GitLabの公開URL。
ex.) https://gitlab-sample.com

コマンドサンプルは以下の通りです。

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

Step2の手順は以上です。

Step3: HashiCorp Vaultでポリシーとロールを作成する

まず、作成するロール名とポリシー名を決めます。
このブログでは例としてロール名を sample_role 、ポリシー名を sample_policy として進めます。

先ほど登録したシークレットにread権限を持つポリシーを、下記のコマンドで作成します。
このコマンド例は、シークレットエンジンv2の場合です。v1の場合はpathの設定が異なるのでこちらをご参照ください。

参考 : KV - Secrets Engines | Vault | HashiCorp Developer

# vault policy write sample_policy - <<EOF
path "secrets/data/aws" {
  capabilities = [ "read" ]
}
EOF

次にロールを作成します。
まず最初に、今回のCI/CDパイプラインを実行するGitLabのプロジェクトのIDを確認します。

プロジェクトIDは、プロジェクトのトップページの上部 (下図の赤枠部分) に表示されています。このブログの場合は 1 です。
このプロジェクトIDは後ほど使用するのでメモしておきます。

プロジェクトIDの確認が終わったら、HashiCorp Vaultでロールを作成します。
このブログで設定するロール名は sample_role です。
コマンドのサンプルは以下の通りです。このサンプルの場合、プロジェクトID : 1 で実行されたCI/CDパイプライン 且つ ブランチ main に対してコミットがあった場合 のみ、シークレットを参照可能になるロールを作成しています。

参考 : Authenticating and reading secrets with HashiCorp Vault | GitLab, JWT/OIDC - Auth Methods - HTTP API | Vault | HashiCorp Developer, Using external secrets in CI | GitLab

# vault write auth/jwt/role/sample_role - <<EOF
{
  "role_type": "jwt",
  "policies": ["sample_policy"],
  "token_explicit_max_ttl": 60,
  "user_claim": "user_email",
  "bound_claims_type": "glob",
  "bound_claims": {
    "project_id": "1",
    "ref_type": "branch",
    "ref": "main"
  }
}
EOF

上記コマンドの各パラメーターの概要は、下の表をご参照ください。

表3. ロールの設定
パラメーター名概要
role_type ロールのタイプ。 jwt
policies ロールに割り当てるポリシー。 sample_policy
token_explicit_max_ttl トークンの最大寿命 (秒) 。 任意の数字。ここでは例として 60 を設定。
user_claim ユーザーを一意に識別するために使用するクレーム。JWTペイロードに含まれる要素から任意の値を指定する。
GitLabの場合はuser_id, user_login, user_emailを指定すると良いと考えられる。
ここでは例として user_email を設定。
bound_claims_type バインドされたクレームのタイプ。glob に設定すると、後述の bound_claims の値がグローバルマッチングで評価される。 glob
bound_claims マッチする値に対するクレームのマップ。値は文字列であることが必須。 表4 を参照。

bound_claimsの設定値は、JWTペイロードに含まれる値を使っています。
このブログで実行するコマンドで使用するパラメーターについては以下の表をご参照ください。

表4. bound_claimsのパラメーター設定
パラメーター名概要
project_id GitLab側のプロジェクトID。数字。 ここでは例として1を指定。
ref_type branch か tag のいずれかを指定可。 ここでは例としてbranchを指定。
ref 前述のref_typeでbranchを指定しているので、ここではブランチ名の条件を指定する。 ここでは例としてmainを指定。

Step3の手順は以上です。

Step4: GitLabでCI/CDパイプラインを作成する

GitLabでCI/CDパイプラインを作成します。

対象のGitLabのプロジェクトのトップページから、[ビルド] > [パイプラインエディタ] を開きます。
この操作はロール Developer, Maintainer, Owner のいずれかである必要があります。

[パイプラインの設定] をクリックします。

CI/CDパイプラインの内容を記載します。
コメントに【要変更】と書いてある箇所は、ご自身の環境の設定に変更してください。
なお、この内容はHashiCorp Vaultでシークレットエンジンv2を使用している場合の例です。v1の場合はキーワードscriptsの中身が異なるので、こちらをご参照ください。

参考 : Tutorial: Update HashiCorp Vault configuration to use ID Tokens | GitLab

s3_deploy:
  # AWS CLIを使用できるイメージ。GitLabが公開しているものを使用。
  image: registry.gitlab.com/gitlab-org/cloud-deploy/aws-base:latest   
  variables:
    VAULT_SERVER_URL: https://vault-sample.com:8200   # 【要変更】HashiCorp Vaultの公開URL。
    VAULT_AUTH_PATH: jwt
    VAULT_AUTH_ROLE: sample_role   # 【要変更】HashiCorp Vaultに作成したロール名。
    AWS_DEFAULT_REGION: ap-northeast-1   # 【要変更】S3バケットを作成するリージョン。
  id_tokens:
    VAULT_ID_TOKEN:
      aud: https://gitlab-sample.com   # 【要変更】GitLabの公開URL。
  secrets:     # HashiCorp Vaultから取得したシークレットをシークレットを取得する処理。
    AWS_ACCESS_KEY_ID:
      vault: aws/aws_access_key_id@secrets
      file: false
    AWS_SECRET_ACCESS_KEY:
      vault: aws/aws_secret_access_key@secrets
      file: false
  script:
    - aws s3 mb s3://gitlab-aws-command-test    # 【要変更】S3バケット名は任意に指定。

画面下の [コミットメッセージ] 欄に任意の文字列を指定し、[ブランチ] 欄に main 以外の任意のブランチ名を入れます。
また、マージリクエストを作成するために [この変更で 新規マージリクエスト を作成する] にチェックを入れます。
最後に [変更をコミット] をクリックします。

この後、マージリクエストを作成してください。ブランチ main へのマージは次の手順で実施しますので、ここではマージせずに次の手順へ進んでください。

Step4の手順は以上です。

動作確認

ここからは意図した通りにHashiCorp Vaultからシークレットを取得出来て、最終的にAWSにS3バケットが作成されることを確認します。

前のStep4でマージリクエストを開始したので、CI/CDパイプラインが既に1回実行されていると思われます。

今回のブログの場合、HashiCorp Vaultに設定したロールで ブランチ main にコミットがあること を条件にしているため、このCI/CDパイプラインは 失敗 で終了しているはずです。
なお、この失敗したCI/CDパイプラインのジョブにおいて、HashiCorp Vaultからシークレットを取得する処理に関するログ (抜粋) は以下の通りです。

Resolving secrets
Resolving secret "AWS_ACCESS_KEY_ID"...
Using "vault" secret resolver...
ERROR: Job failed (system failure): resolving secrets: initializing Vault service: preparing authenticated client: authenticating Vault client: writing to Vault: api error: status code 400: error validating claims: claim "ref" does not match any associated bound claim values

次に、前のStep4で作成したマージリクエストをブランチ main にマージしてください。
マージ後、CI/CDパイプラインが 正常終了 することを確認してください。
なお、この正常終了したCI/CDパイプラインのジョブにおいて、HashiCorp Vaultからシークレットを取得する処理に関するログ (抜粋) は以下の通りです。

Resolving secrets
Resolving secret "AWS_ACCESS_KEY_ID"...
Using "vault" secret resolver...
Resolving secret "AWS_SECRET_ACCESS_KEY"...
Using "vault" secret resolver...

マージ後のCI/CDパイプラインが正常終了したら、実際にAWS上にS3バケットが作成されていることを確認してください。

後始末 ~S3バケットの削除~

今回の実装例では、S3に作成したバケットを削除する処理は入れておりません。
S3に作成したバケットは手動で削除をお願い致します。

最後に

この度はGitもCI/CDもよくわかっていないど素人SEによるGitLab検証ブログをお読みいただき、誠にありがとうございます。
このブログの目標は以下のとおりでしたが、皆さまはいかがでしたでしょうか。


今回のゴール
  • GitLabのCI/CDパイプライン処理で、HashiCorp Vaultに登録済のシークレットをJWT認証で取得する。

APIのシークレット等はGitLabのCI/CD変数に事前に登録することも可能ですが、Maintainer 以上のロールを持っているユーザーは容易にCI/CD変数の値を変更することが可能になってしまいます。プロジェクトによってはこういったケースを容認できない場合もあるかと存じます。

こういう場合にHashiCorp Vaultを使うことにより、シークレット (HashiCorp Vault) を管理する要員と、GitLabのプロジェクトを管理する要員を分けつつ、HashiCorp Vault上のシークレット情報を利用することが可能になります。

また、HashiCorp Vaultのロールはかなり細かい設定をすることができます。できるだけ限定的なロールを作ることにより、セキュリティ面をより強固にすることができます。

この記事がGitLabを触り始めた方の一助となれば幸いにございます。


GitLabに関するお問い合わせは、以下のフォームからお願い致します。
GitLab製品 お問い合わせ

今までのGitLabの検証ブログはこちらです。
GitLab カテゴリーの記事一覧 - ネットワールド らぼ

GitLab操作デモ動画 (基本編) を作ってみました。(音声の録音は自宅でiPhoneのボイスメモ使うという超低クオリティですが…。)
つたない内容ではありますが、ご興味がおありでしたら是非ご視聴いただければと存じます。

www.youtube.com