在 Kubernetes 中使用 WIF
使用投影的服务账号令牌从自管理 Kubernetes 集群认证到 Claude API。
自管理 Kubernetes 集群(kubeadm、k3s、OpenShift 和本地部署发行版)通过投影的服务账号令牌为每个 Pod 签名 OIDC JSON Web Token (JWT)。集群的 API 服务器充当 OIDC 签发方,每个令牌的 sub 声明遵循格式 system:serviceaccount:<namespace>:<service-account>。你可以通过读取发现文档来查找集群的签发方 URL:
kubectl get --raw /.well-known/openid-configuration | jq -r .issuer
本页的机制(投影的服务账号令牌、集群 API 服务器作为 OIDC 签发方)是 Kubernetes 本身的原生机制,因此它适用于所有 Kubernetes 发行版。如果你在托管 Kubernetes 服务上运行,云提供者指南将引导你找到提供者管理的签发方 URL:AWS (EKS)、Google Cloud (GKE) 或 Azure (AKS)。如果你的集群运行 SPIRE,SPIRE OIDC Discovery Provider 是签发方而非集群 API 服务器;请参阅 SPIFFE。对于未列出的其他发行版或托管提供者,请按照本指南操作并使用集群报告的签发方 URL。
前置条件
- 熟悉 WIF 概念:服务账号、联合签发方和联合规则。
- 一个在 API 服务器上配置了
--service-account-issuer标志的 Kubernetes 集群。大多数发行版默认设置此标志;kubeadm 集群通常使用https://kubernetes.default.svc.cluster.local。如果你没有直接访问 API 服务器配置的权限,你的平台团队可以确认该值。 - 以下之一,以便 Anthropic 可以验证令牌签名:
- 签发方的 JWKS 端点可通过公共互联网在 443 端口通过 HTTPS 访问,或者
- 你可以从集群内部获取 JWKS 并以
inline模式注册(在配置 Anthropic 中介绍)。
- 拥有在 Claude Console 中为你的 Anthropic 组织创建服务账号、联合签发方和联合规则的权限。
配置 Kubernetes
将服务账号令牌投影到你的 Pod 中,使用你的联合规则期望的受众和生命周期。serviceAccountToken 投影将新的 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 # required when the rule covers multiple workspaces
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 中注册联合签发方、创建 Anthropic 服务账号并创建联合规则。使用以下 Kubernetes 特定的值。
联合签发方: 许多自管理集群使用的签发方 URL(如 https://kubernetes.default.svc.cluster.local)无法从公共互联网访问。如果你的集群属于这种情况,请选择 inline JWKS 源并粘贴集群的密钥。从集群内部获取它们:
kubectl get --raw /openid/v1/jwks
然后使用返回的 keys 数组的内容(不是外层的 {"keys": [...]} 包装器)配置签发方:
{
"name": "onprem-k8s",
"issuer_url": "https://kubernetes.default.svc.cluster.local",
"jwks_source": "inline",
"jwks_keys": [{ "kty": "RSA", "kid": "...", "n": "...", "e": "AQAB" }]
}
在 inline 模式下,issuer_url 仅与 JWT 的 iss 声明进行比较;Anthropic 永远不会尝试访问它。如果你的签发方可公开访问,请改用 "jwks_source": "discovery" 并省略 jwks_keys。
使用 inline 密钥时,你有责任在集群轮换服务账号签名密钥时更新签发方。轮换很少发生(通常仅在集群升级期间),但在你推送新的 JWKS 之前,令牌交换会因签名错误而失败。
联合规则: 匹配服务账号的 sub 声明和你在投影令牌上设置的受众。
{
"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
}
尽可能精确地匹配工作负载。只有当命名空间中的每个服务账号都需要映射到同一个 Anthropic 服务账号时,才将 subject_prefix 放宽为 system:serviceaccount:inference:*(末尾的 * 使其成为前缀匹配)。将规则的 fdrl_... ID 添加到 Pod 的 ANTHROPIC_FEDERATION_RULE_ID 环境变量中。
获取并使用令牌
配置 Kubernetes 中的 Pod 规范将 ANTHROPIC_IDENTITY_TOKEN_FILE 设置为投影的挂载路径,同时设置了 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、ANTHROPIC_SERVICE_ACCOUNT_ID 和 ANTHROPIC_WORKSPACE_ID。有了这些,SDK 会在每次交换时从磁盘读取令牌,并自动刷新 Anthropic 访问令牌。
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 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-sonnet-4-6",
"max_tokens": 1024,
"messages": [{"role": "user", "content": "Hello, Claude"}]
}' | jq -r '.content[0].text'
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-sonnet-4-6",
max_tokens=1024,
messages=[{"role": "user", "content": "Hello, Claude"}],
)
print(message.content[0].text)
import Anthropic from "@anthropic-ai/sdk";
// 从 Pod 环境中读取 ANTHROPIC_IDENTITY_TOKEN_FILE、ANTHROPIC_FEDERATION_RULE_ID、
// ANTHROPIC_ORGANIZATION_ID、ANTHROPIC_SERVICE_ACCOUNT_ID 和 ANTHROPIC_WORKSPACE_ID。
const client = new Anthropic();
const message = await client.messages.create({
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello, Claude" }]
});
for (const block of message.content) {
if (block.type === "text") {
console.log(block.text);
}
}
package main
import (
"context"
"fmt"
"github.com/anthropics/anthropic-sdk-go"
)
func main() {
// 从 Pod 环境中读取 ANTHROPIC_IDENTITY_TOKEN_FILE、ANTHROPIC_FEDERATION_RULE_ID、
// ANTHROPIC_ORGANIZATION_ID、ANTHROPIC_SERVICE_ACCOUNT_ID 和 ANTHROPIC_WORKSPACE_ID。
client := anthropic.NewClient()
message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
Model: anthropic.ModelClaudeSonnet4_6,
MaxTokens: 1024,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")),
},
})
if err != nil {
panic(err)
}
fmt.Println(message.Content[0].Text)
}
import com.anthropic.client.AnthropicClient;
import com.anthropic.client.okhttp.AnthropicOkHttpClient;
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.Model;
void main() {
AnthropicClient client = AnthropicOkHttpClient.fromEnv();
var message = client.messages().create(MessageCreateParams.builder()
.model(Model.CLAUDE_SONNET_4_6)
.maxTokens(1024)
.addUserMessage("Hello, Claude")
.build());
IO.println(message.content());
}
using Anthropic.Models.Messages;
using Anthropic.Oidc;
var result = AnthropicCredentials.Resolve()
?? throw new InvalidOperationException("No federation credentials found in environment");
using var client = new AnthropicOidcClient(result);
var message = await client.Messages.Create(new()
{
Model = Model.ClaudeSonnet4_6,
MaxTokens = 1024,
Messages = [new() { Role = Role.User, Content = "Hello, Claude" }],
});
foreach (var block in message.Content)
{
if (block.Value is TextBlock textBlock)
{
Console.WriteLine(textBlock.Text);
}
}
# 从 Pod 环境中读取 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
# ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID 和 ANTHROPIC_IDENTITY_TOKEN_FILE。
ant messages create \
--model claude-sonnet-4-6 \
--max-tokens 1024 \
--message '{role: user, content: "Hello, Claude"}'
<?php
require 'vendor/autoload.php';
use Anthropic\Client;
// 从 Pod 环境中读取 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
// ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID 和 ANTHROPIC_IDENTITY_TOKEN_FILE。
$client = new Client();
$message = $client->messages->create(
model: 'claude-sonnet-4-6',
maxTokens: 1024,
messages: [['role' => 'user', 'content' => 'Hello, Claude']],
);
echo $message->content[0]->text, PHP_EOL;
require "anthropic"
# 从 Pod 环境中读取 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
# ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID 和 ANTHROPIC_IDENTITY_TOKEN_FILE。
client = Anthropic::Client.new
message = client.messages.create(
model: "claude-sonnet-4-6",
max_tokens: 1024,
messages: [{role: "user", content: "Hello, Claude"}]
)
puts message.content.first.text
验证设置
成功交换会返回一个以 sk-ant-oat01- 开头的 access_token 和一个以秒为单位的 expires_in 值。如果出现 400 invalid_grant,请参阅排查交换失败;最常见的 Kubernetes 侧原因是 JWKS 密钥不匹配(对于 inline 模式,使用 kubectl get --raw /openid/v1/jwks 重新获取并更新签发方)。
限定规则范围
subject_prefix 为 system:serviceaccount:* 会匹配集群中的每个服务账号,因此任何 Pod 都可以获取联合 Anthropic 令牌。如果没有 audience 匹配器,规则还会匹配集群的默认受众令牌,而每个 Pod 已经投影了该令牌。
将规则的 match 块锁定到适合你用例的最窄范围:
- 固定命名空间和服务账号名称: 使用完整的
system:serviceaccount:<namespace>:<name>值,不带末尾的*。 - 始终设置受众: 在规则上要求
audience,并在 Pod 的serviceAccountToken投影上设置相同的值,这样默认受众令牌会被拒绝。 - 为每个命名空间使用单独的规则: 为每个命名空间创建不同的规则和 Anthropic 服务账号,而不是放宽一个规则。
- 将 inline-JWKS 签发方限定到一个集群: 当多个集群共享一个签发方 URL 时,将每个集群的 JWKS 注册为自己的联合签发方,并将规则仅绑定到该签发方。
后续步骤
- Workload Identity Federation:概念、令牌交换流程和 SDK 配置选项。
- WIF 参考文档:环境变量、JWKS 源模式和规则匹配模式。