ブログ一覧に戻る
セキュリティDevOpsGoogleCloud

SA キー不要 — Workload Identity Federation で GitHub Actions ↔ Google Cloud を keyless 連携

はじめに

GitHub Actions から GCP リソースを触りたい場面は多くあります。Cloud Run へのデプロイ、Artifact Registry への push、Secret Manager の参照などなど。古い記事だと「Service Account JSON Key を発行して GitHub Secrets に貼り付ければ OK」と書かれていますが、これは現在では推奨されません。

理由:

  • 鍵が漏れたら即座に侵害される。GitHub Secrets は基本安全ですが、Action のログ流出や悪意ある dependency による持ち出しなど経路はゼロではありません
  • rotation 不可同然: 数十リポジトリで使い回している鍵を一気に更新するのは現実的にきつい
  • 有効期限が無いことが多い: SA Key は明示的に削除しない限り永続。退職者が持ち出していても発覚しません
  • SOC 2 Type II 監査: 「long-lived static credentials を使わない」が現代の標準的な期待値 (CC6.1, CC6.7)

Google が公式に推奨しているのは Workload Identity Federation (WIF) という仕組みで、GitHub Actions の OIDC token を GCP 側で信頼する形に変えれば鍵は一切要りません。本記事ではその設定と、実際にハマるポイントを整理します。

本記事のコマンド例は <OWNER> (GitHub org 名)、<REPO> (リポジトリ名)、<PROJECT_ID> (GCP プロジェクト ID 、文字列)、<PROJECT_NUMBER> (12 桁の数字) などのプレースホルダーを使います。自分の環境に置き換えてください。

アーキテクチャの全体像

┌─────────────────────────────────────────────────────────────────┐
│ GitHub Actions Workflow (push / workflow_dispatch)              │
│                                                                 │
│   1. permissions: id-token: write を宣言                         │
│   2. google-github-actions/auth@v2 を呼び出す                    │
│                       ↓                                          │
│   GitHub の OIDC issuer が短命 JWT を発行 (~10分)                │
│   { iss: token.actions.githubusercontent.com,                   │
│     sub: repo:<OWNER>/<REPO>:ref:refs/heads/main,               │
│     repository_owner: <OWNER>,                                  │
│     repository: <OWNER>/<REPO>, ... }                           │
└─────────────────────────────────────────────────────────────────┘
                          │
                          ↓ JWT を提示
┌─────────────────────────────────────────────────────────────────┐
│ GCP Workload Identity Pool                                      │
│   - OIDC Provider                                               │
│   - issuer-uri: https://token.actions.githubusercontent.com     │
│   - attribute-condition:                                        │
│       assertion.repository_owner == '<OWNER>'                   │
│   - attribute-mapping:                                          │
│       google.subject       = assertion.sub                      │
│       attribute.repository = assertion.repository               │
│       attribute.repository_owner = assertion.repository_owner   │
└─────────────────────────────────────────────────────────────────┘
                          │
                          ↓ pool 通過 → principal token に変換
┌─────────────────────────────────────────────────────────────────┐
│ Service Account: <SA_NAME>@<PROJECT_ID>.iam.gserviceaccount.com │
│   - bindings に principalSet を許可:                             │
│     principalSet://iam.googleapis.com/projects/<PROJECT_NUMBER>/│
│       locations/global/workloadIdentityPools/<POOL_ID>/         │
│       attribute.repository/<OWNER>/<REPO>                       │
│   - 付与 role: artifactregistry.writer / run.admin /            │
│     iam.serviceAccountUser / cloudbuild.builds.editor 等        │
└─────────────────────────────────────────────────────────────────┘
                          │
                          ↓ SA impersonation トークン取得
┌─────────────────────────────────────────────────────────────────┐
│ GCP API (Cloud Run / Artifact Registry / Secret Manager / ...)  │
└─────────────────────────────────────────────────────────────────┘

ポイントは 2 段階の防御:

  1. Pool 層: repository_owner == '<OWNER>' で自分の GitHub org の repo からのみ token 取得可能
  2. SA 層: principalSet://.../attribute.repository/<OWNER>/<REPO> で特定 repo だけが SA を impersonate 可能

将来同じ org に別 repo を追加しても、SA 側に明示的に binding を追加しない限り impersonate できません。安全な拡張モデルです。

設定の主要 4 ステップ

1. Workload Identity Pool を作る

