メインコンテンツへスキップ
友田 陽大
インフラ・IaC・CI/CD
CI/CD
セキュリティ
AWS
GCP
DevOps

GitHub Actions を OIDC で鍵レスにする:AWS IAM ロールと GCP Workload Identity Federation で長期キーを捨てる

GitHub ActionsのCI/CDから長期クラウド資格情報を廃止する実装ガイド。OIDC federationで短命トークンを発行し、AWS(IAM OIDCプロバイダ+ロール信頼ポリシー)とGCP(Workload Identity Federation)を設定、sub/aud/repo/branchで信頼範囲を絞り最小権限にする方法を、実設定とTerraformで解説します。

公開日
読了時間
25分
著者
友田 陽大
シェア
目次

「CI/CD から AWS にデプロイしたい」「GitHub Actions から GCP にイメージを push したい」——要件は一行です。でも、その実装を AWS_SECRET_ACCESS_KEY や サービスアカウント鍵(JSON)を GitHub Secrets に貼り付けて済ませた瞬間、あなたのリポジトリは 「漏れたら終わり」の長期キーを抱えた攻撃面 に変わります。

この記事は、GitHub Actions のCI/CDから 長期クラウド資格情報を完全に廃止する ための実装ガイドです。鍵を保存する代わりに、OIDC federation で「各ワークフロー実行ごとに短命の署名付きIDトークンを発行 → クラウド側が『どのrepo/branch/environmentからの実行か』を検証して一時クレデンシャルを払い出す」構成に置き換えます。鍵は、どこにも存在しません。

題材として、私が実際に AWS と GCP の両方 で鍵レスCI/CDを運用している構成を交えます。国内大手放送事業者向けの社内AIプラットフォーム(Workload Identity Federation で鍵レスCI/CDを実現)では GCP 側を、別の木材業界DX案件では AWS 側を、それぞれ OIDC で組んでいます。

この記事のルール:信頼できる情報源は GitHub / AWS / GCP の公式ドキュメント(2026年6月時点) のみです。トークンのクレーム名・アクションの入力名・信頼ポリシーのキーは公式から引用し、推測は明示します。クラウドの仕様・推奨は改定されるため、本番投入前に必ず公式で最新値を確認してください。長期キーは『漏れたら終わり』。OIDCの短命トークンに置き換えるのが、いまの正解です。


0. メンタルモデル:なぜ長期キーが「最大の攻撃面」なのか

設計に入る前に、ここだけは腹落ちさせてください。CIに置く長期キーが危険な理由は、抽象論ではなく構造にあります。

0.1 長期キーの何がまずいのか

AWS_SECRET_ACCESS_KEY や GCP のサービスアカウント鍵(JSON)を GitHub Secrets に保存する——これは便利ですが、次の性質を同時に抱えます。

  • 無期限:明示的にローテーションしない限り、その鍵は何ヶ月も有効なまま。発行した本人が忘れた頃にも生きている。
  • どこからでも使える:鍵は「誰が・どこから使ったか」を区別しません。手元のラップトップからでも、悪意あるフォークのPRからでも、漏洩先の攻撃者のサーバーからでも、同じように本番権限を行使できる。
  • 漏洩が静かに進行する:ログへの誤出力、悪意ある依存パッケージ、サードパーティ Action の乗っ取り、pull_request_target の設定ミス——CI で秘密が漏れる経路は無数にあります。そして漏れても、鍵はそのまま動き続けるので気づけない

GitHub 公式が OIDC を勧める理由も、まさにこれです。「クラウドの資格情報を、長期的な GitHub シークレットとして複製する必要がなくなる(no longer need to duplicate your cloud credentials as long-lived GitHub secrets)」と明記しています。**問題は「鍵が漏れること」ではなく、「漏れても被害が止まらない無期限の鍵を、最も狙われる場所(CI)に置くこと」**なのです。

0.2 OIDC federation は何を変えるのか

