Claude Media
WIFをGitHub Actionsと連携する — OIDCでAPIキーレスにデプロイ

WIFをGitHub Actionsと連携する — OIDCでAPIキーレスにデプロイ

GitHub ActionsのワークフローからAPIキーを持たずにClaude APIを呼ぶ、Workload Identity FederationのOIDC設定手順を追います。

GitHub ActionsでAPIキーを持たずにClaude APIを呼ぶ仕組み

GitHub Actionsのワークフロー実行は、GitHubがホストする発行者https://token.actions.githubusercontent.comから署名済みのIDトークンを取得できます。Workload Identity Federation(WIF)は、このトークンをPOST /v1/oauth/tokenで短命のAnthropicアクセストークンに交換する仕組みです。リポジトリにANTHROPIC_API_KEYのシークレットを保存しなくても、CIジョブからClaude APIを呼べるようになります。

手順

ジョブがAPIキーなしで認証される流れ

  1. 1

    GitHubがIDトークンを発行する

    id-token: writeを付けたジョブが、audienceを指定してJWTを取得します。

  2. 2

    Anthropicがルールと照合して交換する

    連合ルールのID・サービスアカウントID・組織IDと一緒にJWTを送ると、ルールの条件に合う場合だけsk-ant-oat01-で始まるアクセストークンが返ります。

  3. 3

    SDKがそのトークンでAPIを呼ぶ

    失効の前にSDKが自動で交換し直すので、アプリ側のコードにキーは出てきません。

WIFの基本用語(サービスアカウント・連合発行者・連合ルール)を押さえたい場合はWIFリファレンスを先に読むと設定が追いやすくなります。

連合ルールが照合するsubクレームの形

トークンのsubクレームには、リポジトリとトリガーの文脈が埋め込まれます。連合ルールはこのクレームとrepository_owner・refなどを照合して、どのワークフロー実行を信頼するかを決めます。

形式

`sub`の末尾が変わる3パターン

  • ブランチへのpush

    repo:<owner>/<repo>:ref:refs/heads/<branch>

  • pull requestの実行

    repo:<owner>/<repo>:pull_request

  • 環境ゲート付きのデプロイ

    repo:<owner>/<repo>:environment:<name>

GitHub側の定義では、pull_requestが入るのはジョブが環境を参照していないときだけです。pull requestで起動したジョブでもenvironmentを指定していれば、subはenvironment:<name>の形になります。環境名に:が含まれる場合は%3Aに置き換わります。

新しいリポジトリではsubにIDが入る

2026年7月15日より後に作成されたリポジトリは、既定のsubにオーナーIDとリポジトリIDが入る形式になります。名前空間が再利用されると別のオーナーが同じsubを作れてしまうため、IDを含む形式に変わりました。

くらべる

`sub`の形式は作成時期で変わる

2026年7月15日より前に作成

従来の形式

repo:octo-org/octo-repo:ref:refs/heads/main

オプトインするか、2026年7月15日以降にリネーム・移管するまでは、この形式のままです。オプトインは、組織またはリポジトリのOIDC設定画面かREST APIで行います。

それより後に作成

IDつきの形式

repo:octo-org@123456/octo-repo@456789:ref:refs/heads/main

2026年7月15日以降のリネームや移管で、従来の形式のリポジトリもこちらに移ります。

subject_prefixは、末尾が*でなければsubとの完全一致、*で終われば前方一致です。どちらでも、後述のrepo:your-org/your-repo:ref:refs/heads/mainのような名前だけの書き方は、IDつきのsub(repo:your-org@123456/your-repo@456789:…)にマッチしません。新しいリポジトリでルールを作るときは、まず実際のsubをデコードして確かめます。オーナーとリポジトリの絞り込みは、claimsのrepository_owner・repositoryで受ける手もあります。GitHub側にはIDを返すrepository_owner_id・repository_idのクレームもあり、claimsで使うときはデコードした実際の値を書き写します。GitHub Enterprise Serverはこの変更の対象外です。

前提条件

設定に入る前に、次の4点を揃えておきます。

  • GitHub側の権限: 対象リポジトリのワークフローファイルを編集でき、id-token: write権限を付与できること。
  • Anthropic組織側の権限: Claude Consoleでサービスアカウント・連合発行者・連合ルールを作成できるロール(admin・owner・primary owner)を持っていること。ウィザードを使わずAdmin APIから直接リソースを作る場合も、この3ロールのいずれかで発行したOAuthトークン(org:adminスコープ)が必要です。
  • 組織ID: Claude ConsoleのSettings → Organizationで確認できる組織ID(ANTHROPIC_ORGANIZATION_ID)。トークン交換のたびに必須パラメータとして渡します。
  • ワークスペースの割り当て: サービスアカウントは組織直下に作られ、既定のワークスペースには暗黙のメンバーとして入ります。それ以外のワークスペースで使う場合は、明示的にメンバー追加が必要です。連合ルールが複数のワークスペースを対象にする場合は、トークン交換時にANTHROPIC_WORKSPACE_IDも渡します。交換時、Anthropicは連合ルールのワークスペースがサービスアカウントのメンバーシップのいずれかと一致するかを確認し、発行後のトークンはそのワークスペースのレート制限と使用量集計に従います。

