CTS-KB
セキュリティ 📚 シリーズ 5/10

WIF でキーレス認証 — GitLab→GCP・GitLab→AWS・GCP↔AWS の OIDC ベストプラクティス

⏱ 約 9 分で読めます
#WIF #Workload Identity Federation #OIDC #キーレス認証 #GCP #AWS #GitLab CI #CI/CD #セキュリティ #ゼロトラスト

🔑 はじめに:なぜいま「キーレス」なのか

本記事はシリーズ 「BtoB SaaS セキュリティ設計」の第 5 回(インフラ・キーレス認証編) です。第 1 回 総覧 の「インフラ・キーレス認証」層を、CI/CD パイプラインからの実装観点まで掘り下げます。

クラウド時代のインフラ・セキュリティの根は、 「静的な鍵をどれだけ無くせるか」 にあります。

長寿命のサービスアカウントキー(GCP の JSON)や AWS のアクセスキーは、CI/CD パイプライン・スクリプト連携で長らく使われてきました。しかし、この方式には常に以下の問題がつきまといます。

  • 漏洩時の被害が大きい:無効化されるまで攻撃者が継続利用できる
  • ローテーション運用の負荷:定期的な再発行・全環境への配布
  • 偶発的な漏洩リスク:リポジトリ・CI ログ・チャットへの混入

これらを根本から解決するのが Workload Identity Federation(WIF)OpenID Connect(OIDC) を組み合わせた キーレス認証 です。

本記事では、CI/CD パイプライン設計で繰り返し登場する以下のシナリオに焦点を絞り、コピペ可能な構成例とベストプラクティスを示します。

  • GitLab → GCP : GitLab CI から GCP リソースを操作する
  • GitLab → AWS : GitLab CI から AWS リソースを操作する
  • GCP ↔ AWS : クラウド間で互いに認証する(マルチクラウド)

🧠 キーレス認証の共通モデル

クラウド各社の WIF / OIDC IdP は命名こそ違いますが、 構造は完全に共通 しています。「外部 IdP が発行した短命の OIDC トークンを、クラウド IAM が直接検証して短命の credential に交換する」というモデルです。

🧠 キーレス認証の共通モデルクラウド APIクラウド STS (GCP/AWS)外部 IdP (gitlab.com)CI ジョブ (GitLab)クラウド APIクラウド STS (GCP/AWS)外部 IdP (gitlab.com)CI ジョブ (GitLab)署名検証 / issuer / audience / sub 条件チェックid_tokens で JWT を要求JWT (sub / aud / exp / 各種 claim)JWT を提示して credential 交換短命 credential (≦1h)短命 credential で API 呼び出し200 OK

