WIFをKubernetesと連携する — Podの署名トークンでAPIキー不要に
自己管理Kubernetesクラスターのprojected service account tokenでClaude APIを呼ぶ、Workload Identity Federationの設定手順を追います。
KubernetesのPodからAPIキーなしでClaude APIを呼ぶ仕組み
自己管理のKubernetesクラスター(kubeadm・k3s・OpenShift・オンプレミス各種)は、projected service account tokenの仕組みで、すべてのPodにOIDCのJSON Web Token(JWT)を発行できます。クラスターのAPIサーバー自身がOIDC発行者になり、各トークンのsubクレームはsystem:serviceaccount:<namespace>:<service-account>という形式です。
Workload Identity Federation(WIF)は、このトークンをPOST /v1/oauth/tokenで短命のAnthropicアクセストークンに交換する仕組みです。長期間有効なAPIキーをPodのSecretに置く必要がなくなります。
クラスターの発行者URLは、discoveryドキュメントを読めば分かります。
kubectl get --raw /.well-known/openid-configuration | jq -r .issuerprojected service account tokenとAPIサーバーによる発行は、Kubernetes自体の機能です。あらゆるディストリビューションの土台になっています。
マネージドKubernetesサービスなら、クラウドごとのガイドにプロバイダー管理の発行者URLの探し方があります。対象はAWS EKS、Google Cloud GKE、Azure AKSです。
SPIREを使っているクラスターでは、APIサーバーではなくSPIRE OIDC Discovery Providerが発行者になります。それ以外のディストリビューションやガイドに載っていないマネージドプロバイダーは、この記事の手順をそのまま使い、クラスターが報告する発行者URLを当てはめます。
WIFの基本用語(サービスアカウント・連合発行者・連合ルール)はWIFリファレンスにまとめています。
前提条件
- クラスターのAPIサーバーに
--service-account-issuerフラグが設定されていること。ほとんどのディストリビューションではデフォルトで設定済みで、kubeadmクラスターは通常 https://kubernetes.default.svc.cluster.local を使う - 以下のいずれかで、Anthropicがトークンの署名を検証できること
- 発行者のJWKSエンドポイントが443番ポートでインターネットから到達可能(Anthropicが取りに行くURL、つまり
discoveryの発行者URLとJWKS URL、explicit_urlのJWKS URLは、https・443番ポート・公開DNSのホスト名に限られ、IPリテラルは使えない) - クラスター内部からJWKSを取得し、
inlineモードで登録できる(手順は後述の「Anthropic側の設定」)
- 発行者のJWKSエンドポイントが443番ポートでインターネットから到達可能(Anthropicが取りに行くURL、つまり
- Claude ConsoleでAnthropicの組織にサービスアカウント・連合発行者・連合ルールを作成できる権限
Kubernetes側の設定
Podに、連合ルールが期待するaudienceと有効期限でサービスアカウントトークンを投影します。serviceAccountTokenのprojectionは、マウントパスに新しいJWTを書き込み、expirationSecondsが経過する前にローテーションします。
apiVersion: v1
kind: Pod
metadata:
name: inference-worker
namespace: inference
spec:
serviceAccountName: inference-worker
volumes:
- name: anthropic-token
projected:
sources:
- serviceAccountToken:
audience: https://api.anthropic.com
expirationSeconds: 3600
path: token
containers:
- name: app
image: your-registry/inference-worker:latest
env:
- name: ANTHROPIC_IDENTITY_TOKEN_FILE
value: /var/run/secrets/anthropic.com/token
- name: ANTHROPIC_FEDERATION_RULE_ID
value: fdrl_...
- name: ANTHROPIC_ORGANIZATION_ID
value: 00000000-0000-0000-0000-000000000000
- name: ANTHROPIC_SERVICE_ACCOUNT_ID
value: svac_...
- name: ANTHROPIC_WORKSPACE_ID # ルールが複数のワークスペースで有効なときは必須
value: wrkspc_...
volumeMounts:
- name: anthropic-token
mountPath: /var/run/secrets/anthropic.com
readOnly: trueこのPodに発行されるトークンはsub: "system:serviceaccount:inference:inference-worker"とaud: ["https://api.anthropic.com"]を持ちます。
expirationSeconds: 3600は、連合発行者が許す最大寿命の既定値(1時間)ちょうどです。JWTのexpとiatの差が発行者の設定した上限を超えると交換は拒否されます。上限はClaude Consoleで発行者ごとに変えられるので、投影の寿命を延ばすときは発行者側の上限も見直します。
Anthropic側の設定
Claude ConsoleのSettings → Workload identityを開き、Connect workloadからKubernetesタイルを選ぶと、発行者の登録・サービスアカウントの作成・連合ルールの作成をウィザードが順に案内します。
ウィザードが作成するリソースは、ウィザードで直接入力する場合も、Admin APIへ送る場合も値は共通です。
連合発行者のJWKSをどう渡すか
発行者を登録するとき、署名鍵(JWKS)の渡し方をjwks.typeで選びます。クラスターの状況ごとの選び分けは次のとおりです。
JWKSの3つの渡し方
discovery(既定)
Anthropicが発行者URLの
/.well-known/openid-configurationを取りに行き、そこに載るjwks_uriから鍵を取得します。発行者がインターネットから443番ポートで見えるクラスター向けです。explicit_url
JWKSのURLを直接指定します。discoveryドキュメントは内部専用で、JWKSだけが公開されている場合に使います。
discoveryとexplicit_urlは、私設CAで署名したTLSの発行者向けにca_cert_pemも受け付けます。inline
鍵の配列を登録時に貼り付けます。Anthropicからの外向き通信は発生しません。kubeadmの既定のように発行者URLが外に出ていないクラスターでは、これが標準的な選択です。
inlineで登録するときは、クラスター内部から鍵を取得します。
kubectl get --raw /openid/v1/jwks返ってきたkeys配列の中身({"keys": [...]}という外側のラッパーではなく中身)を使って発行者を設定します。
{
"name": "onprem-k8s",
"issuer_url": "https://kubernetes.default.svc.cluster.local",
"jwks": {
"type": "inline",
"keys": [{ "kty": "RSA", "kid": "...", "n": "...", "e": "AQAB" }]
}
}inlineモードではissuer_urlはJWTのissクレームとの文字列比較にだけ使われ、AnthropicがこのURLにアクセスすることはありません。発行者がインターネットから到達可能な場合は、"jwks": {"type": "discovery"}を使います。
鍵のローテーションで交換が止まる条件
クラスターがサービスアカウントの署名鍵を入れ替えたとき、inlineの鍵を使っているなら発行者の更新は利用者の仕事です。ローテーションは頻度が低く、通常はクラスターのアップグレード時だけです。ただし新しいJWKSを反映するまで、トークン交換は署名エラーで失敗し続けます。
discoveryとexplicit_urlでは、Anthropic側がJWKSをキャッシュします。数字で並べると次のとおりです。
鍵と時計にまつわる数値
キャッシュ更新待ち
最大1分
新しい鍵で署名した直後は署名エラーになりうる
新しい鍵の事前公開
15分以上前
署名に使い始める前にJWKSへ載せる
時計のずれの許容
30秒
exp・nbf・iatに適用される
assertionの上限
16 KiB
交換に渡すJWTの大きさ
自己管理クラスターは、この事前公開の運用を自分で守る側にいます。新しい鍵を先にJWKSへ載せ、古い鍵は、それで署名したトークンの期限が切れるまで残します。
連合ルール
サービスアカウントのsubクレームと、投影したトークンに設定したaudienceをマッチ条件にします。
{
"name": "onprem-inference",
"issuer_id": "fdis_...",
"match": {
"subject_prefix": "system:serviceaccount:inference:inference-worker",
"audience": "https://api.anthropic.com"
},
"target": {
"type": "service_account",
"service_account_id": "svac_..."
},
"workspace_id": "wrkspc_...",
"oauth_scope": "workspace:developer",
"token_lifetime_seconds": 600
}subject_prefixをsystem:serviceaccount:inference:*まで緩めるのは、そのnamespace内のすべてのサービスアカウントを同じAnthropicサービスアカウントにマッピングしたい場合だけにします。作成したルールのfdrl_...IDは、PodのANTHROPIC_FEDERATION_RULE_ID環境変数に設定します。
oauth_scopeは、交換で得たトークンが呼べるエンドポイントの上限です。workspace:developerはFilesやSkillsなど非管理系のAPI全般に届きます。Messagesを呼ぶだけのPodならworkspace:inferenceで足り、範囲外のエンドポイントはHTTP 403になります。
トークンを取得してClaude APIを呼ぶ
「Kubernetes側の設定」のPod定義は、ANTHROPIC_IDENTITY_TOKEN_FILEを投影されたマウントパスに、ANTHROPIC_FEDERATION_RULE_ID・ANTHROPIC_ORGANIZATION_ID・ANTHROPIC_SERVICE_ACCOUNT_ID・ANTHROPIC_WORKSPACE_IDとあわせて設定済みです。SDKは交換のたびにディスクからトークンを読み、Anthropicアクセストークンを自動で更新します。
import anthropic
# Podの環境からANTHROPIC_IDENTITY_TOKEN_FILE、ANTHROPIC_FEDERATION_RULE_ID、
# ANTHROPIC_ORGANIZATION_ID、ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_IDを読む
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-opus-5-5",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
)curlで直接交換する場合は、マウントされたトークンファイルをそのままassertionに使います。
JWT=$(cat "$ANTHROPIC_IDENTITY_TOKEN_FILE")
ACCESS_TOKEN=$(curl -sS https://api.anthropic.com/v1/oauth/token \
-H "content-type: application/json" \
--data @- <<JSON | jq -r .access_token
{
"grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
"assertion": "$JWT",
"federation_rule_id": "$ANTHROPIC_FEDERATION_RULE_ID",
"organization_id": "$ANTHROPIC_ORGANIZATION_ID",
"service_account_id": "$ANTHROPIC_SERVICE_ACCOUNT_ID",
"workspace_id": "$ANTHROPIC_WORKSPACE_ID"
}
JSON
)
curl -sS https://api.anthropic.com/v1/messages \
-H "authorization: Bearer $ACCESS_TOKEN" \
-H "anthropic-version: 2023-06-01" \
-H "content-type: application/json" \
-d '{"model":"claude-opus-5-5","max_tokens":1024,"messages":[{"role":"user","content":"Hello, Claude"}]}'交換で得るトークンの寿命は、ルールのtoken_lifetime_secondsと、提示したJWTの残り寿命の2倍のうち短いほうです。ただし60秒を下回ることはありません。上の例のルール(600秒)に、残り200秒のJWTを出すと、Anthropicのトークンは400秒です。残りが20秒なら下限の60秒になります。
token_lifetime_secondsに指定できる範囲は60〜86400秒で、既定は3600秒です。
環境変数の代わりに、プロファイルファイルでも同じ設定を渡せます。<設定ディレクトリ>/configs/<プロファイル名>.jsonは秘密情報を含まないため、コンテナイメージに焼き込んだりConfigMapから配ったりできます。
{
"version": "1.0",
"authentication": {
"type": "oidc_federation",
"federation_rule_id": "fdrl_...",
"service_account_id": "svac_...",
"identity_token": {
"source": "file",
"path": "/var/run/secrets/anthropic.com/token"
}
},
"organization_id": "00000000-0000-0000-0000-000000000000",
"workspace_id": "wrkspc_...",
"base_url": "https://api.anthropic.com"
}設定ディレクトリは$ANTHROPIC_CONFIG_DIR、なければLinuxとmacOSでは~/.config/anthropicです。使うプロファイルはANTHROPIC_PROFILEで選びます。
設定を検証する
交換に成功すると、sk-ant-oat01-で始まるaccess_tokenとexpires_inが返ります。
401のauthentication_error(メッセージはAuthentication failedという不透明な固定文言)で失敗する場合は、認証履歴ページで拒否理由を見ます。拒否の種類を呼び出し側が嗅ぎ分けて、ルールの設定を探れないようにするため、どの検査で落ちても応答は同じです。理由は履歴にだけ残り、たとえばsubが合わなければmatch_subject_prefixと出ます。
Kubernetes側で最も多い原因は、JWKSの鍵の不一致です。inlineモードなら、kubectl get --raw /openid/v1/jwksで鍵を取り直し、発行者の登録を更新します。
交換が失敗するときの調べ方
認証履歴ページに履歴が出ないまま401が返るときは、federation_rule_idそのものが認識されていない可能性があります。履歴が出るなら、その理由を手がかりに、次の順でトークンの中身とルールを突き合わせます。
トークンとルールを突き合わせる順序
- 1
トークンをデコードする
Podの中でマウントされたトークンを読み、ペイロードを表示します。JWTのペイロードはBase64URLなので、
-と_を戻してからデコードします。kubectl exec <pod> -- cat /var/run/secrets/anthropic.com/token \ | jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson' - 2
issが発行者URLと一致するか
issは、登録したissuer_urlと1文字単位で一致する必要があります。スキームやポート、末尾のスラッシュの違いも不一致です。 - 3
audがルールのaudienceを含むか
audが配列のときは、要素のどれかがルールのaudienceと完全に一致すれば通ります。PodのserviceAccountToken投影に書いた値と食い違っていれば、ここで落ちます。 - 4
subがsubject_prefixに合うか
subject_prefixは大文字小文字を区別します。末尾に*がなければ完全一致で、あれば前方一致です。namespaceやサービスアカウント名のタイプミスは、ここで見つかります。 - 5
exp・iatとノードの時計を見る
expは未来、iatは過去でなければならず、30秒の許容が付きます。ノードの時計が大きくずれていると、有効なトークンでも拒否されます。 - 6
JWKSが現在の署名鍵と合うか
inlineなら、kubectl get --raw /openid/v1/jwksの出力と、登録済みの鍵を突き合わせます。クラスターの鍵ローテーションの後だけ失敗するなら、ここが原因です。トークン自体はexpirationSecondsで自動更新されても、inlineの登録は追従しないためです。
同じJWTの再送にも注意が必要です。jtiクレームを持つassertionは、発行者ごとに1回しか交換できません。リトライのループや、ローテーション前のトークンを再読込する更新処理が同じJWTを2度送ると、2度目は拒否されます。認証履歴にはjti_reusedと出るので、交換のたびに新しいassertionを作ります。
複数のワークスペースで有効なルールを使うPodがANTHROPIC_WORKSPACE_IDを設定し忘れると、履歴にはworkspace_id_requiredという理由が残ります。
APIキーからの移行でSDK側につまずくとき
ルールが正しくても、SDKが連合に進まなかったり、呼び出しが途中で止まったりすることがあります。Pod内で起きやすい原因は次の4つです。
SDK側でつまずく4つの原因
APIキーが先に勝つ
ANTHROPIC_API_KEYやANTHROPIC_AUTH_TOKENが環境にあると、連合の環境変数より優先されます。SecretからAPIキーを注入していたPodを移行するときは、その変数を外します。空文字でも勝つ
ANTHROPIC_API_KEY=""のように空で定義しただけでも、その変数は優先順位の枠を占めます。空のAPIキーで認証しようとするので、Pod定義から項目ごと消します。トークンファイルがない
初回のリクエストで
FileNotFoundErrorが出るときは、ANTHROPIC_IDENTITY_TOKEN_FILEのパスが存在しません。SDKはファイルを交換の直前に開くので、projectedボリュームのマウント漏れや、パスの打ち間違いが原因です。403で止まる
交換は成功するのに、FilesやSkillsなどの呼び出しだけが403になるなら、ルールが
workspace:inferenceのままになっていないかを見ます。
直接の環境変数で連合が働くのは、ANTHROPIC_FEDERATION_RULE_ID・ANTHROPIC_ORGANIZATION_ID・ANTHROPIC_SERVICE_ACCOUNT_IDと、ANTHROPIC_IDENTITY_TOKEN_FILE(またはANTHROPIC_IDENTITY_TOKEN)がすべてそろったときです。ANTHROPIC_WORKSPACE_IDは一緒に読まれるだけで、有効化の条件には入りません。
ルールを絞り込むときの注意点
system:serviceaccount:*だけのsubject_prefixは、クラスター内のすべてのサービスアカウントにマッチしてしまい、どのPodでも連合済みのAnthropicトークンを取得できてしまいます。audienceのマッチ条件がなければ、すべてのPodがすでに持っているデフォルトaudienceのトークンにもルールがマッチします。
マッチ条件は、書いたものがすべてAND条件になります。ルールにはsubject_prefix・claims・conditionのうち少なくとも1つが必要で、audienceだけのルールは作成時に拒否されます。audienceは、subject_prefixを絞った上で重ねる条件です。
ルールのmatchブロックは、用途に合う最も狭いスコープに絞ります。
| 絞り込み | やり方 |
|---|---|
| namespaceとサービスアカウント名を固定 | やり方末尾に*を付けず、完全なsystem:serviceaccount:<namespace>:<name>を使う |
| audienceを必ず設定する | やり方ルールにaudienceを必須にし、PodのserviceAccountToken投影にも同じ値を設定してデフォルトaudienceのトークンを拒否する |
| namespaceごとに別ルールを用意する | やり方1つのルールを広げるのではなく、namespaceごとに専用のルールとAnthropicサービスアカウントを作る |
| inline JWKS発行者をクラスター単位に絞る | やり方複数クラスターが同じ発行者URLを共有していても、クラスターごとにJWKSを別の連合発行者として登録し、ルールをその発行者だけに紐づける |
GCPやGitHub Actionsとの違い
同じWIFでも、Kubernetesの素の発行者と、Google Cloud・GitHub Actionsではトークンの発行元とクレームの形式が異なります。GKEでWorkload Identityを使う場合の手順はWIFをGoogle Cloudと連携する、CI/CDパイプラインからGitHub Actions経由で呼ぶ手順はWIFをGitHub Actionsと連携するにまとめています。
いずれの構成でも、トークン交換のリクエスト・レスポンス仕様と連合ルールの粒度設計は共通です。CEL条件式の書き方やエラーコードの逆引きはWIFリファレンスを参照してください。
まとめ
発行者URLが外に出ていないクラスターなら、inlineのJWKSで始め、鍵のローテーションを自分の運用に組み込めるかを先に決めます。ルールの条件はnamespaceとサービスアカウント名の完全一致から始めます。