CI側からAdmin APIを叩いて連合設定自体をInfrastructure as Codeで管理したい場合は、まずorg:adminスコープを持つ連合ルールを1本だけConsoleで手動作成します。以降の発行者・ルール管理は、そのワークロードに任せる順番です。OAuthのorg:adminトークンで作成・変更できるのはworkspace:developerかworkspace:inferenceのルールだけで、org:adminのルール自体は必ずConsoleから作ります。

このルールはsubject_prefixを保護ブランチへの完全一致(例: repo:your-org/your-repo:ref:refs/heads/main)に絞り、末尾を*にしたワイルドカード一致にしないことが重要です。ワイルドカードにするとフォークからのpull request実行にもマッチします。リポジトリにpull requestを開ける人なら、org:adminのトークンを取得できてしまいます。この1本のルール専用に発行者を別に登録しておく手もあります。org:adminなどのルールが紐づく発行者は、OAuth経由では更新できないためです。

ワークフロー側の設定

GitHubは、明示的にリクエストしたジョブにしかIDトークンを発行しません。ワークフローかジョブのレベルでid-token: write権限を追加します。

permissions:
  id-token: write
  contents: read

ジョブの内部では、ランナーがACTIONS_ID_TOKEN_REQUEST_URLとACTIONS_ID_TOKEN_REQUEST_TOKENという2つの環境変数を公開します。リクエストURLをベアラー資格情報として呼び出し、audienceをクエリパラメータで指定して、返ってきたJWTをファイルに書き出します。audienceを省くと、GitHubの既定値はリポジトリオーナーのURLになり、ルールのaudience(https://api.anthropic.com)と一致しません。

curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
  "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" \
  | jq -r .value > /tmp/gha-jwt

JavaScriptで書きたい場合は、actions/github-scriptのcore.getIDToken(audience)で同じ処理ができます。

- name: Fetch GitHub OIDC token
  uses: actions/github-script@v8
  with:
    script: |
      const fs = require('fs');
      const token = await core.getIDToken('https://api.anthropic.com');
      fs.writeFileSync('/tmp/gha-jwt', token);

デコードしたトークンには、ワークフロー実行を説明するクレームが含まれます。連合ルールはこれらを照合します。

{
  "iss": "https://token.actions.githubusercontent.com",
  "sub": "repo:your-org/your-repo:ref:refs/heads/main",
  "aud": "https://api.anthropic.com",
  "repository": "your-org/your-repo",
  "repository_owner": "your-org",
  "ref": "refs/heads/main",
  "sha": "abc123...",
  "workflow": "CI",
  "actor": "octocat",
  "event_name": "push"
}

subの完全な形式一覧はGitHubのOIDCリファレンスにまとまっています。2026年7月15日より後に作成したリポジトリでは、上のsubがrepo:your-org@123456/your-repo@456789:ref:refs/heads/mainの形になります。

Anthropic側の設定

Claude ConsoleのSettings → Workload identityを開き、Connect workloadからGitHub Actionsタイルを選びます。発行者の登録・サービスアカウントの作成・連合ルールの作成は、ウィザードが順に案内します。作成後、ウィザードは15分間、トークン交換の成功を待ち受けます。その間にワークフローを1回走らせると、接続を確かめられます。

ウィザードが作成するリソースは、ウィザードで直接入力する場合も、Admin APIへ送る場合も値は共通です。

連合発行者: GitHubはOIDC discoveryドキュメントとJWKSを公開しているため、discoveryモードを使います。GitHubが鍵をローテーションしても、Anthropicが自動で追随します。

{
  "name": "github-actions",
  "issuer_url": "https://token.actions.githubusercontent.com",
  "jwks": { "type": "discovery" }
}

連合ルール: 信頼するワークフロー実行だけにマッチさせます。安全な絞り込み方は後述の「認証を許可するワークフローを絞り込む」の節にまとめています。

{
  "name": "gha-main",
  "issuer_id": "fdis_...",
  "match": {
    "subject_prefix": "repo:your-org/your-repo:ref:refs/heads/main",
    "audience": "https://api.anthropic.com",
    "claims": {
      "repository_owner": "your-org"
    }
  },
  "target": {
    "type": "service_account",
    "service_account_id": "svac_..."
  },
  "workspace_id": "wrkspc_...",
  "oauth_scope": "workspace:developer",
  "token_lifetime_seconds": 600
}

このsubject_prefixは従来の形式です。リポジトリを2026年7月15日より後に作成した場合は、repo:your-org@<オーナーID>/your-repo@<リポジトリID>:ref:refs/heads/mainの形に書き換えます。

subject_prefixをrepo:your-org/your-repo:*まで緩めるのは、同じリポジトリの複数イベント種別をまとめて1つのルールでマッチさせたい場合だけにします。末尾がref:...・environment:...・pull_requestで変わるため、緩めるならclaims.refのような制約と組み合わせます。

トークンを取得してClaude APIを呼ぶ

連合用の環境変数をジョブに設定し、SDKを通常どおり呼び出すだけです。Anthropic()はANTHROPIC_IDENTITY_TOKEN_FILEを読み、初回リクエストでJWTを交換し、失効前に自動で更新します。

name: Call Claude
on: push
 
permissions:
  id-token: write
  contents: read
 
jobs:
  call-claude:
    runs-on: ubuntu-latest
    env:
      ANTHROPIC_FEDERATION_RULE_ID: fdrl_...
      ANTHROPIC_ORGANIZATION_ID: 00000000-0000-0000-0000-000000000000
      ANTHROPIC_SERVICE_ACCOUNT_ID: svac_...
      ANTHROPIC_WORKSPACE_ID: wrkspc_...
      ANTHROPIC_IDENTITY_TOKEN_FILE: /tmp/gha-jwt
    steps:
      - uses: actions/checkout@v5
      - name: Fetch GitHub OIDC token
        run: |
          curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
            "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" \
            | jq -r .value > "$ANTHROPIC_IDENTITY_TOKEN_FILE"
      - name: Run your script
        run: |
          pip install anthropic
          python your_script.py

Python SDKを直接呼ぶ場合、コード側で書くのは通常のanthropic.Anthropic()初期化だけです。

import anthropic
 
# ジョブの環境からANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
# ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID、ANTHROPIC_IDENTITY_TOKEN_FILEを読む
client = anthropic.Anthropic()
 
message = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello, Claude"}],
)
print(next(block.text for block in message.content if block.type == "text"))