OIDC(OpenID Connect)federation は、この構造を根本から変えます。流れはこうです。

  1. ワークフロー実行が始まると、GitHub の OIDC プロバイダ(token.actions.githubusercontent.com)が、その実行に固有の 署名付きJWT(IDトークン) を発行する。
  2. このトークンには「どのリポジトリの、どのブランチ/タグ/environment から、どのワークフローが」実行されているかが**クレーム(claim)**として刻まれている。
  3. ワークフローはこのトークンをクラウド(AWS STS / GCP STS)に提示する。
  4. クラウド側は、事前に設定された信頼ポリシーと照合し、「sub クレームやその他のクレームが、ロールのOIDC信頼定義に事前設定された条件に一致するか(check if the OIDC token's subject and other claims are a match for the conditions...)」を検証する。
  5. 一致したときだけ、そのジョブ1回限りで自動失効する短命の一時クレデンシャルを払い出す。

GitHub 公式の表現を借りれば、OIDC トークンは「1つのジョブでのみ有効で、その後自動的に失効する短命のアクセストークン(short-lived access token that is only valid for a single job, and then automatically expires)」と引き換えられます。

鍵は存在しません。 保存すべき秘密が消え、そのうえ「どこから来た実行か」をクラウド側が能動的に検証する。これが鍵レスCI/CDの核心です。

0.3 旧来 vs OIDC:決定表

観点長期アクセスキー(旧来)OIDC 鍵レス(推奨)
保存される秘密あり(Secretsに永続)なし(トークンは実行時に発行)
有効期間無期限(手動ローテ)ジョブ1回限り・自動失効
発行元の検証できない(鍵に主体情報なし)できる(sub/repo/ref/environment クレーム)
漏洩時の被害気づくまで本番権限を行使され続けるトークンは即失効・再利用不可
ローテーション運用必要(忘れると事故の温床)不要(毎回新規発行)
信頼範囲の絞り込み鍵を持つ全員repo/branch/environment 単位
監査鍵単位(誰が使ったか不明瞭)クレーム単位で追跡可能

「ローテーション不要」は地味に見えて巨大です。長期キーの運用で最も多い事故は、ローテーションを忘れて何年も同じ鍵が生き続けることだからです。OIDC はその運用負債そのものを消し去ります(YAGNI:そもそも要らない仕組みは持たない)。


1. すべての出発点:id-token: write パーミッション

AWS でも GCP でも、ワークフロー側の第一歩は共通です。GitHub に OIDC トークンの発行を許可すること。これは permissions ブロックで明示します。

permissions:
  id-token: write   # GitHub の OIDC プロバイダに JWT を発行させる(必須)
  contents: read    # actions/checkout がリポジトリを読むため

公式の説明はこうです。id-token: write は「GitHub の OIDC プロバイダが各実行ごとに JSON Web Token を作成することを許可する(allows GitHub's OIDC provider to create a JSON Web Token for every run)」。ただし注意点として、この権限自体はクラウドのリソースを変更する力を持ちません。あくまで「トークンを作っていい」という許可であり、実際に何ができるかはクラウド側のロール/ポリシーが決めます。

最小権限の出発点permissionsジョブ単位でも宣言できます。トークン発行が必要なデプロイジョブにだけ id-token: write を付け、テストだけのジョブには付けない——これだけで攻撃面が狭まります(SRP:ジョブごとに責務と権限を分ける)。リポジトリ全体のデフォルトを permissions: {}(全拒否)にして、必要なジョブで足していくのが堅牢です。

1.1 sub クレームの形——信頼範囲を決める主役

OIDC トークンに刻まれる最重要クレームが sub(subject)です。GitHub 公式が示す形式は、コンテキストによって変わります。

  • ブランチrepo:octo-org/octo-repo:ref:refs/heads/main
  • environmentrepo:octo-org/octo-repo:environment:prod
  • プルリクエストrepo:OWNER/REPO:pull_request
  • タグrepo:OWNER/REPO:ref:refs/tags/v1.0.0

そのほかトークンに含まれる主なクレーム(公式)は次のとおりです。

クレーム意味
isshttps://token.actions.githubusercontent.com発行者(GitHub の OIDC プロバイダ)
subrepo:octo-org/octo-repo:ref:refs/heads/main誰が:repo / ブランチ / environment
aud(クラウド側で固定。AWSなら sts.amazonaws.com誰宛て:トークンの受け手
repositoryocto-org/octo-repoリポジトリ名
refrefs/heads/mainブランチ/タグ参照
environmentprodenvironment 名
job_workflow_refワークフローファイル参照どのワークフロー定義から実行されたか

クラウド側の信頼設定は、これらのクレームを照合します。特に subaud の2つを正しく絞ることが、鍵レス構成の安全性のほぼすべてです。次章から、AWS と GCP それぞれで具体的に見ていきます。


2. AWS:IAM OIDC プロバイダ + ロール信頼ポリシー

AWS 側の鍵レス化は、2つの設定で成立します。

  1. IAM OIDC アイデンティティプロバイダを作る(GitHub を信頼の起点として AWS に登録)。
  2. IAM ロールを作り、その**信頼ポリシー(trust policy)**で「どの repo/branch/environment からの OIDC トークンなら AssumeRole を許すか」を条件付ける。

2.1 IAM OIDC プロバイダ

GitHub を AWS の信頼できる ID プロバイダとして登録します。公式が示す設定値は次のとおりです。

  • プロバイダURLhttps://token.actions.githubusercontent.com
  • オーディエンス(Audience)sts.amazonaws.com(公式アクション aws-actions/configure-aws-credentials を使う場合)

このオーディエンスが、OIDC トークンの aud クレームと一致することを AWS が検証します。「このトークンはSTS宛てに発行されたものか」を確認する仕組みです。

2.2 ロール信頼ポリシー(trust policy)——ここが心臓部

OIDC プロバイダを登録しただけでは、まだ何もできません。「どの GitHub 実行ならこのロールになれるか」を条件で縛るのが信頼ポリシーです。GitHub 公式が示すJSONはこうです。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456123456:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": "repo:octo-org/octo-repo:*"
        }
      }
    }
  ]
}