gcloud iam workload-identity-pools create <POOL_ID> \
  --location=global \
  --display-name="GitHub Actions" \
  --project=<PROJECT_ID>

locationglobal 固定。WIF Pool / Provider はリージョン分離されていません。

2. OIDC Provider を作る (ここでハマりがち)

gcloud iam workload-identity-pools providers create-oidc github \
  --location=global \
  --workload-identity-pool=<POOL_ID> \
  --display-name="GitHub OIDC" \
  --attribute-mapping='google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner,attribute.ref=assertion.ref' \
  --attribute-condition="assertion.repository_owner == '<OWNER>'" \
  --issuer-uri='https://token.actions.githubusercontent.com' \
  --project=<PROJECT_ID>

ハマりどころ:

  • --attribute-condition を絶対に省略しない。これが無いと「token.actions.githubusercontent.com を issuer とする任意の repo からの token」を pool が受け入れてしまう (= 全世界の GitHub Actions が WIF を通過してくる)。repository_owner == '<your-org>' で必ず絞る
  • google.subject のマッピング必須。GCP 側で principal を識別するために最低限これは必要
  • issuer-uri は schema 付き https://...。スキーマ無しだと「OpenID configuration が見つかりません」エラー

3. Service Account への binding

GCP project の PROJECT_NUMBER (12 桁の数字、PROJECT_ID とは別物) を取得して principalSet を組み立てる:

PROJECT_NUMBER=$(gcloud projects describe <PROJECT_ID> --format='value(projectNumber)')

gcloud iam service-accounts add-iam-policy-binding \
  <SA_NAME>@<PROJECT_ID>.iam.gserviceaccount.com \
  --project=<PROJECT_ID> \
  --role=roles/iam.workloadIdentityUser \
  --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/<POOL_ID>/attribute.repository/<OWNER>/<REPO>"

ここで 絶対に PROJECT_ID と PROJECT_NUMBER を混同しない ようにしてください。projects/<PROJECT_ID>/... のように文字列 ID を書いてしまうとエラーは出ないのに認証が通らず、デバッグで 30 分を無駄にします (体験談)。

4. GitHub Secrets に値を投入

GCP 側の準備ができたら、GitHub repo の Settings → Secrets and variables → Actions で以下を登録:

Secret 名
GCP_WORKLOAD_IDENTITY_PROVIDERprojects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<POOL_ID>/providers/github
GCP_SERVICE_ACCOUNT<SA_NAME>@<PROJECT_ID>.iam.gserviceaccount.com
GCP_PROJECT_ID<PROJECT_ID>

gh CLI 経由でスクリプトから一括投入するのが楽:

gh secret set GCP_WORKLOAD_IDENTITY_PROVIDER \
  --body "projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/<POOL_ID>/providers/github"
gh secret set GCP_SERVICE_ACCOUNT \
  --body "<SA_NAME>@<PROJECT_ID>.iam.gserviceaccount.com"
gh secret set GCP_PROJECT_ID --body "<PROJECT_ID>"

複数環境 (dev / stg / prd) を扱うなら、Secret 名にサフィックスを付けて (GCP_WORKLOAD_IDENTITY_PROVIDER_DEV 等) workflow 側で参照を切り替える。

ワークフロー側の書き方

name: Deploy (Cloud Run)

on:
  workflow_dispatch:

permissions:
  id-token: write # ← 必須。これが無いと OIDC token が取れない
  contents: read

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Authenticate to GCP (WIF)
        uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}

      - uses: google-github-actions/setup-gcloud@v2

      - name: Verify auth
        run: gcloud auth list --filter=status:ACTIVE --format='value(account)'

google-github-actions/auth@v2 が裏で OIDC token 取得 → WIF 経由で SA impersonation トークン取得 → gcloud にセット、までやってくれます。

よくあるエラーと対処

WIF は便利ですが、エラーメッセージが分かりにくいので主要なものを挙げます。

could not get token: not_found

原因: Workload Identity Pool または Provider が存在しない、または workload_identity_provider Secret の文字列が間違っている

対処:

gcloud iam workload-identity-pools providers describe github \
  --location=global \
  --workload-identity-pool=<POOL_ID> \
  --project=<PROJECT_ID> \
  --format='value(name)'

で返ってくる文字列と GitHub Secret の値が完全一致しているか確認。projects/<PROJECT_NUMBER>/... の数字部分が、もしや project ID になっていないか特にチェック。

