WIFリファレンス — トークン交換・CEL評価・エラー逆引き
AnthropicのWorkload Identity Federationのトークン交換API・環境変数・CEL条件・エラーコードを一枚にまとめます。GCPのWIFとは別物です。
WIFとは何か — GCPやAWSの同名機能との違い
Workload Identity Federation(WIF)は、ワークロードが長期のsk-ant-...APIキーを持たずにClaude APIへ認証する仕組みです。IdP(AWS IAM・Google Cloud・GitHub Actions・Kubernetes・SPIFFE・Microsoft Entra ID・Oktaなど)が発行した短命のOIDC JWTを、Anthropicが自前で運用するPOST /v1/oauth/tokenエンドポイントへ渡し、組織内のサービスアカウントに紐づく短命のAnthropicアクセストークンと交換します。
設定するリソースは3種類です。サービスアカウント(svac_...、組織内の非人間アイデンティティ)、連合発行者(fdis_...、IdPの登録)、連合ルール(fdrl_...、発行者とサービスアカウントを結ぶマッチ条件とスコープ)。この3つが揃って初めてJWTの交換が成立します。
トークン交換のリクエストとレスポンス仕様
POST /v1/oauth/tokenはRFC 7523のjwt-bearerグラントを使うJSONボディを受け付けます。必須フィールドはgrant_type(固定値urn:ietf:params:oauth:grant-type:jwt-bearer)・assertion(IdPが署名したJWT)・federation_rule_id・organization_id・service_account_idの5つ。workspace_idはルールが複数ワークスペースで有効なときだけ必須になる条件付きフィールドです。
レスポンスはRFC 6749 §5.1準拠のOAuth 2.0トークンレスポンスで、access_token(sk-ant-oat01-...から始まる短命トークン)・token_type(常にBearer)・expires_in・scopeの4フィールドを返します。
JWT=$(cat /var/run/secrets/anthropic.com/token)
curl -sS https://api.anthropic.com/v1/oauth/token \
-H "content-type: application/json" \
-d @- <<JSON
{
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": "$JWT",
"federation_rule_id": "fdrl_...",
"organization_id": "00000000-0000-0000-0000-000000000000",
"service_account_id": "svac_...",
"workspace_id": "wrkspc_..."
}
JSONSDKはこのやり取りを環境変数から自動で組み立てるため、実際にcURLを直接叩く場面はデバッグ時に限られます。
認証情報の優先順位 — APIキーとプロファイルの力関係
SDKは5段階の優先順位で認証情報を解決し、最初に見つかった情報源で確定します。上位が下位を黙って覆い隠す設計なので、移行時にどれが勝つかを把握していないとハマります。
| 優先順位 | 情報源 | 挙動 |
|---|---|---|
| 1 | 情報源コンストラクタ引数(api_key=等) | 挙動常に他のすべてを上書き |
| 2 | 情報源ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN | 挙動連合を完全に無効化する |
| 3 | 情報源ANTHROPIC_PROFILE | 挙動指定名のプロファイルが無ければエラー(フォールバックしない) |
| 4 | 情報源連合用の環境変数 | 挙動ANTHROPIC_FEDERATION_RULE_ID等4変数がすべて揃うと有効化 |
| 5 | 情報源アクティブなプロファイル | 挙動<config_dir>/active_config、無ければdefault |
空文字列でもスロットを占有する点に注意が必要です。ANTHROPIC_API_KEY=""をexportした状態では、SDKは空キーのままAPIキー経路を選び、連合へフォールバックしません。APIキーから移行する際は空文字にせずunsetする必要があります。
プロファイルは<config_dir>/configs/<name>.json(非秘匿、イメージに焼き込んでよい)と<config_dir>/credentials/<name>.json(秘匿、SDKがmode 0600で書き込む)の2ファイルに分かれます。設定ディレクトリは$ANTHROPIC_CONFIG_DIR→macOS/Linuxの~/.config/anthropic→Windowsの%APPDATA%\Anthropicの順で解決され、Claude CodeとAgent SDKも同じ解決順序に従うため、ここで組んだプロファイルはそのまま両方の認証に使えます。
JWTを受け入れる条件 — 静的マッチとCEL式
連合ルールのmatchブロックは、subject_prefix(末尾*で前方一致)・audience(完全一致)・claims(トップレベルの完全一致マップ)・condition(CEL式)の4種類をAND条件で評価します。subject_prefix・claims・conditionのいずれかは必須です。audienceだけのルールは「発行者からのあらゆるトークンを受け入れてしまう」ため拒否されます。
ネストしたクレームや配列を扱うときはclaimsマップでは表現できないため、CELの式に切り替えます。評価環境で使える変数は(デコード済みJWTクレーム全体)の1つだけです。
claims.sub.startsWith("repo:acme-corp/") && claims.ref in ["refs/heads/main", "refs/heads/release"]CEL条件は認証の境界線そのものです。意図より広い入力にtrueを返す式を書くと、そのぶんアクセス範囲が広がります。可能な限り静的マッチャーで表現し、CELは複雑な論理が必要なときだけに絞るのが安全側の設計です。
JWT検証そのものにも制約があります。署名アルゴリズムはRSA/ECDSA系(RS256/384/512・ES256/384/512・PS256/384/512)のみ受理し、HMAC(HS256等)とnoneは拒否。kidヘッダが無いトークンも拒否されます。jtiクレームを持つトークンは発行者単位で1回しか交換できず、リトライループで同じJWTを再送するとjti_reusedとして弾かれます。
OAuthスコープの実効権限 — ルールとロールの掛け算
連合ルールのoauth_scopeは、発行されるトークンが呼べるAPI範囲を決めます。workspace:developerはそのワークスペースのMessages・Models・Managed Agents・Files・Skillsなど非管理系エンドポイント全部で、ワークスペースAPIキーと同等の範囲です。workspace:inferenceはMessagesとModels、OpenAI互換チャットエンドポイントだけに絞った推論専用スコープ。workspace:manage_tunnelsはMCP tunnels APIの操作(トンネル作成・CA証明書登録・トークンのローテーション)専用で、Consoleのトンネル作成モーダルから生成したルールはこのスコープに固定されます。org:adminはAdmin API全体への到達を許可しますが、他のスコープを持つルールの発行者更新はできません。スコープ外のエンドポイントへのリクエストは常にHTTP 403です。
ここで見落としやすいのが、スコープはあくまで上限であって実効権限ではない点です。実効権限はルールのoauth_scopeと、対象サービスアカウントのorganization_role(developerかadmin)の積集合で決まります。org:adminスコープのルールを組んでも、ターゲットのサービスアカウントがdeveloperロールのままなら、発行されるトークンはAdmin APIを呼べません。スコープを緩めるだけでなく、サービスアカウント側のロールも同時に見直す必要があります。
JWKSの取得方式とトークンの有効期限
発行者を登録する際のjwks設定は3方式から選びます。discovery(既定)はissuer_url(または別途指定したdiscovery_base)から/.well-known/openid-configurationを取得し、そこに書かれたjwks_uriをさらに取得する標準的な経路です。EKS・GKE・Cloud Run・GitHub Actions・Entra IDなど主要なマネージドIdPはこれで足ります。explicit_urlはディスカバリー文書を持たないIdP向けにJWKSのURLを直接指定するモード。inlineはエアギャップ環境や、クラスタ内部限定の発行者URLを使う自己管理Kubernetesクラスタ向けに、JWKの配列をそのまま登録します。
discoveryとexplicit_urlではJWKSがキャッシュされるため、IdP側が署名鍵をローテーションした直後は最大1分ほど検証が失敗する可能性があります。新しい鍵は使用開始の15分以上前にJWKSへ公開し、古い鍵もそれで署名済みのトークンが失効するまでJWKSに残しておくのが安全です。inlineモードには自動更新の仕組みが無いため、鍵をローテーションしたら発行者の設定を手動で更新しないと、すべてのトークン交換が署名検証で失敗します。
発行されるアクセストークンの有効期間は、ルールのtoken_lifetime_seconds(既定3,600秒)と、提示したIdP JWTの残り有効期間の2倍のうち短いほうです。SDKはbotocoreを参考にした二段階のスケジュールでトークンを更新します。有効期限の120秒前に予備更新を試み、失敗してもキャッシュ済みトークンをそのまま使い続けられます。30秒前になると必須更新に切り替わり、ここで交換が失敗するとエラーを返します。
エラーを逆引きする — 症状から原因を特定する
トークン交換の失敗は2系統に分かれます。400番台はリクエスト自体の不備(federation_rule_idの形式不正、workspace_idの形式不正など)で、レスポンスメッセージがそのまま原因を名指しします。401番台は認証の失敗で、原因に関わらず固定文言Authentication failedだけが返る仕様です。
| SDK側の症状 | 原因 | 対処 |
|---|---|---|
| 「no credentials」としてSDKが動かない | 原因連合4変数のいずれかが未設定、かつプロファイルも未設定 | 対処4変数を揃えるか、プロファイルを設定する |
| 連合ではなくAPIキーで認証されてしまう | 原因ANTHROPIC_API_KEYかANTHROPIC_AUTH_TOKENが残っている | 対処環境変数側をunsetする |
初回リクエストでFileNotFoundError | 原因ANTHROPIC_IDENTITY_TOKEN_FILEのパスが存在しない | 対処Projected Token用のボリュームがマウントされているか確認 |
| 交換は成功するのにAPIリクエストが403 | 原因ルールのoauth_scopeが呼び出したエンドポイントを許可していない | 対処ルールのスコープを見直す(workspace:manage_tunnels等) |
| 空の資格情報で認証失敗 | 原因変数がエクスポートされているが値が空文字列 | 対処VAR=""ではなくunset VARを使う |
原因を1文字も教えてくれない401は、Claude Consoleの認証履歴ページを見ればやのような具体的な却下理由が記録されています。JWTを自分でデコードして・・をルールの設定と1つずつ突き合わせる作業は、この履歴ページを確認したあとの最終手段です。
なぜ認証失敗のレスポンスは常に同じ文言なのか
401の理由を伏せる設計は不親切に見えますが、これは意図的なセキュリティ判断です。もしsubが一致しない場合とaudが一致しない場合とでエラーメッセージが変わるなら、攻撃者はレスポンスの違いを手がかりにルールの設定を少しずつ推測できてしまいます。認証履歴ページという「自分だけが見られる場所」に理由を逃がし、レスポンス自体は無差別に同じ文言を返す構成は、APIキーの総当たり攻撃対策としては手堅い設計です。
同じ発想はfederation_rule_idが存在しない場合と、存在はするがJWTが認可されない場合を意図的に区別しない扱いにも表れています。ルールIDの存在有無を外部から探索できないようにする、列挙攻撃への配慮です。
まとめ — WIFに向く運用、まだ早い運用
CI/CDのジョブやKubernetesのPodのように、実行のたびに使い捨てのアイデンティティを持てるワークロードにはWIFがよく合います。GitHub ActionsのOIDCトークンやKubernetesのプロジェクテッドトークンをそのまま使え、組織がAPIキー認証を無効化した環境でも運用を止めずに済みます。Managed Agentsのようにサービスアカウント単位で権限を切り分けたい構成とも相性がよく、監査ログでどのワークロードがどのサービスアカウントとして動いたかを追いやすくなります。
一方、IdPを持たない個人開発や、そもそもCI/CDを組んでいない小規模な検証環境では、APIキー1本で足りる場面のほうが多いはずです。連合発行者とルールの設計・JWKSの到達性確認・CEL条件のテストといった初期コストは、ワークロードの数が増えて初めて回収できます。