各要素の意味を分解します。

  • Action: sts:AssumeRoleWithWebIdentity:Web ID(=OIDCトークン)でロールを引き受ける、という専用アクション。長期キーの世界には存在しない、federation 専用の入口です。
  • Principal.Federated:先ほど登録した OIDC プロバイダの ARN。「GitHub の OIDC を信頼する」という宣言。
  • aud 条件(StringEquals):トークンが sts.amazonaws.com 宛てであることを厳密一致で要求。これを省くと、別用途のトークンを流用される余地が生まれます。aud は必ず StringEquals で固定してください。
  • sub 条件:ここで「どの repo/branch/environment か」を絞ります。

2.3 sub をどこまで絞るか——ワイルドカードの危険

上の公式例は repo:octo-org/octo-repo:* というワイルドカードを使っています。これは「このリポジトリのあらゆるブランチ・PR・environment から AssumeRole を許す」という意味です。

GitHub 公式は、この点にはっきり警告しています。sub 条件にワイルドカード(*)を使うと「任意のブランチ、プルリクエストのマージブランチ、または environment(any branch, pull request merge branch, or environment)」がそのロールを引き受けられてしまう、と。

これは本番では避けるべきです。たとえば外部コントリビュータが送ってきた PR からも、このロール(=本番権限)が握られる可能性が生まれます。本番デプロイ用ロールなら、StringEquals1点に固定します。

"Condition": {
  "StringEquals": {
    "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
    "token.actions.githubusercontent.com:sub": "repo:octo-org/octo-repo:ref:refs/heads/main"
  }
}

これで「octo-org/octo-repomain ブランチからの実行だけ」がこのロールになれます。さらに堅くするなら、environment で縛ります。

"token.actions.githubusercontent.com:sub": "repo:octo-org/octo-repo:environment:production"

