Claude Media
WIFリファレンス — トークン交換・CEL評価・エラー逆引き

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_idorganization_idservice_account_idの5つ。workspace_idはルールが複数ワークスペースで有効なときだけ必須になる条件付きフィールドです。

レスポンスはRFC 6749 §5.1準拠のOAuth 2.0トークンレスポンスで、access_token(sk-ant-oat01-...から始まる短命トークン)・token_type(常にBearer)・expires_inscopeの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_..."
}
JSON

SDKはこのやり取りを環境変数から自動で組み立てるため、実際に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_prefixclaimsconditionのいずれかは必須です。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(developeradmin)の積集合で決まります。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の配列をそのまま登録します。

discoveryexplicit_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_KEYANTHROPIC_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条件のテストといった初期コストは、ワークロードの数が増えて初めて回収できます。

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