# 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で解説します。

- 公開日: 2026-06-24
- 著者: 友田 陽大
- タグ: CI/CD, セキュリティ, AWS, GCP, DevOps
- URL: https://tomodahinata.com/blog/github-actions-oidc-keyless-cicd-aws-gcp-guide

## 要点

- 問題は鍵が漏れることではなく、漏れても被害が止まらない無期限の鍵をCIに置くこと。OIDC federationの短命トークンに置き換える
- ワークフローの第一歩はpermissions: id-token: write。ただしこの権限自体はクラウドを変更せず、トークン発行を許すだけ
- AWSはIAM OIDCプロバイダ＋ロール信頼ポリシーで、audをStringEquals固定・subをenvironmentやブランチに1点固定する
- GCPはWorkload Identity Federationで、Attribute Conditionにassertion.repositoryの一致を必ず設定し他人のリポジトリを門前払いする
- subのワイルドカードとAttribute Condition未設定は厳禁。多層防御としてenvironment承認・用途別ロール分割・ActionのSHAピン留めを重ねる

---

「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を実現](/case-studies/broadcaster-ai-content-platform))では 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` ブロックで明示します。

```yaml
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`
- **environment**：`repo:octo-org/octo-repo:environment:prod`
- **プルリクエスト**：`repo:OWNER/REPO:pull_request`
- **タグ**：`repo:OWNER/REPO:ref:refs/tags/v1.0.0`

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

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

クラウド側の信頼設定は、これらのクレームを照合します。**特に `sub` と `aud` の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 プロバイダとして登録します。公式が示す設定値は次のとおりです。

- **プロバイダURL**：`https://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はこうです。

```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 からも、このロール(=本番権限)が握られる可能性が生まれます。本番デプロイ用ロールなら、`StringEquals` で**1点に固定**します。

```json
"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-repo` の `main` ブランチからの実行だけ**」がこのロールになれます。さらに堅くするなら、**environment** で縛ります。

```json
"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`

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

```yaml
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_ID` も `AWS_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 で制限します。公式が強く推奨する設定はこれです。

```text
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)**」と説明しています。

```yaml
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_format`**：`access_token` または `id_token`。Cloud Run の IAM 呼び出しなどで `id_token` が要るケースがあります。

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

---

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

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

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

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

---

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

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

### 5.1 AWS：OIDC プロバイダ + ロール

```hcl
# 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 に絞る
    }]
  })
}
```

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

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

```hcl
# 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 と組み合わせる

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

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

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

### 6.2 PR からの濫用を断つ

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

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

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

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

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

| ジョブ | 信頼する `sub` / 条件 | 付与する権限（最小） |
| --- | --- | --- |
| 本番デプロイ | `environment:production` | ECR 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プラットフォーム([事例](/case-studies/broadcaster-ai-content-platform))では、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`)→ ロール信頼ポリシーで `aud` を `StringEquals` 固定、`sub` を `repo:OWNER/REPO:environment:production` などに**1点固定** → ワークフローは `aws-actions/configure-aws-credentials` に `role-to-assume` を渡すだけ。
- **GCP**：Workload Identity Pool + Provider → **Attribute Condition** で `assertion.repository == 'OWNER/REPO'` を**必ず**設定 → ワークフローは `google-github-actions/auth` に `workload_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、まだ長期キー貼ってるんだけど…」**——その置き換えこそ、最も費用対効果の高いセキュリティ投資です。要件整理の段階からでも、お気軽にご相談ください。

---

### 参考（公式ドキュメント）

- [About security hardening with OpenID Connect（GitHub Docs）](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect) — OIDC の概念・`id-token: write`・`sub` クレーム形式・短命トークン
- [Configuring OpenID Connect in Amazon Web Services（GitHub Docs）](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-amazon-web-services) — IAM OIDC プロバイダ・信頼ポリシー(`aud`/`sub`)・`configure-aws-credentials`・ワイルドカード警告
- [Configuring OpenID Connect in Google Cloud Platform（GitHub Docs）](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-google-cloud-platform) — `google-github-actions/auth`・`workload_identity_provider`/`service_account`
- [Workload Identity Federation（Google Cloud Docs）](https://cloud.google.com/iam/docs/workload-identity-federation) — WIF の概念・プール/プロバイダ・Attribute Mapping/Condition・鍵を作らない直接アクセス
- [google-github-actions/auth（GitHub）](https://github.com/google-github-actions/auth) — Direct WIF / インパーソネーション・各入力(`workload_identity_provider` ほか)・checkout 順序