environment で縛る利点は次節で説明する environment protection rules と組み合わさることです。「production environment に紐づいた実行」かつ「承認者が承認した実行」だけが本番ロールを引ける——という多層防御になります。

絞り込みの原則(ETC:変更が起きる単位を意識する):信頼条件は「この権限を、いつ・どこから使わせたいか」と1対1で対応させます。本番デプロイは environment:production に固定、ステージングは ref:refs/heads/develop に、という具合に用途ごとにロールを分ける。1つのロールに複数用途を StringLike で詰め込むと、絞り込みが緩くなり、結局ワイルドカードと大差なくなります。

2.4 ワークフロー側:aws-actions/configure-aws-credentials

クラウド側の準備ができたら、ワークフローはこうなります。

name: Deploy to AWS
on:
  push:
    branches: [main]

permissions:
  id-token: write   # OIDC トークン発行(必須)
  contents: read    # checkout 用

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production   # environment 縛り + 承認ゲートと連動
    steps:
      - uses: actions/checkout@v4

      - name: Configure AWS credentials (OIDC)
        uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502
        with:
          role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
          role-session-name: github-actions-deploy
          aws-region: ap-northeast-1

      # ここから先は一時クレデンシャルで AWS を操作(鍵は一切なし)
      - name: Push image to ECR
        run: |
          aws sts get-caller-identity   # 引き受けたロールの確認
          # docker build / push, ECS デプロイ など

ポイントは role-to-assume です。保存された鍵ではなく「引き受けたいロールのARN」を渡すだけ。アクションが内部で OIDC トークンを取得し、AssumeRoleWithWebIdentity を呼び、短命クレデンシャルを環境変数に展開してくれます。AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY も、どこにも書きません。

Action のピン留め(セキュリティ):公式例が configure-aws-credentialsコミットSHA(@e3dd...)で固定しているのは偶然ではありません。サードパーティ Action は乗っ取られ得るため、@v4 のような可変タグではなくフルSHAでピン留めするのが、サプライチェーン攻撃への基本防御です。OIDC で鍵を消しても、悪意ある Action に一時クレデンシャルを盗ませては本末転倒です。


3. GCP:Workload Identity Federation(WIF)

GCP 側の鍵レス化は Workload Identity Federation(WIF) で行います。GCP 公式の定義はこうです。WIF は「**サービスアカウント鍵の代わりに、フェデレーションされたID(federated identities instead of a service account key)**を使って」外部ワークロードに GCP リソースへのアクセスを与える仕組みであり、「サービスアカウント鍵に伴う保守とセキュリティの負担を排除する(eliminates the maintenance and security burden associated with service account keys)」。

サービスアカウント鍵(JSON)は、AWS の長期アクセスキーと同じ「漏れたら終わり」の長期秘密です。WIF はこれを短命の OAuth 2.0 トークンに置き換えます。

3.1 登場人物:Pool と Provider

WIF には2つの構成要素があります(公式用語)。

  • Workload Identity Pool(ワークロードアイデンティティプール):外部のIDを管理する入れ物。公式は「非Google Cloud環境ごとに1つのプールを推奨」しています。GitHub Actions 用に1つ作る、というイメージです。
  • Workload Identity Pool Provider(プロバイダ):GCP とあなたのIDプロバイダ(ここでは GitHub)との関係を記述するもの。GitHub の OIDC issuer をここに設定します。

3.2 Attribute Mapping と Attribute Condition——信頼範囲の絞り込み

WIF の安全性を決めるのが、この2つです。AWS の sub 条件に相当します。

Attribute Mapping(属性マッピング) は、GitHub のトークンのクレームを GCP の属性に変換します。CEL(Common Expression Language)で書きます。公式が示す代表例:

  • google.subject=assertion.sub(GitHub の sub を GCP の主体IDにマッピング)
  • attribute.repository=assertion.repository(リポジトリ名を独自属性として取り込む)

Attribute Condition(属性条件) は、「どの外部IDなら認証を許すか」を CEL で制限します。公式が強く推奨する設定はこれです。