iam.serviceAccounts.getAccessToken denied

原因: SA に対する roles/iam.workloadIdentityUser の binding が無い、または principalSet の repo パスが間違っている

対処:

gcloud iam service-accounts get-iam-policy \
  <SA_NAME>@<PROJECT_ID>.iam.gserviceaccount.com \
  --project=<PROJECT_ID>

で binding 一覧を確認。principalSet://iam.googleapis.com/projects/<PROJECT_NUMBER>/locations/global/workloadIdentityPools/<POOL_ID>/attribute.repository/<OWNER>/<REPO> の各部分を順に check:

  • <PROJECT_NUMBER> が 12 桁数字であって PROJECT_ID 文字列ではないこと
  • <OWNER>/<REPO> が GitHub の実際のリポジトリ名であること

attribute condition not matched

原因: OIDC Provider の attribute-condition (例: assertion.repository_owner == '<OWNER>') と実際の repository_owner が一致していない

典型ケース:

  • fork からの PR で実行された: fork PR の repository_owner は fork 側になる。本家 org からの実行のみ通す設計だとここで弾かれる (これは意図通り)
  • org 名の typo: 大文字小文字の違いまで一致が必要

permission denied (workflow 内の gcloud コマンドで)

原因: WIF auth は通過したが、SA に対象 GCP API へのアクセス権限が無い

対処: SA に最小権限を追加。例えば Cloud Run deploy なら:

gcloud projects add-iam-policy-binding <PROJECT_ID> \
  --member="serviceAccount:<SA_NAME>@<PROJECT_ID>.iam.gserviceaccount.com" \
  --role="roles/run.admin"

CI 用 SA に付ける標準セット (deploy 用途):

Role用途
roles/artifactregistry.writerDocker image を push
roles/run.adminCloud Run service を deploy / update
roles/iam.serviceAccountUserSA を別の SA として使う (Cloud Run runtime SA など)
roles/cloudbuild.builds.editorCloud Build を起動する場合

意図的に secretAccessor などは外しておくのがおすすめ。CI は image を push & deploy するだけで Secret Manager を直接読む必要はないため。

workflow が動かないがエラーも出ない (no-op で終わる)

原因: permissions: id-token: write が抜けている

対処: workflow の top-level (または job-level) に明示的に書く。default では id-token は granted されないため、ここを忘れると認証ステップで何も起きないまま落ちる。

smoke test workflow を 1 個用意する

WIF が正しく動くか確認する用に、専用の手動実行 workflow を用意しておくとデバッグが楽です。

name: WIF Smoke Test (manual)

on:
  workflow_dispatch:

permissions:
  id-token: write
  contents: read

jobs:
  smoke:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: ${{ secrets.GCP_WORKLOAD_IDENTITY_PROVIDER }}
          service_account: ${{ secrets.GCP_SERVICE_ACCOUNT }}
      - uses: google-github-actions/setup-gcloud@v2
      - run: gcloud auth list --filter=status:ACTIVE --format='value(account)'
      - run: gcloud artifacts repositories list --location=asia-northeast1
      - run: gcloud run services list --region=asia-northeast1

新しい env を立ち上げたとき、新メンバーの onboarding 時、IAM 変更後など「WIF が動くこと」だけ確認したい時にこれを回せば 30 秒で判定できます。

鍵を発行しなかったことのリターン

WIF 化したあとに振り返ると、以下のメリットが効きました。

  • rotation 作業が消えた: 鍵が無いので rotation 自体が概念として無くなった
  • 退職者対応が簡単: 「あの人が持っていた SA Key、まだ生きてないよね?」という心配が無くなる
  • audit 証跡: Cloud Audit Logs に「いつ・どの GitHub workflow run が・どの SA を impersonate したか」が残る。SOC 2 / ISO 27001 のような監査で出せる証拠が増える
  • Secret Manager の負担減: SA Key を Secret Manager に置いていた組織もあるかもしれませんが、それすら不要

逆に苦しんだのは初期設定だけ。一度動けば後はメンテナンス不要です。

参考

初期設定は冪等な構築スクリプト (TypeScript や bash) にまとめておけば、新しい env (stg / prd / DR drill 用 ephemeral project 等) を立ち上げる時にも一発で組み直せます。鍵 0 のインフラ運用は、最初の山さえ越えれば本当に楽です。