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を呼べるようになります。

トークンのsubクレームには、リポジトリとトリガーの文脈が埋め込まれます。ブランチへのpushならrepo:<owner>/<repo>:ref:refs/heads/<branch>、pull requestの実行ならrepo:<owner>/<repo>:pull_request、環境ゲート付きのデプロイならrepo:<owner>/<repo>:environment:<name>という形式です。連合ルールはこのクレーム(とrepository_ownerrefなど)を照合して、どのワークフロー実行を信頼するかを決めます。

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

前提条件

設定に入る前に、次の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)。トークン交換のたびに必須パラメータとして渡します。
  • ワークスペースの割り当て: サービスアカウントは組織直下に作られますが、既定のワークスペース以外で使う場合は、そのワークスペースへ明示的にメンバー追加が必要です。連合ルールが複数のワークスペースを対象にする、またはall-workspaces指定になっている場合は、トークン交換時にANTHROPIC_WORKSPACE_IDも渡します。トークン交換時、Anthropicは連合ルールのワークスペースがサービスアカウントのワークスペースメンバーシップのいずれかと一致するかを確認し、発行後のトークンはそのワークスペースのレート制限と使用量集計に従います。

CI側からAdmin APIを叩いて連合設定自体をInfrastructure as Codeで管理したい場合は、org:adminスコープを持つ連合ルールをまず1本だけConsoleで手動作成し、そのワークロードに以降の発行者・ルール管理を任せる、という順番になります。org:adminスコープを持つルールはワークロード自身が自動でブートストラップすることはできず、必ずConsoleでの人手操作を経由します。この1本のルールはsubject_prefixを保護ブランチへの完全一致(例: repo:your-org/your-repo:ref:refs/heads/main)に絞り、末尾を*にしたワイルドカード一致にしないことが重要です。ワイルドカードにするとフォークからのpull request実行にもマッチし、org:admin権限を持つトークンを誰でも取得できてしまいます。

ワークフロー側の設定

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

permissions:
  id-token: write
  contents: read

ジョブの内部では、ランナーがACTIONS_ID_TOKEN_REQUEST_URLACTIONS_ID_TOKEN_REQUEST_TOKENという2つの環境変数を公開します。リクエストURLをベアラー資格情報として呼び出し、audienceをクエリパラメータで指定して、返ってきたJWTをファイルに書き出します。

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

この2つの環境変数はGitHub Actionsのコントロールプレーンがジョブに注入するもので、runs-onで指定するランナーがGitHubホストか自己ホストかには依存しません。自己ホストランナーでも同じcurl呼び出しでIDトークンを取得できます。ただし自己ホストランナーはネットワーク的に外部と隔離された環境に置かれることが多く、その場合はランナーからhttps://token.actions.githubusercontent.com(IDトークン発行元)とhttps://api.anthropic.com(トークン交換先)へのoutbound通信を許可しておく必要があります。プロキシ経由のネットワークでは、curlと後続のSDK呼び出しの両方がそのプロキシ設定を継承しているかを確認してください。

JavaScriptで書きたい場合は、actions/github-scriptcore.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 subjectクレームリファレンスにまとまっています。

Anthropic側の設定

Claude ConsoleのSettings → Workload identityを開き、Connect workloadからGitHub Actionsタイルを選ぶと、発行者の登録・サービスアカウントの作成・連合ルールの作成をウィザードが順に案内します。

ウィザードが作成するリソースは、ウィザードで直接入力する場合も、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_prefixrepo:your-org/your-repo:*まで緩めるのは、同じリポジトリの複数イベント種別をまとめて1つのルールでマッチさせたい場合だけにします。subの末尾セグメントは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",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello, Claude"}],
)

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

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

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

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

どちらが優先されても最短60秒は保証されます。この2番目の上限は、Anthropicトークンが元になったGitHub側の身元より長生きしすぎないようにするための制約です。GitHubのIDトークンはおよそ5分で失効するため、実務上はほぼ常に2番目の条件がボトルネックになり、token_lifetime_secondsを長く設定しても発行されるAnthropicトークンの寿命はGitHub側のトークン残存時間に引きずられます。

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

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

GitHubのIDトークンはjtiクレームを持つため、既定で使い捨て(single-use)として扱われます。一度交換に使ったIDトークンを再提示すると、認証履歴にjti_reusedという理由で拒否記録が残ります。ワークフロー側で/tmp/gha-jwtをリトライループの中で使い回すと踏みやすい失敗パターンなので、リフレッシュのたびに新しいIDトークンを取得し直す実装にします。

設定を検証する

交換に成功すると、sk-ant-oat01-で始まるaccess_tokenexpires_inが返ります。拒否された場合は401authentication_error(メッセージはAuthentication failedという不透明な固定文言)になり、どのチェックで落ちたかは自分では読み取れません。実際の失敗理由は認証履歴ページの該当エントリに記録されるので、失敗が起きたらまずここを開きます。

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

reason典型的な原因GitHub Actions側の対処
match_subject_prefix典型的な原因subクレームの形式不一致(最多)GitHub Actions側の対処イベント種別で末尾がref:...environment:...pull_requestのどれに変わるかを確認し、subject_prefixをそのイベントに合わせる
jti_reused典型的な原因同じIDトークンを2回目の交換に再提示したGitHub Actions側の対処リトライ・キャッシュ処理を見直し、交換のたびに新しいIDトークンを取得する
(履歴に現れず、静かに古い認証が使われる)典型的な原因シェルやCIシークレットに残ったANTHROPIC_API_KEYが連合より優先されるGitHub Actions側の対処資格情報の優先順位はANTHROPIC_API_KEY/ANTHROPIC_AUTH_TOKENが連合系の環境変数より上位。移行時はワークフローの全ステップと組織のCIシークレットから旧ANTHROPIC_API_KEYを削除し、ant auth statusでどの資格情報が勝っているかを確認する

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

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やフィーチャーブランチを除外この設定で防げる攻撃シナリオフォークから開かれたpull requestのワークフロー実行が、レビュー前の任意コードのまま連合済みトークンを取得する(最も実害の大きい経路)
ownerを明示的に固定やり方claims"repository_owner": "your-org"を追加し、subのパース漏れに対する多層防御にするこの設定で防げる攻撃シナリオsubject_prefix側の実装ミスや将来のGitHub側フォーマット変更で、意図しないowner配下のリポジトリまでsubが一致してしまう
デプロイ環境に固定やり方デプロイジョブではsubject_prefix: "repo:your-org/your-repo:environment:production"にマッチさせ、GitHub側でその環境にrequired reviewersを設定してゲートするこの設定で防げる攻撃シナリオレビュー承認を経ていないブランチ・ワークフローが、本番相当のスコープを持つトークンを直接取得する

GCPやKubernetesとの違い

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

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

まとめ

GitHub ActionsからClaude APIを呼ぶワークフローは、id-token: write権限を付けてGitHubのOIDCトークンを取得し、Anthropic側の連合ルールでリポジトリ・ブランチ・環境を絞り込むだけでAPIキーレス化できます。subクレームの末尾がイベント種別ごとに変わる点だけ意識してルールを組めば、シークレット管理の負担なくCI/CDパイプラインにClaude API呼び出しを組み込めます。まずは1つの保護ブランチ向けにルールを1本作り、認証履歴ページで想定どおりのクレームが記録されることを確認してから、他のワークフローへ広げるのが安全な進め方です。

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