assertion.repository == 'OWNER/REPO'

公式はこの条件の意義を明確に述べています。これは「他プラットフォーム向けに発行された資格情報が GCP にアクセスするのを防ぎ、confused deputy 問題を緩和する」。

WIF の最重要落とし穴:Attribute Condition を設定しない、あるいは緩く設定すると、GitHub の任意のリポジトリ(=世界中の他人のリポジトリ)から発行されたトークンが、あなたの GCP プロバイダで受理されかねません。GitHub の OIDC issuer は全 GitHub ユーザー共通だからです。プロバイダ作成時に assertion.repository == 'OWNER/REPO' を必ず付ける——これは AWS の sub をワイルドカードにしない、と完全に同じ問題です。さらに assertion.ref == 'refs/heads/main' を足せばブランチまで縛れます。

3.3 直接アクセス vs サービスアカウント インパーソネーション

WIF には2つのアクセスモデルがあります(公式)。

  • Direct resource access(直接リソースアクセス):外部IDに、GCP リソース上のロールを直接付与する。「中間のサービスアカウントも鍵も存在しない(no intermediate service accounts or keys)」最もシンプルな形。
  • Service account impersonation(サービスアカウント インパーソネーション):外部IDが中間のサービスアカウントを介してリソースにアクセスする。既存のサービスアカウント権限を再利用したい場合や、一部 GCP サービスがインパーソネーション前提の場合に使います。

鍵を作らない」という目的はどちらも達成します。違いは中間サービスアカウントを挟むかどうかだけです。新規なら直接アクセスがシンプル(KISS)、既存資産を活かすならインパーソネーション、と選びます。

3.4 ワークフロー側:google-github-actions/auth

ワークフローは公式アクション google-github-actions/auth を使います。GCP 公式は、このアクションが「GitHub の OIDC トークンを Google Cloud のアクセストークンと交換する(exchanges a GitHub OIDC token for a Google Cloud access token)」と説明しています。

name: Deploy to GCP
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write   # OIDC トークン発行(必須)
    steps:
      - uses: actions/checkout@v4   # auth の前に必ず checkout

      - id: auth
        name: Authenticate to Google Cloud (WIF)
        uses: google-github-actions/auth@v3
        with:
          project_id: my-project
          # プロジェクト番号 + プール名 + プロバイダ名のフル識別子
          workload_identity_provider: projects/123456789/locations/global/workloadIdentityPools/github/providers/my-repo
          service_account: deployer@my-project.iam.gserviceaccount.com  # インパーソネーション時のみ

      # 以降は短命トークンで GCP を操作(鍵JSONは一切なし)
      - name: Push to Artifact Registry / deploy Cloud Run
        run: gcloud run deploy ...

入力の意味(公式):

  • workload_identity_provider:「プロジェクト番号・プール名・プロバイダ名を含むフル識別子」。形式は projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL/providers/PROVIDER
  • service_accountインパーソネーションする場合のみ指定するサービスアカウントのメール。直接アクセス(Direct WIF)なら省略します。
  • create_credentials_file:デフォルト true。後続の gcloud / SDK が参照する一時的な認証ファイルを生成。
  • token_formataccess_token または id_token。Cloud Run の IAM 呼び出しなどで id_token が要るケースがあります。

公式の重要注意google-github-actions/authactions/checkout の後に置くこと。順序を逆にすると、認証情報ファイルが checkout で上書き/削除される事故が起きます。


4. AWS と GCP の対応表:頭の中を1枚で整理する

両クラウドを行き来すると混乱しがちなので、設定要素の対応を1枚にまとめます。「鍵を消す」という同じゴールに、各クラウドの語彙でどう到達するかの地図です。

