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 .issuerこの仕組み(projected service account tokenとAPIサーバーがOIDC発行者になる構成)はKubernetes自体にネイティブな機能で、あらゆるディストリビューションの土台になっています。マネージドKubernetesサービスを使っている場合は、クラウドプロバイダー側のガイド(AWS EKS / Google Cloud GKE / Azure AKS)でプロバイダー管理の発行者URLの探し方が示されています。SPIREを使っている場合は、クラスターのAPIサーバーではなくSPIRE OIDC Discovery Providerが発行者になります。それ以外のディストリビューションやガイドに載っていないマネージドプロバイダーでは、本記事の手順をそのままクラスターが報告する発行者URLに適用できます。
WIFの基本用語(サービスアカウント・連合発行者・連合ルール)はWIFリファレンスにまとめています。
前提条件
- クラスターのAPIサーバーに
--service-account-issuerフラグが設定されていること。ほとんどのディストリビューションではデフォルトで設定済みで、kubeadmクラスターは通常 https://kubernetes.default.svc.cluster.local を使う - 以下のいずれかで、Anthropicがトークンの署名を検証できること
- 発行者のJWKSエンドポイントが443番ポートでインターネットから到達可能
- クラスター内部からJWKSを取得し、
inlineモードで登録できる(手順は後述の「Anthropic側の設定」)
- 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"]を持ちます。
Anthropic側の設定
Claude ConsoleのSettings → Workload identityを開き、Connect workloadからKubernetesタイルを選ぶと、発行者の登録・サービスアカウントの作成・連合ルールの作成をウィザードが順に案内します。
ウィザードが作成するリソースは、ウィザードで直接入力する場合も、Admin APIへ送る場合も値は共通です。
連合発行者: 多くの自己管理クラスターはhttps://kubernetes.default.svc.cluster.localのような、インターネットから到達できない発行者URLを使っています。該当する場合はinlineのJWKSソースを選び、クラスターの鍵を貼り付けます。クラスター内部から鍵を取得するには次のコマンドを使います。
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を反映するまでトークン交換は署名エラーで失敗し続けます。
連合ルール: サービスアカウントの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環境変数に設定します。
トークンを取得して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",
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","max_tokens":1024,"messages":[{"role":"user","content":"Hello, Claude"}]}'設定を検証する
交換に成功すると、sk-ant-oat01-で始まるaccess_tokenとexpires_inが返ります。401のauthentication_error(メッセージはAuthentication failedという不透明な固定文言)で失敗する場合は、認証履歴ページで拒否理由を確認します。Kubernetes側で最も多い原因はJWKSの鍵の不一致です。inlineモードの場合はで鍵を再取得し、発行者の登録を更新します。
よくあるつまずき
トークン交換が失敗するときは、まず認証履歴ページの拒否理由を見てから、以下の早見表で原因を切り分けます。
| 症状 | 原因 | 確認コマンド |
|---|---|---|
401 authentication_error(Authentication failedの固定文言) | 原因拒否理由がメッセージに出ないため、まず認証履歴ページで内訳を見る必要がある | 確認コマンドcurl -sS https://api.anthropic.com/v1/oauth/token -H "content-type: application/json" --data @request.json |
| JWKSの鍵が一致しない | 原因inlineで登録した鍵がクラスターの現在の署名鍵と異なる(ローテーション後の更新漏れが典型) | 確認コマンドkubectl get --raw /openid/v1/jwks | jq .keys |
audience不一致 | 原因連合ルールのmatch.audienceと、PodのserviceAccountToken投影に設定したaudienceが食い違っている | 確認コマンドkubectl exec <pod> -- cat /var/run/secrets/anthropic.com/token | cut -d. -f2 | base64 -d | jq .aud |
subject_prefix不一致 | 原因ルールのsubject_prefixが、実際のnamespace・サービスアカウント名と完全一致していない(タイプミスや*の付け忘れ) | 確認コマンド上記と同じデコードコマンドで.subを確認する |
issuer_urlとissの不一致 | 原因連合発行者に登録したissuer_urlと、トークンのissクレームの値(末尾スラッシュの有無や大文字小文字の違いを含む)が一致しない | 確認コマンド上記と同じデコードコマンドで.issを確認する |
| ローテーション直後にだけ失敗する | 原因expirationSecondsでトークンは自動更新されるが、inlineのJWKS登録側はクラスターの鍵ローテーションに追従しないため、両者がずれる | 確認コマンドkubectl get --raw /openid/v1/jwksの出力とAnthropic側に登録済みのJWKSを突き合わせる |
いずれの場合も、まずトークンをデコードしてsub・aud・issを実際の値で確認してから、連合ルールと連合発行者の設定を突き合わせるのが最短です。JWTのペイロードはBase64URLエンコードされているだけなので、上記のようにcutとbase64 -dで中身を直接読めます。
ルールを絞り込むときの注意点
system:serviceaccount:*だけのsubject_prefixは、クラスター内のすべてのサービスアカウントにマッチしてしまい、どのPodでも連合済みのAnthropicトークンを取得できてしまいます。audienceのマッチ条件がなければ、すべてのPodがすでに持っているデフォルトaudienceのトークンにもルールがマッチします。
ルールの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リファレンスを参照してください。
まとめ
自己管理Kubernetesクラスターは、projected service account tokenとクラスター自身のOIDC発行者だけでWIFに対応できます。発行者URLが外部から到達できない構成が多いため、inlineのJWKS登録を選ぶケースが標準的です。連合ルールはnamespaceとサービスアカウント名を完全一致させ、audienceを必ず設定して、Pod側の投影設定と対にしておくのが安全です。まずは1つのnamespaceでルールを作り、認証履歴ページで想定どおりのクレームが記録されることを確認してから、他のnamespaceへ展開するのが着実な進め方です。