検証されるのは主に以下の 4 点です。

  1. 署名:IdP の JWKs(公開鍵)で JWT 署名を検証
  2. issuer:信頼している IdP から発行されたか
  3. audience(aud:自分宛のトークンか
  4. subject(sub)等の条件:許可した発行元かどうか(プロジェクト・ブランチ・ロール等)

このうち 「4. sub 等の条件」を省くと、同じ IdP を使う任意のテナントから認証できてしまう のが最大の落とし穴です。詳細は後述します。

各クラウドでの命名対応

クラウド機能名短命 credential 払出し API
GCPWorkload Identity Federationiam.googleapis.com/.../token
AWSIAM OIDC Identity Providersts:AssumeRoleWithWebIdentity
AzureWorkload Identity Federationoauth2/v2.0/token

🟢 GitLab → GCP:WIF + OIDC で SA キーを撤廃する

最も需要の高い経路です。GitLab CI のジョブから GCP リソース(Cloud Run・GCS・BigQuery 等)を操作する際、サービスアカウント JSON キーを完全に廃止 できます。

構築手順

① GCP:Workload Identity Pool と OIDC Provider を作成

# Pool(環境ごとに分割するのが推奨。横展開時の事故を防ぐ)
gcloud iam workload-identity-pools create gitlab-prod-pool \
  --location="global" \
  --display-name="GitLab Production Pool"

# OIDC Provider(GitLab SaaS の場合)
gcloud iam workload-identity-pools providers create-oidc gitlab-saas \
  --location="global" \
  --workload-identity-pool="gitlab-prod-pool" \
  --issuer-uri="https://gitlab.com" \
  --attribute-mapping="google.subject=assertion.sub,attribute.project_id=assertion.project_id,attribute.namespace_id=assertion.namespace_id,attribute.ref=assertion.ref" \
  --attribute-condition="assertion.namespace_id=='1234567' && assertion.ref=='refs/heads/main'"

要点:

  • attribute-condition を必ず付ける。省くと「GitLab.com 上の任意プロジェクト」から認証可能になり、ほぼ無防備になります
  • namespace_id(数値 ID)で「自社グループのプロジェクトのみ」を担保。namespace_path の prefix マッチは類似名グループでの成りすまし可能性があるため避けます
  • 本番デプロイは assertion.ref=='refs/heads/main' を AND で重ねるのが定番

② GCP:直接アクセス か サービスアカウント なりすましか を選ぶ

GCP には 2 つの権限付与モデルがあります。

方式推奨度仕組み
直接アクセス (principalSet://)★ デフォルト推奨連携 ID 自身に IAM ロールを直接付与(最短経路)
サービスアカウント なりすまし★ 必要時のみSA に IAM ロールを与え、連携 ID には roles/iam.workloadIdentityUser を付与して SA を借りる

なりすましが必要となる代表例は、 連携プリンシパルを直接受け取らない一部の GCP サービス(Cloud Storage の signed URL 生成など)です。それ以外は直接アクセスを推奨します。経路が短いほど監査が容易で、攻撃面も小さくなります。

# 直接アクセス:GitLab プロジェクト 9876543 全体に Cloud Run の roles/run.developer を付与
gcloud projects add-iam-policy-binding MY_PROJECT \
  --role="roles/run.developer" \
  --member="principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/gitlab-prod-pool/attribute.project_id/9876543"

③ GitLab CI:id_tokens で OIDC トークンを取得

deploy_gcp:
  id_tokens:
    GCP_ID_TOKEN:
      # WIF Provider の正規 URI を audience に指定
      aud: https://iam.googleapis.com/projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/gitlab-prod-pool/providers/gitlab-saas
  script:
    - echo "$GCP_ID_TOKEN" > /tmp/id-token.jwt
    - |
      cat <<EOF > /tmp/cred.json
      {
        "type": "external_account",
        "audience": "//iam.googleapis.com/projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/gitlab-prod-pool/providers/gitlab-saas",
        "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
        "token_url": "https://sts.googleapis.com/v1/token",
        "credential_source": { "file": "/tmp/id-token.jwt" }
      }
      EOF
    - export GOOGLE_APPLICATION_CREDENTIALS=/tmp/cred.json
    - gcloud auth login --cred-file=/tmp/cred.json
    - gcloud run deploy my-service --image gcr.io/$MY_PROJECT/app:$CI_COMMIT_SHA

これで JSON キーは一切リポジトリ・GitLab Variables に置かない 構成になります。

よくある失敗パターン

症状原因
Invalid token audienceaud が WIF Provider の正規 URI と一致していない
任意のプロジェクトから認証されてしまうattribute-condition 未設定
Invalid issuerissuer-uri の trailing slash の有無(GitLab.com は https://gitlab.com、末尾なしが正)
Permission denied直接アクセスの member 文字列を principalSet:// ではなく principal:// と書いている

🟠 GitLab → AWS:OIDC Provider + AssumeRoleWithWebIdentity

AWS には「WIF」という名称はありませんが、 「IAM OIDC Identity Provider」+「sts:AssumeRoleWithWebIdentity が同じ役割を果たします。

構築手順

① AWS IAM:OIDC Provider を作成

aws iam create-open-id-connect-provider \
  --url https://gitlab.com \
  --client-id-list "https://gitlab.com" \
  --thumbprint-list <GitLab TLS 証明書の Thumbprint>

Thumbprint は GitLab の TLS 証明書から SHA-1 を計算します(多くの IaC ツールは自動取得をサポート)。

② AWS IAM:信頼ポリシー付きロールを作成

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/gitlab.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "gitlab.com:aud": "https://gitlab.com",
          "gitlab.com:sub": "project_path:my-org/my-app:ref_type:branch:ref:main"
        }
      }
    }
  ]
}

要点:

  • sub 条件は必ず付ける。省くと GitLab.com 全プロジェクトから AssumeRole 可能になります
  • sub のフォーマットは project_path:GROUP/PROJECT:ref_type:TYPE:ref:NAME。ブランチ・タグ・GitLab 環境(environment)のいずれでも条件化できます
  • ワイルドカード(StringLike*)で許容範囲を広げる場合も、 必ず project_path を組織内に閉じる こと

③ GitLab CI:AssumeRoleWithWebIdentity を呼ぶ

deploy_aws:
  variables:
    AWS_REGION: ap-northeast-1
    ROLE_ARN: arn:aws:iam::123456789012:role/gitlab-deploy
  id_tokens:
    AWS_ID_TOKEN:
      aud: https://gitlab.com
  script:
    - >
      creds=$(aws sts assume-role-with-web-identity
      --role-arn "${ROLE_ARN}"
      --role-session-name "gitlab-${CI_PROJECT_ID}-${CI_PIPELINE_ID}"
      --web-identity-token "${AWS_ID_TOKEN}"
      --duration-seconds 3600
      --query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]'
      --output text)
    - export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" $creds)
    - aws sts get-caller-identity
    - aws s3 sync ./build s3://my-bucket/$CI_COMMIT_SHA/