役割AWSGCP(WIF)
GitHub を信頼の起点に登録IAM OIDC アイデンティティプロバイダWorkload Identity Pool Provider
GitHub の issuertoken.actions.githubusercontent.com同左(GitHub の OIDC issuer)
トークンの受け手(aud)sts.amazonaws.comプロバイダ設定の audience
「誰からか」を絞る条件信頼ポリシーの sub 条件Attribute Conditionassertion.repository == '...'
クレーム→内部属性の変換(条件で直接参照)Attribute Mappinggoogle.subject=assertion.sub
実権限を与える対象IAM ロールリソースへの直接ロール付与 or サービスアカウント
トークン交換の専用APIsts:AssumeRoleWithWebIdentityGCP Security Token Service
ワークフロー側アクションaws-actions/configure-aws-credentialsgoogle-github-actions/auth
ワークフロー側で渡す値role-to-assume(ロールARN)workload_identity_provider(+service_account
共通の前提permissions: id-token: writepermissions: id-token: write

構造はまったく同じだと分かります。「GitHub を信頼登録 → クレームで発行元を絞る → 短命クレデンシャルを払い出す」。語彙が違うだけで、設計判断は両クラウドで共通(DRY:同じ思考を2回しない)。だからこそ、片方を理解すればもう片方も素早く組めます。


5. Terraform で OIDC プロバイダ / WIF プールを作る

GUI でポチポチ作るのは再現性ゼロで、設定ミス(特に sub / Attribute Condition の絞り込み漏れ)を生みます。OIDC の信頼関係こそ IaC で管理すべきです。誰がいつ何を信頼させたかが、コードレビューと履歴に残るからです。

5.1 AWS:OIDC プロバイダ + ロール

# GitHub Actions を信頼する IAM OIDC プロバイダ
resource "aws_iam_openid_connect_provider" "github" {
  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"] # = aud
  # サムプリントは AWS が自動検証するため、近年は固定値ハードコード不要
  thumbprint_list = ["ffffffffffffffffffffffffffffffffffffffff"]
}

# main ブランチ + production environment からの実行だけを信頼するロール
data "aws_iam_policy_document" "trust" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github.arn]
    }

    # aud は厳密一致で固定(流用防止)
    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    # sub も厳密一致で1点固定(ワイルドカード禁止)
    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:sub"
      values   = ["repo:octo-org/octo-repo:environment:production"]
    }
  }
}

resource "aws_iam_role" "github_deploy" {
  name               = "github-actions-deploy"
  assume_role_policy = data.aws_iam_policy_document.trust.json
}

# 最小権限:このロールが触れてよいリソースだけを許可(例は雛形)
resource "aws_iam_role_policy" "deploy_least_privilege" {
  role = aws_iam_role.github_deploy.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["ecr:GetAuthorizationToken", "ecr:BatchGetImage", "ecr:PutImage"]
      Resource = "*" # 本番では特定 ECR リポジトリ ARN に絞る
    }]
  })
}

conditionStringEquals で書いている点に注目してください。ワイルドカードを使う StringLike ではなく、1点固定です。コードに残るので「なぜここを固定したか」がレビューで議論でき、緩める変更には必ず承認が要る——これが IaC で信頼関係を扱う最大の利点です。

5.2 GCP:WIF プール + プロバイダ + 属性条件

# GitHub Actions 用の Workload Identity Pool
resource "google_iam_workload_identity_pool" "github" {
  workload_identity_pool_id = "github"
  display_name              = "GitHub Actions"
}

# GitHub を OIDC プロバイダとして登録
resource "google_iam_workload_identity_pool_provider" "github" {
  workload_identity_pool_id          = google_iam_workload_identity_pool.github.workload_identity_pool_id
  workload_identity_pool_provider_id = "my-repo"

  oidc {
    issuer_uri = "https://token.actions.githubusercontent.com"
  }

  # Attribute Mapping:GitHub のクレームを GCP 属性へ
  attribute_mapping = {
    "google.subject"       = "assertion.sub"
    "attribute.repository" = "assertion.repository"
    "attribute.ref"        = "assertion.ref"
  }

  # Attribute Condition:このリポジトリ以外のトークンを門前払い(必須)
  attribute_condition = "assertion.repository == 'octo-org/octo-repo'"
}

