ユースケーススタディ: CI ジョブでのシークレット利用

目的

  • ユーザーが CI ジョブでネイティブ GitLab シークレットを使用する方法をまとめます。
  • OpenBao は HashiCorp Vault のフォークであるため、Runner における Vault インテグレーションとの互換性を確認します。
  • OpenBao のポリシーJWT ロールを、プロジェクトごとに異なる GitLab ユーザーロールのパーミッションと互換性を持つ形で構成する方法を高レベルで理解します。

前提条件

このワークフローでは、capabilities(例: read+updateread+update+create)のすべての組み合わせに対するテンプレート化されたポリシーが事前に定義されている必要があります。たとえば、プロジェクトのシークレットへの完全なアクセスを許可する次のテンプレート化されたポリシーを考えます。

bao policy write project_full_access - <<EOF
path "kv-v2/data/projects/{{identity.entity.aliases.auth_jwt_02163755.metadata.project_id}}/*" {
  capabilities = [ "read", "create", "update", "delete", "list" ]
}
EOF

ポリシーは認可時に JWT ロールと関連付けられます。project_full_access ポリシーは特に初期プロジェクトオーナーロールに対して重要です。

bao write auth/jwt/role/project_owner - <<EOF
{
  "role_type": "jwt",
  "policies": ["project_full_access"],
  "token_explicit_max_ttl": 60,
  "user_claim": "user_id",
  "claim_mappings": {
    "project_id": "project_id"
  },
  "bound_audiences": "secrets.gitlab.com",
  "bound_claims_type": "glob",
  "bound_claims": {
    "user_access_level": "owner"
  }
}
EOF

OpenBao のポリシーはデフォルトで拒否されるため、プロジェクトオーナーにシークレットの読み書き完全アクセス権を付与するためにこの初期 JWT ロールが必要です。

初期セットアップワークフロー

プロジェクトのネイティブシークレットが初めて設定される際の手順と技術情報を詳述します。

  1. プロジェクトオーナーが GitLab UI から GitLab Secrets Manager を有効化します。
  2. プロジェクトオーナーが GitLab UI から、どの GitLab ユーザーロールがシークレットを読み取り・書き込み・作成できるかについて追加パーミッションを定義します。
    • デフォルトでは、プロジェクトオーナーは完全アクセス権を持ち、他のロールは拒否されます。

    • たとえば、オーナーが developer ロールに読み取り専用アクセスを許可する場合、OpenBao API を通じて Rails バックエンドが project_88_developer を定義します。

      # ロール名のフォーマットは `project_<project-id>_<user-role>`
      bao write auth/jwt/role/project_88_developer - <<EOF
       {
         "role_type": "jwt",
         "policies": ["project_read_only"],
         "token_explicit_max_ttl": 60,
         "user_claim": "user_id",
         "claim_mappings": {
           "project_id": "project_id"
         },
         "bound_audiences": "secrets.gitlab.com",
         "bound_claims_type": "glob",
         "bound_claims": {
           "user_access_level": "developer"
         }
       }
       EOF
      
    • project_owner の汎用ロールとは異なり、プロジェクトごとにユーザーロールのパーミッションの組み合わせが異なる可能性があるため、オーナー以外のロールをプロジェクトに紐付けて定義する必要があります。

  3. プロジェクトオーナーが GitLab UI からシークレットを定義します。
    • ユーザーは名前・キー・値などの詳細を定義します。入力例:

      • name: Production Database Password
      • key: DB_PASS
      • value: mydbpass
    • シークレットは kv-v2/data/projects/88/ci/DB_PASS の下に以下の JSON データで OpenBao に保存されます。

      {
        "data": "mydbpass"
      }
      
    • ユーザーはシークレットの値を JSON 形式で入力する必要はありません。Rails バックエンドが入力を OpenBao へ送信する前に data キーを持つ JSON オブジェクトに変換します。

  4. 開発者が .gitlab-ci.ymlsecrets キーワードを使用します。
    • 設定例:

      job-with-secrets:
        secrets:
          MY_SECRET_ON_OPENBAO:
            key: DB_PASS # kv-v2/data/projects/88/DB_PASS(フィールド `data`)に対応
      
    • aud のデフォルトが OpenBao サービスのある https://secrets.gitlab.com になるため、id_tokens:VAULT_ID_TOKEN を指定する必要はありません。

    • HashiCorp Vault とは異なり、CI/CD 変数を定義する必要はありません。

      • VAULT_SERVER_URL は OpenBao サービスのある https://secrets.gitlab.com がデフォルトです。
      • VAULT_AUTH_ROLE は OpenBao の JWT ロールに一致するよう project_<project_id>_<job_user_role> がデフォルトです。
  5. CI ジョブが実行され、MY_SECRET_ON_OPENBAO が環境変数として利用可能になります。
    • OpenBao は ID トークンの整合性を検証し、bound_claims がカスタムクレーム(特に GitLab ユーザーロールを含む user_access_level)と一致するかを検証します。
    • HashiCorp Vault のシークレットと同様に、これは file 変数です。

技術実装の知見

