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_owner・refなど)を照合して、どのワークフロー実行を信頼するかを決めます。
事前に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_URLとACTIONS_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-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 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_prefixをrepo: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.pyPython 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つのうち短い方になります。
- 連合ルールの
token_lifetime_seconds(既定3,600秒。ウィザード経由の作成では600秒が初期値) - 提示した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_tokenとexpires_inが返ります。拒否された場合は401のauthentication_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本作り、認証履歴ページで想定どおりのクレームが記録されることを確認してから、他のワークフローへ広げるのが安全な進め方です。