attribute_condition必ず設定してください。これが無いと、前述のとおり世界中の任意の GitHub リポジトリのトークンがプロバイダに受理され得ます。assertion.repository == 'octo-org/octo-repo' で発行元リポジトリを固定し、必要なら && assertion.ref == 'refs/heads/main' でブランチまで縛ります。

HCL の注意oidc/attribute_mapping のフィールド名や google_iam_workload_identity_pool_provider のスキーマは Provider バージョンで差異が出ることがあります。上記は構造を示す雛形です。本番適用前に、お使いの Google Provider バージョンのリソース仕様を確認してください(要確認)。


6. 多層防御:信頼範囲をさらに固くする実務

OIDC を入れただけで満足しないでください。鍵レス化は「土台」であって、その上に信頼範囲を絞る運用を積み上げてはじめて堅牢になります。実案件で効いた打ち手を挙げます。

6.1 environment protection rules と組み合わせる

subenvironment:production で縛り、かつ GitHub の environment protection rules(必須レビュアー、待機タイマー、デプロイ可能ブランチ制限)を設定します。すると本番ロールを引けるのは——

  1. production environment に紐づいた実行で、
  2. かつ承認者が承認した実行だけ、

という二重ゲートになります。OIDC の sub 検証(クラウド側)と、environment 承認(GitHub 側)が直交する防御層として働きます。

6.2 PR からの濫用を断つ

最も多い事故が「フォークやPRからの実行で本番権限が握られる」ケースです。対策は明快です。

  • 本番ロールの subref:refs/heads/main または environment:production に固定し、pull_request 由来のトークン(subrepo:OWNER/REPO:pull_request)を最初から弾く
  • ワイルドカード repo:OWNER/REPO:* は、まさにこの pull_request を含めてしまうので本番ロールには絶対に使わない
  • 検証用のジョブには、本番とは別の読み取り専用ロールを割り当て、書き込み権限を渡さない。

GitHub 公式が「ワイルドカードは任意のブランチ・PRマージブランチ・environment を許す」と警告するのは、この PR 濫用を念頭に置いています。

6.3 ロールは「用途 × 環境」で分割する

1つの全能ロールを共有するのは、長期キーを共有するのと同じ過ちです。用途と環境ごとにロールを分け、各ロールには最小権限だけを与えます。

ジョブ信頼する sub / 条件付与する権限(最小)
本番デプロイenvironment:productionECR push + 該当 ECS サービス更新のみ
ステージングデプロイref:refs/heads/developステージング ECR / ECS のみ
PRでの検証(read-only)pull_request読み取り専用(plan / lint のみ)

これは私が AWS 側で実際に採っている分割です。Terraform は plan(PRで tfsec による静的解析)/ apply(権限境界付きロール)/ drift検知(定期 cron → 差分を Issue 起票) をワークフローで分離し、それぞれに別の OIDC ロールを割り当てています。**「PR では絶対に apply できない」**という構造が、最小権限を保証します(SRP:plan と apply は別の責務 = 別ロール)。

6.4 aud は絶対に省かない

AWS の aud 条件、GCP のプロバイダ audience は、トークンの宛先を保証します。これを緩めると、別用途に発行されたトークンの流用余地が生まれます。aud は常に StringEquals で固定——例外なし、と覚えてください。


7. 実例:両クラウドで鍵レスCI/CDを運用する

抽象論で終わらせないために、私が実際に運用している2つの構成を示します。AWS と GCP の両方で、長期キーは1本も存在しません。

7.1 GCP 側:放送事業者の社内AIプラットフォーム

