🔑 はじめに:なぜ「認証方式の歴史」を整理するのか
GitLab CI から GCP リソースを操作する認証方式は、ここ数年で 3 世代 にわたって進化しました。それぞれの世代は単に「古い・新しい」の関係ではなく、 解決した課題と新たに引き受けた制約のトレードオフ を持ちます。「最新だから正しい」と一律に飛びつくと、運用の現実に合わずに痛い目を見るのが、認証アーキテクチャの常です。
本記事では、
- 3 世代の 進化の流れと差分
- CTS が infra 集約リポジトリ と クライアントリポジトリ で異なる方式を採用した判断
- 「インパーソネーションあり / なし」という社内用語の整理
を、実際の external_account credential JSON と principalSet:// バインディング例を交えて解説します。
前提知識:WIF でキーレス認証 で WIF の基本構造と用語を扱っています。本記事は「その上に乗る運用設計」の話です。
🕰️ GitLab → GCP 認証の 3 世代
第 1 世代:Service Account Key(JSON キー)
最も古典的な方式。GCP のサービスアカウントから JSON キーを発行し、GitLab Project Variables に貼り付けて使います。
# 第 1 世代:JSON キーをそのまま使う
deploy:
variables:
GOOGLE_APPLICATION_CREDENTIALS: /tmp/sa-key.json
before_script:
- echo "$GCP_SA_KEY" > /tmp/sa-key.json # Variables から復元
script:
- gcloud auth activate-service-account --key-file=/tmp/sa-key.json
- gcloud run deploy ...
| 項目 | 状況 |
|---|---|
| キーの有効期限 | 無期限(明示削除まで生き続ける) |
| ローテーション | 手動。各環境への再配布が必要 |
| 漏洩時の被害 | キー無効化までの全期間、攻撃者が任意の API を叩ける |
| ログ流出リスク | set -x 等で平文出力する事故が定番 |
結論: 2026 年時点では新規採用は推奨されません。既存のキー運用は全て次世代に置き換えるのが望ましい状態です。
第 2 世代:WIF + Service Account Impersonation
WIF(Workload Identity Federation)が GA した 2021 年以降、デファクトとなった方式です。GitLab CI が発行する OIDC ID トークンを GCP STS が検証し、 指定したサービスアカウントを impersonate して短命 access token に交換します。
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/<num>/locations/global/workloadIdentityPools/<pool>/providers/<provider>",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/<target-sa>:generateAccessToken",
"credential_source": { "file": "/tmp/oidc_token" }
}
特徴:
- 鍵は不要。鍵を作らないので漏洩リスクがゼロ
- IAM ロールは サービスアカウント側に付与 し、CI 側は SA を「借りる」
- WIF Pool と SA の間に
roles/iam.workloadIdentityUserのバインドが必要
| 項目 | 状況 |
|---|---|
| 鍵の存在 | なし(短命 access token のみ) |
| 権限の主体 | サービスアカウント(CI は SA を impersonate) |
| 必要な IAM | SA に通常ロール + roles/iam.workloadIdentityUser を WIF principal に付与 |
| 主な弱点 | SA が「経路上の中間オブジェクト」として 1 つ余計に存在する |
第 3 世代:WIF + Direct Resource Access
最新の推奨パスです。 サービスアカウントを経由せず、連携 ID(principal:// / principalSet://)自身に直接 IAM ロールを付与 します。
# 第 3 世代:Direct Resource Access の IAM バインディング
resource "google_project_iam_member" "ci_run_developer" {
project = var.project_id
role = "roles/run.developer"
member = "principalSet://iam.googleapis.com/${google_iam_workload_identity_pool.pool.name}/attribute.project_path/my-org/clients/client-a/app-repo"
}
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/<num>/locations/global/workloadIdentityPools/<pool>/providers/<provider>",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": { "file": "/tmp/oidc_token" }
}
第 2 世代との JSON の差は 1 行(service_account_impersonation_url を持たない)ですが、IAM 設計上の意味は大きく変わります。
| 項目 | 状況 |
|---|---|
| 中間 SA | 不要(連携 ID 自身が権限主体) |
| IAM 設計 | 1 段階(SA を経由する 2 段階構造が消える) |
| 監査ログ | principal://... が直接アクター名として残る |
| 適用できない場面 | 連携プリンシパルを直接受け取らない API(一部の Cloud Storage signed URL 等) |
「経路が短いほど安全で観測しやすい」という IAM の基本原則に最も沿った方式です。Google も公式ドキュメント上で direct resource access を デフォルト推奨 に格上げしています。
📊 3 世代の比較
| 観点 | 第 1 世代 SA Key | 第 2 世代 WIF + SA Impersonation | 第 3 世代 WIF + Direct Resource Access |
|---|---|---|---|
| 鍵の存在 | あり(無期限) | なし | なし |
| キー漏洩リスク | 高 | なし | なし |
| ローテーション | 手動 | 不要 | 不要 |
| 必要な中間 SA | あり | あり | なし |
| IAM の段数 | 2 段(CI→Key→SA) | 2 段(CI→WIF→SA) | 1 段(CI→WIF→Resource) |
| 監査ログのアクター | SA | SA(CI から impersonate) | 連携 ID 自身 |
| 複数 SA への切替 | 鍵の切替で対応 | impersonation URL を差し替え | バインド対象を増やす |
| API 互換性 | 全 API | 全 API | 一部 API は不可 |
| 採用推奨度 | × | ○ | ◎ |
🧭 CTS の選択:1 つではない最適解
CTS のインフラ運用では、 目的の異なるリポジトリで方式を使い分けています 。「最新だから第 3 世代」と一律に押し通すのではなく、運用要件に合わせて第 2 / 第 3 を選択しています。
infra 集約リポジトリ:第 2 世代(WIF + SA Impersonation)
infra リポジトリは 複数のクライアントプロジェクト・複数環境を 1 本のパイプラインで管理 します。MR 1 件で複数クライアント(client-a / client-b / 共通サービス系)のスタックが任意の組み合わせで影響範囲になり、それぞれ別の GCP プロジェクト・別の Terraform SA で実行する必要があります。
そのため、認証は 「2 段階 WIF」 構造になっています。
GitLab OIDC
├── (1) Bootstrap SA に impersonate → Secret Manager から WIF 設定取得
└── (2) スタック固有の Terraform SA に impersonate → terraform plan/apply
実装上は同じ external_account credential を、 2 つの異なる SA に対して 順次切り替える設計です。
# wif_bootstrap.sh — Bootstrap SA への impersonation
cat > /tmp/wif_bootstrap.json <<JSON
{
"type": "external_account",
"audience": "${BOOTSTRAP_WIF_AUDIENCE}",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"service_account_impersonation_url":
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${BOOTSTRAP_SA}:generateAccessToken",
"credential_source": { "file": "/tmp/oidc_token" }
}
JSON
# wif_auth.sh — Stack SA への impersonation(Secret Manager から取得した値で)
cat > /tmp/wif_cred.json <<JSON
{
"type": "external_account",
"audience": "//iam.googleapis.com/${WIF_AUDIENCE}",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"service_account_impersonation_url":
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${WIF_SA}:generateAccessToken",
"credential_source": { "file": "/tmp/oidc_token" }
}
JSON
第 3 世代に移行できないのは、 「Bootstrap → Stack」という SA 切替の運用が、SA という中間オブジェクトを必要とする ためです。具体的には:
- スタックごとに別プロジェクトの Terraform SA が必要
- どの SA を使うかは Secret Manager から動的に決まる(パイプライン生成時に確定)
- principalSet:// で全プロジェクトの全ロールを直付けすると、 WIF Provider 1 個に対して権限が爆発的に増え 、最小権限の原則を保てない
つまりこの場合、 SA 経由は「冗長な経路」ではなく「権限を集約・差し替えるためのレイヤ」として機能 しています。中間 SA を捨てると IAM 設計が破綻する典型例です。
クライアントリポジトリ:第 3 世代(Direct Resource Access)
一方、各クライアントのアプリ用リポジトリ(例:app-repo)は 単一プロジェクトのアプリデプロイ専用 です。CI で Cloud Run へのデプロイ・Artifact Registry への push しか行いません。
このシナリオでは、SA を経由する理由がありません。 WIF Pool の連携 ID 自身に必要なロールを直接付与 すれば十分です。
# クライアントリポジトリの IAM バインディング(一部)
resource "google_project_iam_member" "ci_run_developer" {
project = module.project.project_id
role = "roles/run.developer"
member = "principalSet://iam.googleapis.com/${module.gitlab_wif.pool_name}/attribute.project_path/my-org/clients/client-a/app-repo"
}
resource "google_project_iam_member" "ci_artifact_writer" {
project = module.project.project_id
role = "roles/artifactregistry.writer"
member = "principalSet://iam.googleapis.com/${module.gitlab_wif.pool_name}/attribute.project_path/my-org/clients/client-a/app-repo"
}
attribute.project_path で 「このクライアントの該当リポジトリからの認証だけ」 を厳密に絞り込んでいるため、第 2 世代の SA バインドより条件記述が直感的です。SA という余分なオブジェクトを管理する必要もなく、IAM ポリシーは gcloud projects get-iam-policy で 直接ポリシー全体を読めば運用がわかる 状態を保てます。
「インパーソネーションあり / なし」の社内用語
CTS のインフラドキュメントでは「インパーソネーションあり / なし」という言い回しが使われていますが、Google の正式用語ではないため定義を揃えます。
| 社内用語 | 公式分類 | 意味 |
|---|---|---|
| インパーソネーションあり | (公式分類なし。社内固有の 2 段階方式) | Principal SA → Target SA の 2 段階で SA を借りる |
| インパーソネーションなし | 第 2 世代 (WIF + SA Impersonation) | WIF から 直接 Target SA を 1 段階で借りる |
| 直接アクセス | 第 3 世代 (Direct Resource Access) | SA を全く介さず連携 ID 自身に IAM ロール |
「インパーソネーションなし」は 公式の分類では第 2 世代 に当たる点に注意が必要です。「中継 SA を経由しない」という意味であり、「impersonation API を呼ばない」という意味ではありません。
🪧 補足:GitLab CI 側の OIDC トークン取得方法の遍歴
クラウド側の認証方式(第 1〜3 世代)と並行して、 GitLab 側の OIDC トークン発行 API にも独立した世代変遷があります。本記事の YAML 例で id_tokens を当然のように使っていますが、これは比較的新しい仕組みで、それ以前は別の方法が標準でした。
| 時期 | 機構 | 特徴 | 状態 |
|---|---|---|---|
| ~ GitLab 15.8 | CI_JOB_JWT / CI_JOB_JWT_V2 事前定義変数 | 全ジョブに自動配布、audience が固定 | GitLab 15.9(2023-02)で deprecated |
| GitLab 15.7+ | id_tokens キーワード | ジョブ単位で複数 audience を明示宣言 | 現行推奨 |
| GitLab 16.1+ | id_tokens:aud で変数展開可能 | aud に CI 変数や式を埋め込める | 機能拡充 |
| GitLab 17.0 | CI_JOB_JWT_V2 削除予定 | — | EOL |
CI_JOB_JWT_V2 は事前定義変数だったため、 すべてのジョブから無条件に参照でき、aud が GitLab インスタンス URL に固定 という制約がありました。これでは複数の信頼境界(GCP・AWS・Vault 等)を 1 パイプラインで使い分けにくく、 流用攻撃のリスクも高い という指摘がコミュニティから上がっていました。
id_tokens キーワードでは:
- ジョブ単位で 複数の独立した audience を宣言できる(GCP 用・AWS 用・Vault 用を別トークンで発行)
- ジョブが明示的に
id_tokens:を書いた場合のみ発行されるため、 不要なジョブにトークンが配布されない - GitLab 16.1 以降は
audに変数展開が効くため、Provider URL を環境別に切替できる
CTS では、CI_JOB_JWT_V2 の非推奨化(GitLab 15.9, 2023 年 2 月)を機に、すべての CI/CD パイプラインを id_tokens 方式へ全面移行 しました。これにより、Terraform 用と AWS 用で別の audience を発行し、各信頼境界に対して 最小限のクレデンシャル だけを渡せる構成になっています。本記事以降の YAML 例はすべて id_tokens 前提で記述しています。
移行時の典型的な落とし穴: WIF Provider 側の
allowed_audiencesを変えずにid_tokens:audだけ書き換えると、Invalid token audienceで失敗します。両側を一貫した値で揃える必要があります。
🔄 同じパイプラインで使い分ける実装例
GitLab CI 1 本で、 Terraform 実行は第 2 世代・アプリデプロイは第 3 世代 という使い分けも当然可能です。id_tokens を共通化し、before_script で credential JSON だけ切り替えます。
.gen2_terraform:
id_tokens:
GOOGLE_OIDC: { aud: https://gitlab.com }
before_script:
- bash scripts/wif_bootstrap.sh
- source scripts/load_env_from_gsm.sh
- bash scripts/wif_auth.sh # Stack SA に impersonate(第 2 世代)
.gen3_app_deploy:
id_tokens:
GOOGLE_OIDC: { aud: https://gitlab.com }
before_script:
- |
cat > /tmp/cred.json <<EOF
{
"type": "external_account",
"audience": "//iam.googleapis.com/${WIF_AUDIENCE}",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": { "file": "/tmp/oidc_token" }
}
EOF
- export GOOGLE_APPLICATION_CREDENTIALS=/tmp/cred.json # Direct(第 3 世代)
「service_account_impersonation_url を入れるかどうか」だけで世代が切り替わる、というのが実装上のポイントです。
🛡️ どの世代でも外せないベストプラクティス
世代に関わらず、 WIF を採用する以上は守るべき 基本原則があります。これは WIF でキーレス認証 で詳述したとおりですが、再掲します。
| 原則 | 理由 |
|---|---|
attribute_condition で project_path を厳密一致 | 同じ GitLab.com の他プロジェクトからの侵入を防ぐ |
allowed_audiences を明示 | リプレイ攻撃の難易度を上げる |
| Pool は環境(prod/staging)ごとに分割 | 横展開時の事故防止 |
ブランチ条件(refs/heads/main / ref_protected)を併用 | ブランチ保護を活かす |
監査ログで principal:// または serviceAccount: のアクターを必ず確認 | 想定外の経路を早期発見 |
📝 移行ガイド:何から始めるべきか
第 1 世代から脱出したい場合の最短ルートです。
- まず第 1 → 第 2 に切り替える(鍵を撤廃する効果が最も大きい)
- その上で、SA を経由する 必然性がない 経路を洗い出し、第 2 → 第 3 に段階移行
- 集約パイプライン等、SA 切替が要件に組み込まれているものは無理に第 3 世代に倒さない
「全部第 3 世代に揃える」をゴールにするのではなく、 経路ごとの最小段数 を達成するのがゴールです。
🎯 まとめ
| 採用判断 | 推奨世代 |
|---|---|
| 単一プロジェクトのアプリデプロイ | 第 3 世代(Direct Resource Access) |
| 単一プロジェクトの Terraform 実行 | 第 3 世代 または第 2 世代(API 互換性で判断) |
| 複数プロジェクト・複数 SA を切り替える集約パイプライン | 第 2 世代(中間 SA が運用上の意味を持つ) |
| 公開済みの SA キーが残っている | 直ちに第 2 世代へ移行 |
CTS では:
- infra 集約リポジトリ:第 2 世代(Bootstrap SA → Stack SA の 2 段階 impersonation)
- クライアント側アプリ用リポジトリ:第 3 世代(principalSet:// で直接バインド)
と方式を分けています。これは 「最新が常に最適」ではなく、運用要件に応じた最小段数を選ぶ」 という技術選定の典型例です。
関連記事
- WIF でキーレス認証 — 本記事の前提となる WIF の基本構造
- GCP → AWS キーレス認証の落とし穴 — AWS 側の Trust Policy 設計とハマりどころ
- Terraform IaC 実践ガイド:CI/CD パイプライン編 — 集約リポジトリ側の自動化設計
- BtoB SaaS セキュリティ設計総覧 — インフラ・キーレス認証の全体位置づけ
関連用語
- Workload Identity Federation — 中心概念
- Service Account Impersonation — 第 2 世代の中核機能
- GitLab id_tokens —
CI_JOB_JWT_V2の置き換え、本記事 §🪧 の主役 - OpenID Connect — 認証プロトコルの基盤
外部資料