GitHubが発行するIDトークンはおよそ5分で失効しますが、トークンリクエストのエンドポイント自体はジョブの実行中ずっと有効なため、いつでも新しいトークンを取得できます。SDKは初回利用時に交換したAnthropicアクセストークンをキャッシュします。Anthropicトークンの有効期限より長く走るジョブでは、ANTHROPIC_IDENTITY_TOKEN_FILEを定期的に上書きするステップを挟みます。もう1つの方法は、ACTIONS_ID_TOKEN_REQUEST_URLを直接呼ぶトークンプロバイダーのコールバックをSDKに渡すことです。

トークンのライフサイクル

交換で発行されるAnthropicアクセストークンの有効期限は、次の2つのうち短い方になります。

  1. 連合ルールのtoken_lifetime_seconds(既定3,600秒。ウィザード経由の作成では600秒が初期値)
  2. 提示したGitHubのIDトークンの残り有効期限の2倍

どちらが優先されても最短60秒は保証されます。GitHubのIDトークンは約5分(300秒)で失効するため、2番目の上限は最大でも約600秒です。token_lifetime_secondsを3,600秒のまま使っても、発行されるトークンの寿命は600秒前後に収まります。

SDKはトークンを2段階でリフレッシュします。

  • 予備リフレッシュ(失効の120秒前): 新しい交換を試みます。交換エンドポイントに到達できない場合は、まだ有効なキャッシュ済みトークン(残り約90秒)をそのまま使い続けます。
  • 必須リフレッシュ(失効の30秒前): ここでの交換失敗はエラーとして送出されます。キャッシュ済みトークンは失効に近すぎて安全に使えないためです。

GitHubのIDトークンはjtiクレームを持つため、既定で使い捨て(single-use)として扱われます。一度交換に使ったIDトークンを再提示すると、認証履歴にjti_reusedという理由で拒否記録が残ります。/tmp/gha-jwtをリトライループの中で使い回すと踏みやすい失敗なので、リフレッシュのたびに新しいIDトークンを取得し直す実装にします。発行者ごとにcheck_jtiを無効にする方法もありますが、リプレイ保護がその発行者の全ルールで外れるため、Anthropicは最後の手段としています。

設定を検証する