国内大手放送事業者向けの社内AIプラットフォーム(事例)では、CI/CD を Workload Identity Federation(OIDC)で鍵レスにしています。

  • GitHub Actions からサービスアカウント鍵(JSON)を一切発行せずに GCP へ認証。漏れたら終わりの鍵が、リポジトリにもCIにも存在しない。
  • Cloud Build で stg / prod を出し分け、デプロイ先を環境ごとに分離。
  • DBマイグレーションは専用ジョブに分離し、アプリデプロイと責務を分けた(SRP)。マイグレーションの失敗がデプロイ全体を巻き込まない。
  • CodeQL・依存更新も自動化し、サプライチェーン側の防御も常時稼働。
  • インフラは100% Terraform(約71モジュール)。WIF プール/プロバイダ/属性条件も当然 IaC 管理で、信頼関係の変更が必ずレビューに乗る。

7.2 AWS 側:木材業界DXのデプロイパイプライン

別案件(木材業界DX)では AWS 側を OIDC で組んでいます。

  • GitHub Actions OIDC(長期キーなし)で ECR → ECS 強制デプロイAWS_SECRET_ACCESS_KEY を Secrets に置かず、ロール ARN を渡すだけ。
  • S3同期 + CloudFront 無効化もOIDCロール経由。
  • Terraform は plan(PRで tfsec)/ apply(権限境界付きロール)/ drift検知(定期 cron → Issue 起票) を分離し、各段階に別ロールを割り当て。

この2案件を通じて言えるのは、OIDC 鍵レス化はクラウドを問わず再現できる定石だということです。AWS の sub 条件と GCP の Attribute Condition は語彙こそ違え、やっていることは同じ——「発行元を絞って、短命トークンを払い出す」。一度身につければ、どちらのクラウドでも数十分で組めます。


8. まとめ:鍵レスCI/CD チートシート

最後に、迷ったときの早見表です。

  • 大原則:CIに長期キーを置かない。AWS_SECRET_ACCESS_KEY も サービスアカウント鍵JSON も Secrets から消す。
  • 共通の第一歩:ワークフローに permissions: id-token: write(+ checkout 用 contents: read)。ジョブ単位で最小化する。
  • AWS:IAM OIDC プロバイダ(token.actions.githubusercontent.com / aud sts.amazonaws.com)→ ロール信頼ポリシーで audStringEquals 固定、subrepo:OWNER/REPO:environment:production などに1点固定 → ワークフローは aws-actions/configure-aws-credentialsrole-to-assume を渡すだけ。
  • GCP:Workload Identity Pool + Provider → Attribute Conditionassertion.repository == 'OWNER/REPO'必ず設定 → ワークフローは google-github-actions/authworkload_identity_provider(+必要なら service_account)を渡す。actions/checkoutに置く。
  • ワイルドカード厳禁sub* も、Attribute Condition の未設定も、「任意のブランチ・PR・他人のリポジトリ」を招き入れる。本番ロールは常に1点固定。
  • 多層防御:environment protection rules で承認ゲート、用途×環境でロール分割、PR には read-only ロール、Action はSHAピン留め。
  • IaC で管理:OIDC プロバイダ/WIF プール/信頼条件は Terraform に。信頼関係の変更を必ずレビューに乗せる。

長期キーは「漏れたら終わり」の静かな時限爆弾です。OIDC federation は、その爆弾そのものを撤去し、代わりに「1回限りで失効する、発行元が検証可能な短命トークン」に置き換えます。鍵レス化は、もはや上級者の贅沢ではなく、CI/CD の標準装備です。

私は、生成AI(Claude Code)を相棒に一人で開発を回すスタイルで、AWS と GCP の両方で鍵レスCI/CDを本番運用しています。Terraform で OIDC の信頼関係を IaC 化し、最小権限ロールと environment 承認ゲートまで含めて設計・実装・運用を一気通貫で担えます。「うちの GitHub Actions、まだ長期キー貼ってるんだけど…」——その置き換えこそ、最も費用対効果の高いセキュリティ投資です。要件整理の段階からでも、お気軽にご相談ください。


参考(公式ドキュメント)

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事で解説した技術の適用事例

国内大手放送事業者の社内AIプラットフォーム(Workload Identity Federation で鍵レスCI/CDを実現)

ケーススタディを見る