ワークフローをサポートするための OpenBao と Rails に関する高レベルの技術実装詳細です。

  1. ワークフローと互換性を持たせるために、OpenBao サービスを適切に設定する必要があります。
    • ID トークン認証と連携させるために JWT 認証を設定します。
    • ドキュメントには vault CLI を使った手順が示されていますが、bao でも同様に機能します。
    • OpenBao API は https://secrets.gitlab.com から到達可能です。
    • テンプレート化されたポリシーで project_id を参照するには、JWT auth マウントアクセサの値(bao auth list の結果から auth_jwt_02163755)を取得する必要がありました。テンプレート化されたポリシーが正しいアクセサで最新の状態を保つよう、デプロイ時に自動化する必要があります。マウントアクセサの値はストレージに永続化され、OpenBao サーバーが再起動・シールされても値が保持されます。
  2. ワークフローをサポートするために、Rails バックエンドに付随する実装が必要です。
    • シークレットの ActiveRecord モデル。UI でのシークレット一覧表示や詳細表示では OpenBao へのリクエストを行わないようにします。
    • パーミッションの ActiveRecord モデル。UI でのパーミッション一覧表示では OpenBao へのリクエストを行わないようにします。
    • CI 設定で id_tokens を定義せずに ID トークンを使用できるよう、ID トークン関連の実装を更新します。
    • VAULT_SERVER_URLVAULT_AUTH_ROLE のデフォルト値の適切なマッピング。

ローカルでのテスト方法

ここで紹介するポリシーとロールの構成は、まず GDK セットアップと dev モードで動作する OpenBao サーバーを使ってローカルでテストしました。

以下はローカルでテストするためのステップバイステップガイドです。

  1. runner を使って GDK を適切にセットアップしていることを確認します。

    • Docker executor を使った GDKでテストし、gdk.test172.16.123.1 に向けましたが、shell executor でも動作するはずです。
    • テストプロジェクトで CI パイプラインを正常に実行できることを確認します。
  2. 後で OpenBao からシークレットを取得するためのテストプロジェクトを作成します。

    • プロジェクト ID をメモしておきます。この例ではプロジェクト ID は 53 でした。
  3. dev モードで OpenBao を起動します。

    bao server -dev -dev-root-token-id="dev-only-token"
    
    • これにより OpenBao が https://127.0.0.1:8200 で到達可能になります。
    • 以下の bao CLI コマンドを動作させるには export BAO_ADDR='https://127.0.0.1:8200' を実行する必要があるかもしれません。
  4. kv-v2 シークレットエンジンを有効化します。

    bao secrets enable kv-v2 # デフォルトで `kv-v2/data` にマウントされます
    
  5. JWT 認証を有効化します。

    bao auth enable jwt
    
  6. OpenBao の JWT 認証を設定します。

    bao write auth/jwt/config \
      oidc_discovery_url="https://gdk.test:3000" \
      bound_issuer="https://gdk.test:3000"
    
  7. GitLab ユーザーロール owner を持つプロジェクトオーナーに対して生成されるポリシーとロールをテストするために、テンプレート化されたポリシーと特定の owner ロール用の JWT ロールを作成します。JWT ロールは GitLab Vault サンプルサーバーロールに基づいています。

    • bao auth list 実行時の JWT auth マウントアクセサの値をメモします。

      Path      Type     Accessor               Description                Version
      ----      ----     --------               -----------                -------
      jwt/      jwt      auth_jwt_02163755      n/a                        n/a
      token/    token    auth_token_90d6d0c1    token based credentials    n/a
      
    • テンプレート化されたポリシーを定義し、マウントされた JWT auth プラグインのメタデータを通じて project_id を参照します。

      bao policy write project_full_access - <<EOF
      
      # オーナーはプロジェクトのシークレットへの完全な読み書きアクセス権を持ちます
      # `auth_jwt_02163755` マウントアクセサの値をコピーしてください
      path "kv-v2/data/projects/{{identity.entity.aliases.auth_jwt_02163755.metadata.project_id}}/*" {
        capabilities = [ "read", "create", "update", "delete", "list" ]
      }
      EOF
      
    • JWT ロールを定義し、project_full_access ポリシーを関連付けます。

      bao write auth/jwt/role/project_owner - <<EOF
      {
        "role_type": "jwt",
        "policies": ["project_full_access"],
        "token_explicit_max_ttl": 60,
        "user_claim": "user_id",
        "claim_mappings": {
          "project_id": "project_id"
        },
        "bound_audiences": "secrets.gitlab.com",
        "bound_claims_type": "glob",
        "bound_claims": {
          "user_access_level": "owner"
        }
      }
      EOF
      
  8. CI ジョブで取得したいサンプルシークレットを作成します。

    bao kv put -mount=kv-v2 projects/53/foo val=my-long-passcode
    
  9. テストプロジェクトで、既存の Vault インテグレーションを使って OpenBao からシークレットを取得するように .gitlab-ci.yml を設定します。

    test_openbao:
      variables:
        VAULT_SERVER_URL: https://127.0.0.1:8200
        VAULT_AUTH_ROLE: project_owner
      id_tokens:
        VAULT_ID_TOKEN:
        aud: secrets.gitlab.com
      secrets:
        SECRET:
          vault: projects/53/foo/val  # シークレット `kv-v2/data/projects/53/foo`(フィールド `val`)に対応
          token: $VAULT_ID_TOKEN
      script:
        - echo "testing..."
        - cat $SECRET
        - echo "done."
    
    • VAULT_AUTH_ROLE は先ほど作成した JWT ロールと一致します。
    • aud はロールの bound_audiences と一致します。
    • このジョブで生成された ID トークンは、bound_claims(特に ID トークンのカスタムクレームに含まれる GitLab ユーザーロールを示す user_access_level)を使って OpenBao によって照合されます。
  10. パイプラインを実行し、ジョブトレースに OpenBao から取得したシークレットのマスクされた出力が表示されることを確認します。