自管理的 Kubernetes 集群(kubeadm、k3s、OpenShift 以及本地部署发行版)通过 projected service account tokens(投射的服务账户令牌)为每个 pod 签发 OIDC “JSON Web Token”(JSON Web 令牌),即 JWT。集群的 API 服务器充当 OIDC issuer(颁发者),每个令牌的 sub 声明遵循 system:serviceaccount:<namespace>:<service-account> 的格式。您可以通过读取集群的发现文档来找到其颁发者 URL:

cURL

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 端点可通过 HTTPS 在 443 端口从公共互联网访问,或者
    • 您可以从集群内部获取 JWKS 并以 inline 模式注册它(在配置 Anthropic 中介绍)。
  • 拥有在 Claude Console 中为您的 Anthropic 组织创建服务账户、联合颁发者和联合规则的权限。

配置 Kubernetes

将服务账户令牌投射到您的 pod 中,并使用您的联合规则所期望的受众和生命周期。serviceAccountToken 投射会将一个新的 JWT 写入挂载路径,并在 expirationSeconds 到期之前对其进行轮换。

Pod

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 中,前往 Workload identity 页面(platform.claude.com/settings/workload-identity-federation),点击 Connect workload,然后选择 Kubernetes 磁贴。向导将引导您完成注册颁发者、创建服务账户和创建联合规则的过程。

向导会为您创建这些资源。无论您是在向导中输入这些值,还是将它们发送到 Admin API,都请使用以下值:

联合颁发者: 许多自管理集群使用诸如 https://kubernetes.default.svc.cluster.local 之类无法从公共互联网访问的颁发者 URL。如果您的集群属于这种情况,请选择 inline JWKS 来源并粘贴集群的密钥。从集群内部获取它们:

cURL

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 从不尝试访问它。如果您的颁发者可公开访问,请改用 "jwks": {"type": "discovery"}。

警告

使用 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 访问令牌。

cURL

JWT=$(cat "$ANTHROPIC_IDENTITY_TOKEN_FILE")

ACCESS_TOKEN=$(curl -sS https://api.anthropic.com/v1/oauth/token \
  -H "content-type: application/json" \
  -d @- <<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-opus-5-5",
    "max_tokens": 1024,
    "messages": [{"role": "user", "content": "Hello, Claude"}]
  }'

CLI

# 读取 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
# ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID 和 ANTHROPIC_IDENTITY_TOKEN_FILE
ant messages create \
  --model claude-opus-5-5 \
  --max-tokens 1024 \
  --message '{role: user, content: "Hello, Claude"}'

Python

import anthropic

# 读取 ANTHROPIC_IDENTITY_TOKEN_FILE、ANTHROPIC_FEDERATION_RULE_ID、
# ANTHROPIC_ORGANIZATION_ID、ANTHROPIC_SERVICE_ACCOUNT_ID 和 ANTHROPIC_WORKSPACE_ID
# (均来自 pod 的环境变量)。
client = anthropic.Anthropic()

message = client.messages.create(
    model="claude-opus-5-5",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello, Claude"}],
)
print(next(block.text for block in message.content if block.type == "text"))

TypeScript

import Anthropic from "@anthropic-ai/sdk";

// 读取 ANTHROPIC_IDENTITY_TOKEN_FILE、ANTHROPIC_FEDERATION_RULE_ID、
// ANTHROPIC_ORGANIZATION_ID、ANTHROPIC_SERVICE_ACCOUNT_ID 和 ANTHROPIC_WORKSPACE_ID
// (均来自 pod 的环境变量)。
const client = new Anthropic();

const message = await client.messages.create({
  model: "claude-opus-5-5",
  max_tokens: 1024,
  messages: [{ role: "user", content: "Hello, Claude" }]
});
for (const block of message.content) {
  if (block.type === "text") {
    console.log(block.text);
  }
}

C#

// 读取 ANTHROPIC_IDENTITY_TOKEN_FILE、ANTHROPIC_FEDERATION_RULE_ID、
// ANTHROPIC_ORGANIZATION_ID、ANTHROPIC_SERVICE_ACCOUNT_ID 和 ANTHROPIC_WORKSPACE_ID
// (从 pod 的环境变量中读取)。
using var client = new AnthropicClient();

var message = await client.Messages.Create(new()
{
    Model = Model.ClaudeOpus5_5,
    MaxTokens = 1024,
    Messages = [new UserMessageParam { Content = "Hello, Claude" }],
});
foreach (var block in message.Content)
{
    if (block.Value is TextBlock textBlock)
    {
        Console.WriteLine(textBlock.Text);
    }
}

Go

// 读取 ANTHROPIC_IDENTITY_TOKEN_FILE、ANTHROPIC_FEDERATION_RULE_ID、
// ANTHROPIC_ORGANIZATION_ID、ANTHROPIC_SERVICE_ACCOUNT_ID 和 ANTHROPIC_WORKSPACE_ID
// (均来自 pod 的环境变量)。
client := anthropic.NewClient()

message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
	Model:     anthropic.ModelClaudeOpus5_5,
	MaxTokens: 1024,
	Messages: []anthropic.MessageParam{
		anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")),
	},
})
if err != nil {
	panic(err)
}
for _, block := range message.Content {
	if textBlock, ok := block.AsAny().(anthropic.TextBlock); ok {
		fmt.Println(textBlock.Text)
		break
	}
}

Java

AnthropicClient client = AnthropicOkHttpClient.fromEnv();

var message = client.messages().create(MessageCreateParams.builder()
    .model(Model.CLAUDE_OPUS_5_5)
    .maxTokens(1024)
    .addUserMessage("Hello, Claude")
    .build());

IO.println(message.content());

PHP

use Anthropic\Client;

// 读取 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-opus-5-5',
    maxTokens: 1024,
    messages: [['role' => 'user', 'content' => 'Hello, Claude']],
);
$textBlock = array_find($message->content, static fn ($block): bool => $block->type === 'text');
echo $textBlock->text, PHP_EOL;

Ruby

require "anthropic"

# 读取 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-opus-5-5",
  max_tokens: 1024,
  messages: [{role: "user", content: "Hello, Claude"}]
)
puts message.content.find { it.type == :text }.text

验证设置

成功的交换会返回一个以 sk-ant-oat01- 开头的 access_token 以及一个以秒为单位的 expires_in 值。如果交换失败并返回不透明的 401 authentication_error 响应(消息为 Authentication failed),请在 Claude Console 中查看身份验证历史记录页面以了解拒绝原因,并参阅排查失败的交换。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 注册为各自的联合颁发者,并仅将规则绑定到该颁发者。

后续步骤