WIFリファレンス — トークン交換・CEL評価・エラー逆引き
AnthropicのWorkload Identity Federationのトークン交換API・環境変数・CEL条件・エラーコードを一枚にまとめます。GCPのWIFとは別物です。
WIFとは何か — GCPの同名機能との違い
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の交換が成立します。
ルールは交換リクエストでIDを指定して使います。Anthropic側がJWTに合うルールを探し出すことはありません。1つの発行者に、チーム・名前空間・権限レベルごとの複数ルールをぶら下げる構成が基本です。
サービスアカウントは組織単位の存在で、ワークスペースのメンバーになって初めてそのワークスペースで動けます。交換時には、ルールのワークスペースがサービスアカウントの所属先に含まれているかも検査されます。既定ワークスペースには全サービスアカウントが暗黙のメンバーとして入っています。
設定にはConsoleのSettings → Workload identityにある「Connect workload」ウィザードが使えます。発行者・サービスアカウント・ルールを一度に作り、15分間だけ実際の交換成功を待ち受けて接続を確かめます。ウィザードが入れるtoken_lifetime_secondsの初期値は600で、APIで省略したときの既定値3,600とは異なります。操作できるのは組織のadmin・owner・primary ownerのロールです。
トークン交換のリクエストとレスポンス仕様
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はルールが複数ワークスペースで有効なときだけ必須になる条件付きフィールドで、省略するとサーバーはルールが有効な唯一のワークスペースを選びます。
workspace_idにはdefaultという固定文字列も通りますが、非推奨です。既定ワークスペースを指すときもwrkspc_...の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を直接叩く場面はデバッグ時に限られます。環境変数での連合は、ANTHROPIC_FEDERATION_RULE_ID・ANTHROPIC_ORGANIZATION_ID・ANTHROPIC_SERVICE_ACCOUNT_IDと、ANTHROPIC_IDENTITY_TOKEN_FILEかANTHROPIC_IDENTITY_TOKENのどちらかがすべて揃ったときだけ有効になります。ANTHROPIC_WORKSPACE_IDは一緒に読まれますが、有効化の条件には入りません。
認証情報の優先順位 — APIキーとプロファイルの力関係
SDKは5段階の優先順位で認証情報を解決し、最初に見つかった情報源で確定します。上位が下位を黙って覆い隠す設計なので、移行時にどれが勝つかを把握していないと、意図しない認証で動き続けます。
| 優先順位 | 情報源 | 挙動 |
|---|---|---|
| 1 | 情報源コンストラクタ引数(api_key=等) | 挙動常に他のすべてを上書き |
| 2 | 情報源ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN | 挙動連合を完全に無効化する |
| 3 | 情報源ANTHROPIC_PROFILE | 挙動指定名のプロファイルが無ければエラー(フォールバックしない) |
| 4 | 情報源連合用の環境変数 | 挙動上の4変数(トークンはファイルか文字列のどちらか)が揃うと有効化 |
| 5 | 情報源アクティブなプロファイル | 挙動<config_dir>/active_config、無ければdefault |
空文字列でもスロットを占有する点に注意が必要です。ANTHROPIC_API_KEY=""をexportした状態では、SDKは空キーのままAPIキー経路を選び、連合へフォールバックしません。APIキーから移行する際は空文字にせずunsetする必要があります。
APIキーで動いているワークロードを止めずに連合へ切り替める手順は、次の4段階です。
APIキーから連合へ切り替える
- 1
連合を並行して設定する
ルールがワークロードのトークンに合うことを確かめます。この時点では既存の
ANTHROPIC_API_KEYを残しておきます。 - 2
どの認証情報が勝つかを見る
ワークロードの中で
ant auth statusを実行します。表の2段目にAPIキーがいるので、この段階ではAPIキーが選ばれているはずです。 - 3
APIキーを注入元ごと外す
CIのシークレット・コンテナの環境変数・シェルのプロファイルから
ANTHROPIC_API_KEYを消します。再度ant auth statusを実行し、連合が選ばれたことを確かめます。 - 4
Consoleでキーを削除する
連合トークンで動くことを確かめてから、Settings → API keysでキーを削除します。
プロファイルは<config_dir>/configs/<name>.json(非秘匿、イメージに焼き込んでよい)と<config_dir>/credentials/<name>.json(秘匿、SDKがmode 0600で書き込む)の2ファイルに分かれます。どちらも先頭のversionは"1.0"形式の文字列で、手書きするときは省略できます。
プロファイルが読み込まれた場合、環境変数はプロファイルに書かれていない項目だけを埋めます。たとえばANTHROPIC_WORKSPACE_IDは、プロファイルにworkspace_idが無いときにだけ効きます。プロファイル側に値があるのに環境変数で上書きしようとしても、無視されます。
設定ディレクトリは$ANTHROPIC_CONFIG_DIR→macOS/Linuxの~/.config/anthropic→Windowsの%APPDATA%\Anthropicの順で解決されます。アクティブなプロファイル名は$ANTHROPIC_PROFILE→active_configの中身(ant profile activate <name>が書く1行のファイル)→defaultの順です。プロファイルはant CLIもSDKと同じように読み、Claude CodeとAgent SDKも同じ解決順序に従います。ここで組んだプロファイルは、そのまま両方の認証に使えます。
JWTを受け入れる条件 — 静的マッチとCEL式
連合ルールのmatchブロックは、subject_prefix(末尾*で前方一致、大文字小文字を区別)・audience(完全一致)・claims(トップレベルの完全一致マップ)・condition(CEL式)の4種類をAND条件で評価します。subject_prefix・claims・conditionのいずれかは必須です。audienceだけのルールは「発行者からのあらゆるトークンを受け入れてしまう」ため拒否されます。
audが配列のときは、どれか1要素がルールのaudienceと完全一致すれば通ります。数値・真偽値・ネスト・配列を含むクレームはclaimsマップでは表現できないため、CELの式に切り替えます。評価環境で使える変数はclaims(デコード済みJWTクレーム全体)の1つだけで、ネストしたオブジェクトはネストしたマップとして辿れます。
claims.sub.startsWith("repo:acme-corp/") && claims.ref in ["refs/heads/main", "refs/heads/release"]CEL条件は認証の境界線そのものです。意図より広い入力にtrueを返す式を書くと、そのぶんアクセス範囲が広がります。可能な限り静的マッチャーを優先し、CELは複雑な論理が必要なときだけに絞ります。
ルールに届く前に、JWT自体が次の検証を通る必要があります。
| 検証項目 | 条件 |
|---|---|
| サイズ | 条件assertionは16 KiB以下 |
| 署名アルゴリズム | 条件RS256/384/512・ES256/384/512・PS256/384/512のみ。HS256等のHMACとnoneは拒否 |
kid | 条件ヘッダにkidがあり、発行者のJWKS内の鍵と一致すること |
| 必須クレーム | 条件subが存在し、iatが未来でなく、expが未来であること |
| 再利用 | 条件jtiを持つトークンは発行者単位で1回しか交換できない。check_jtiは既定で有効 |
| 寿命の上限 | 条件expとiatの差が、発行者に設定した最大値(既定1時間)以下 |
| 時計のずれ | 条件exp・nbf・iatに30秒の猶予 |
jtiを持たないトークンは再利用の検査を受けません。check_jtiを無効にすると、その発行者に紐づく全ルールでリプレイ防止が外れます。公式はトークンの供給元がそれほど頻繁に更新できない場合の最後の手段として位置づけています。
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全体への到達を許可しますが、このトークンで作成・変更できるのはworkspace:developerかworkspace:inferenceのルールだけです。他のスコープを持つルールの発行者更新もできません。スコープ外のエンドポイントへのリクエストは常にHTTP 403で、リソース別や読み書き別の細かいスコープはありません。
ここで見落としやすいのが、スコープはあくまで上限であって実効権限ではない点です。実効権限はルールのoauth_scopeと、対象サービスアカウントのorganization_role(developerかadmin)の積集合で決まります。org:adminスコープのルールはorganization_role=adminのサービスアカウントにしか向けられず、developerのままでは組めません。
逆方向にも効きます。adminロールのサービスアカウントにworkspace:developerのルールを向けても、トークンが使えるのはそのワークスペースのClaude APIだけで、Admin APIには届きません。スコープを緩めるときも絞るときも、サービスアカウント側のロールと必ず対で見直します。
JWKSの取得方式とトークンの有効期限
発行者を登録する際のjwks設定は3方式から選びます。
JWKSの3方式
discovery(既定)
issuer_url(または別指定のdiscovery_base)の/.well-known/openid-configurationを取り、そこに書かれたjwks_uriをさらに取得します。EKS・GKE・Cloud Run・GitHub Actions・Entra IDなど主要なマネージドIdPはこれで足ります。
explicit_url / inline
explicit_urlはディスカバリー文書を持たないIdP向けに、JWKSのURLを直接指定します。inlineはエアギャップ環境や、クラスタ内部限定の発行者URLを使う自己管理Kubernetesクラスタ向けに、JWKの配列(keysの中身)をそのまま登録します。
Anthropicが取りに行くURLには条件があります。対象はissuer_url・jwks.discovery_base・jwks.urlで、スキームはhttps、ポートは443、ホストは公開DNSで引ける名前に限られ、IPアドレスの直書きは通りません。違反すると400 invalid_request_errorが返り、エラーメッセージの先頭にフィールド名が付きます。
この制約が効くのはAnthropicが実際にアクセスするURLだけです。explicit_urlとinlineでは、issuer_urlはJWTのissとの文字列比較にしか使われないため、社内ホスト名や標準外のポートでも登録できます。discoveryでもdiscovery_baseを指定していれば同じです。私設CAで運用するTLSの発行者には、discoveryとexplicit_urlのどちらでもca_cert_pemを足せます。
discoveryとexplicit_urlではJWKSがキャッシュされるため、IdP側が署名鍵をローテーションした直後は最大1分ほど検証が失敗する可能性があります。新しい鍵は使用開始の15分以上前にJWKSへ公開し、古い鍵もそれで署名済みのトークンが失効するまでJWKSに残しておくのが安全です。inlineモードには自動更新の仕組みが無いため、鍵をローテーションしたら発行者の設定を手動で更新しないと、すべてのトークン交換が署名検証で失敗します。
トークン寿命まわりの数字
設定できる範囲
60〜86,400
範囲外は作成・更新時に拒否
予備更新
失効の120秒前
失敗しても使い続けられる
必須更新
失効の30秒前
失敗するとエラー
発行されるアクセストークンの有効期間は、ルールのtoken_lifetime_secondsと、提示したIdP JWTの残り有効期間の2倍のうち短いほうです。ただし60秒を下回ることはありません。SDKはbotocoreを参考にした二段階のスケジュールで更新します。予備更新でトークンエンドポイントに届かなくても、キャッシュ済みトークンにはまだ約90秒の有効期間が残っています。
ANTHROPIC_IDENTITY_TOKEN_FILEはSDKが交換のたびに読み直します。更新の前にファイルが新しいトークンに入れ替わっていないと、同じjtiを再送してjti_reusedで弾かれます。再起動したプロセスが交換済みのトークンをもう一度出す場合も同じです。
エラーを逆引きする — 症状から原因を特定する
トークン交換の失敗は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を使う |
401の中身は、Claude Consoleの認証履歴ページに残ります。履歴には評価した発行者とルール、検査したJWTクレーム、落ちた検証の段階が出ます。たとえばsubがsubject_prefixに合わなければmatch_subject_prefix、複数ワークスペースのルールでworkspace_idを省けばworkspace_id_required、同じJWTの再送ならjti_reusedです。
履歴に該当エントリが無い401は、federation_rule_idそのものが認識されていないことが多いようです。400番台のようにルールの所属組織を確かめる前に弾かれたリクエストは、履歴にも残りません。
履歴を見てもわからないときは、JWTを手元で調べます。次のjqでペイロードをデコードできます。
jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson' <<< "$JWT"手元で組み立てたダミーのJWT(署名部はsig)に通すと、次のようなJSONが出ます。issにはわざと末尾の/を付けてあります。
{
"iss": "https://token.actions.githubusercontent.com/",
"sub": "repo:Acme-Corp/api:ref:refs/heads/main",
"aud": "https://api.anthropic.com",
"iat": 1790000000,
"exp": 1790000600
}登録したissuer_urlが末尾に/のないhttps://token.actions.githubusercontent.comなら、このissとは一致せず401になります。比較はスキーム・ポート・末尾のスラッシュまで1バイトずつ行われます。ここに出たクレームを、次の順にルール側の設定と突き合わせます。
デコードしたJWTを突き合わせる順序
- 1
iss を発行者と比べる
登録した
issuer_urlと1文字違いでも落ちます。 - 2
aud をルールの audience と比べる
配列の中身を見ます。
- 3
sub と claims を比べる
ダミーの例なら
Acme-Corpの大文字小文字に注目します。 - 4
exp・nbf・iat を時計と比べる
30秒の猶予の中に収まっていなければ、ワークロードのホストの時計ずれが疑われます。
- 5
JWKSに届くか確かめる
discoveryなら<discovery_base または issuer_url>/.well-known/openid-configurationを公開HTTPSの443番で取得し、jwks_uriが引けるかを見ます。explicit_urlはJWKSのURLを直接取り、inlineは発行者の署名鍵がその後ローテーションされていないかを確かめます。
なぜ認証失敗のレスポンスは常に同じ文言なのか
401の理由を伏せる設計は不親切に見えますが、これは意図的なセキュリティ判断です。もしsubが一致しない場合とaudが一致しない場合とでエラーメッセージが変わるなら、呼び出し側はレスポンスの違いを手がかりにルールの設定を少しずつ推測できてしまいます。認証履歴ページという組織内の管理画面に理由を逃がし、レスポンス自体は無差別に同じ文言を返す構成です。
同じ発想は、federation_rule_idが存在しない場合・アーカイブ済みの場合・存在はするがJWTが認可されない場合を、意図的に区別しない扱いにも表れています。ルールIDの存在を外から探れないようにする、列挙攻撃への配慮です。
まとめ — WIFに向く運用、まだ早い運用
CI/CDのジョブやKubernetesのPodのように、実行のたびに使い捨てのアイデンティティを持てるワークロードにはWIFが向きます。サービスアカウントを権限の単位に切れば、どのワークロードがどのアイデンティティで動いたかを後から追えます。Managed AgentsのようにAPIを継続的に呼ぶ構成も、workspace:developerのルールでAPIキーなしに動かせます。組織単位でAPIキー認証を止める設定は別の記事で扱っています。
一方、IdPを持たない個人開発や、CI/CDを組んでいない小規模な検証環境では、APIキー1本で足りる場面のほうが多いはずです。連合発行者とルールの設計・JWKSの到達性・CEL条件のテストといった初期コストは、ワークロードの数が増えて初めて回収できます。