セッション名(role-session-name)に CI_PIPELINE_ID を含めると、AWS CloudTrail での追跡が容易になります。

🌐 GCP ↔ AWS:クロスクラウド連携

「GCP の Cloud Run から AWS S3 を読みたい」「AWS Lambda から GCP の BigQuery を叩きたい」というマルチクラウド要件もキーレスにできます。両方とも長寿命キーは不要です。

GCP → AWS(GCP の SA を AWS に信頼させる)

GCP のサービスアカウントは OIDC ID トークンを自前で発行できる(issuer = https://accounts.google.com)ため、AWS が GCP を OIDC IdP として信頼すれば成立します。

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/accounts.google.com" },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "accounts.google.com:aud": "sts.amazonaws.com",
        "accounts.google.com:sub": "1234567890..."
      }
    }
  }]
}

sub には GCP サービスアカウントの unique ID(メールアドレスではなく数値の unique_id を入れます。GCP 側では SA の generateIdToken API(または ADC)で audience=sts.amazonaws.com の ID トークンを取得し、assume-role-with-web-identity を呼べば短命の AWS credential が得られます。

AWS → GCP(AWS IAM Role を GCP に信頼させる)

逆方向は OIDC ではなく、GCP の AWS provider(専用フロー)を使います。

gcloud iam workload-identity-pools providers create-aws aws-prod \
  --location="global" \
  --workload-identity-pool="aws-pool" \
  --account-id="123456789012" \
  --attribute-mapping="google.subject=assertion.arn,attribute.aws_role=assertion.arn.extract('assumed-role/{role}/')" \
  --attribute-condition="attribute.aws_role=='LambdaPipelineRole'"

AWS 側では Lambda の実行ロールから GCP 用の credential config を生成し、gcloud または GCP クライアントライブラリの ADC が自動的に AWS の GetCallerIdentity を署名して GCP STS に渡します。

🛡️ 共通のベストプラクティス

観点推奨理由
sub の条件attribute-condition / trust policy condition で必ず絞る省くと同 IdP の他テナントから認証可能
prefix マッチではなく ID 一致namespace_id のような数値 ID で厳密比較類似名のグループ・リポジトリでの成りすましを防ぐ
Pool / Provider の粒度環境(prod/staging)ごとに Pool を分割横展開時の事故防止・最小権限の徹底
audienceデフォルト値ではなく Provider 固有 URI を明示リプレイ・流用攻撃の難易度を上げる
ロールの権限デプロイ単位の最小ロール万一の侵入経路を狭くする
セッション名CI_PIPELINE_ID 等を含めるCloudTrail / Audit Log での追跡性
直接アクセス vs SA なりすましデフォルト直接、必要時のみなりすまし経路が短いほど安全・観測しやすい
トークン有効期限1 時間以内(既定値)を維持長くするほど漏洩時のリスクが累積

sub 条件を省略してはいけない理由

attribute-conditiontrue にしたり、AWS trust policy の Condition を空にすると、 同じ IdP(例: gitlab.com)を使う第三者プロジェクト から AssumeRole できてしまいます。これは過去に GitHub Actions の OIDC で実際にインシデント化したパターンで、 「IdP を信頼する ≠ そこを使う全プロジェクトを信頼する」 という原則を覚えておく必要があります。

📝 CTS-EC での採用状況

CTS-EC では現状、 GitLab CI → GCP の経路で WIF を採用しています。

  • サービスアカウントキーをリポジトリ・GitLab Variables のいずれにも保存していない
  • namespace_id + assertion.ref=='refs/heads/main' で本番デプロイを限定
  • staging 環境は別 Pool に分離(横展開事故の防止)
  • GitLab の secrets: キーワードと組み合わせ、 GCP Secret Manager からの DB パスワード取得もキーレス化済み

今後の対策候補:

  • AWS との連携追加(ハイブリッドクラウド要件発生時)
  • Terraform 実行ロールの最小権限化(IaC モジュール単位での粒度調整)
  • OIDC audience に Pipeline 固有値を含める運用(リプレイ耐性のさらなる強化)

本記事はそのインフラ・キーレス認証層の 実装ハンドブック として位置付けられます。

🎯 まとめ

やめるべきもの置き換え先
GCP Service Account JSON キーGitLab id_tokens + GCP WIF
AWS アクセスキー(CI 用)GitLab id_tokens + AWS OIDC Provider + AssumeRoleWithWebIdentity
GCP ↔ AWS の長寿命キー双方の OIDC IdP / GCP の AWS Provider

「鍵を作らない」という選択肢は、 2026 年時点ではすでに揃っています 。CI/CD パイプラインを設計するときに デフォルトでキーレスを選び、condition で必ず sub を絞る ─── これが現時点のベストプラクティスです。


関連用語

関連記事

外部資料