交換に成功すると、sk-ant-oat01-で始まるaccess_tokenとexpires_inが返ります。拒否された場合は401のauthentication_error(メッセージはAuthentication failedという固定文言)になり、どのチェックで落ちたかは応答から読み取れません。失敗理由は多くの場合、認証履歴ページの該当エントリに記録されます。履歴に出ない400系は、応答メッセージが原因を直接示します。失敗が起きたらまず履歴を開きます。

履歴で原因が絞れないときは、送ったJWTをデコードして、ルールの設定と1項目ずつ突き合わせます。

jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson' < /tmp/gha-jwt

突き合わせでは、issが登録したissuer_urlと1文字単位で一致するか、audがルールのaudienceと完全一致するか、subがsubject_prefixと大文字小文字まで含めて合うかを見ます。exp・nbf・iatは30秒のずれまで許容されるため、ランナーの時計が大きくずれていると、有効なトークンでも拒否されます。

エントリのreason欄からよくある原因を逆引きすると次のようになります。

reason典型的な原因GitHub Actions側の対処
match_subject_prefix典型的な原因subクレームの形式不一致(最も多い)GitHub Actions側の対処実際のsubをデコードし、subject_prefixをそのイベントとリポジトリの形式に合わせる
jti_reused典型的な原因同じIDトークンを2回目の交換に再提示したGitHub Actions側の対処リトライ・キャッシュ処理を見直し、交換のたびに新しいIDトークンを取得する
workspace_id_required典型的な原因ルールが複数のワークスペースを対象にしているのに、リクエストがワークスペースを指定していないGitHub Actions側の対処ANTHROPIC_WORKSPACE_IDをジョブの環境に追加する
(交換が起きない)典型的な原因シェルやCIシークレットに残ったANTHROPIC_API_KEYが連合より優先されるGitHub Actions側の対処ワークフローの全ステップと組織のCIシークレットから旧キーを削除し、ant auth statusで勝っている資格情報を確認する

資格情報の優先順位は、コンストラクタ引数、ANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKEN、ANTHROPIC_PROFILE、連合の環境変数、既定のプロファイルの順で、最初に見つかったものが使われます。旧キーを残したまま連合を足すと、キーが勝ち続けて連合は使われません。

認証を許可するワークフローを絞り込む

repo:your-org/*だけのsubject_prefixは、組織内のすべてのリポジトリにマッチしてしまいます。refの制約がなければ、フォークから開かれたpull request実行にもマッチするため、対象リポジトリへpull requestを開けるだれもが連合済みのAnthropicトークンを取得できてしまいます。

ルールのmatchブロックは、用途に合う最も狭いスコープに絞ります。

絞り込みやり方マッチしなくなるもの
単一リポジトリに固定やり方subject_prefix: "repo:your-org/your-repo:*"にするマッチしなくなるもの同じ組織の他のリポジトリ
保護ブランチに固定やり方claimsに"ref": "refs/heads/main"(またはリリースブランチ)を追加するマッチしなくなるものpull requestの実行とフィーチャーブランチ
ownerを固定やり方claimsに"repository_owner": "your-org"を追加するマッチしなくなるものsubの解釈の取りこぼしで通ってしまう別オーナー(多層防御)
デプロイ環境に固定やり方subject_prefix: "repo:your-org/your-repo:environment:production"にし、GitHub側でその環境にrequired reviewersを設定するマッチしなくなるものレビュー承認を経ていない実行

上の表のsubject_prefixも従来の形式です。2026年7月15日より後に作成したリポジトリでは、repo:your-org@<オーナーID>/your-repo@<リポジトリID>:…の形に読み替えます。

Dependabotの更新ジョブが発行するトークンでは、event_nameがdynamicになります(GitHubのOIDCリファレンス)。event_nameをclaimsで絞る場合は、ワークフローが実際に出すイベント名だけを許可します。

GCPやKubernetesとの違い

同じWIFでも、GitHub ActionsとGoogle Cloud・Kubernetesではトークンの発行元とクレームの形式が異なります。Google CloudのCloud Run・GKEから同じ仕組みを使う手順はWIFをGoogle Cloudと連携するにあります。自己管理のKubernetesクラスターのPodから使う手順はWIFをKubernetesと連携するにまとめています。

いずれの構成でも、トークン交換のリクエスト・レスポンス仕様と連合ルールの粒度設計は共通です。CEL条件式の書き方やトラブルシュートの手順はWIFリファレンスを参照してください。

まとめ

詰まりやすいのは、subの形式がリポジトリの作成時期とイベント種別で変わる点です。ルールを書く前にデコードしたsubを見て、保護ブランチ1本から始めると、拒否されたときに原因を切り分けやすくなります。

この記事を共有:XはてブLinkedIn