# 在 Kubernetes 中使用 WIF

使用投影的服务账号令牌从自管理 Kubernetes 集群认证到 Claude API。

---

自管理 Kubernetes 集群（kubeadm、k3s、OpenShift 和本地部署发行版）通过[投影的服务账号令牌](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#serviceaccount-token-volume-projection)为每个 Pod 签名 OIDC JSON Web Token (JWT)。集群的 API 服务器充当 OIDC 签发方，每个令牌的 `sub` 声明遵循格式 `system:serviceaccount:<namespace>:<service-account>`。你可以通过读取发现文档来查找集群的签发方 URL：

```bash cURL nocheck
kubectl get --raw /.well-known/openid-configuration | jq -r .issuer
```

<Note>
本页的机制（投影的服务账号令牌、集群 API 服务器作为 OIDC 签发方）是 Kubernetes 本身的原生机制，因此它适用于所有 Kubernetes 发行版。如果你在托管 Kubernetes 服务上运行，云提供者指南将引导你找到提供者管理的签发方 URL：[AWS (EKS)](/docs/en/manage-claude/wif-providers/aws#use-eks-projected-service-account-tokens)、[Google Cloud (GKE)](/docs/en/manage-claude/wif-providers/gcp) 或 [Azure (AKS)](/docs/en/manage-claude/wif-providers/azure)。如果你的集群运行 SPIRE，SPIRE OIDC Discovery Provider 是签发方而非集群 API 服务器；请参阅 [SPIFFE](/docs/en/manage-claude/wif-providers/spiffe)。对于未列出的其他发行版或托管提供者，请按照本指南操作并使用集群报告的签发方 URL。
</Note>

## 前置条件

- 熟悉 [WIF 概念](/docs/en/manage-claude/workload-identity-federation#concepts)：服务账号、联合签发方和联合规则。
- 一个在 API 服务器上配置了 [`--service-account-issuer`](https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/) 标志的 Kubernetes 集群。大多数发行版默认设置此标志；kubeadm 集群通常使用 `https://kubernetes.default.svc.cluster.local`。如果你没有直接访问 API 服务器配置的权限，你的平台团队可以确认该值。
- 以下之一，以便 Anthropic 可以验证令牌签名：
  - 签发方的 JWKS 端点可通过公共互联网在 443 端口通过 HTTPS 访问，或者
  - 你可以从集群内部获取 JWKS 并以 `inline` 模式注册（在[配置 Anthropic](#configure-anthropic) 中介绍）。
- 拥有在 Claude Console 中为你的 Anthropic 组织创建服务账号、联合签发方和联合规则的权限。

## 配置 Kubernetes

将服务账号令牌投影到你的 Pod 中，使用你的联合规则期望的受众和生命周期。`serviceAccountToken` 投影将新的 JWT 写入挂载路径，并在 `expirationSeconds` 到期前轮换它。

```yaml Pod nocheck
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

按照[设置指南](/docs/en/manage-claude/workload-identity-federation#set-up-federation)在 Claude Console 中注册联合签发方、创建 Anthropic 服务账号并创建联合规则。使用以下 Kubernetes 特定的值。

**联合签发方：** 许多自管理集群使用的签发方 URL（如 `https://kubernetes.default.svc.cluster.local`）无法从公共互联网访问。如果你的集群属于这种情况，请选择 **inline** JWKS 源并粘贴集群的密钥。从集群内部获取它们：

```bash cURL nocheck
kubectl get --raw /openid/v1/jwks
```

然后使用返回的 `keys` 数组的内容（不是外层的 `{"keys": [...]}` 包装器）配置签发方：

```json
{
  "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`。

<Warning>
使用 `inline` 密钥时，你有责任在集群轮换服务账号签名密钥时更新签发方。轮换很少发生（通常仅在集群升级期间），但在你推送新的 JWKS 之前，令牌交换会因签名错误而失败。
</Warning>

**联合规则：** 匹配服务账号的 `sub` 声明和你在投影令牌上设置的受众。

```json
{
  "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](#configure-kubernetes) 中的 Pod 规范将 `ANTHROPIC_IDENTITY_TOKEN_FILE` 设置为投影的挂载路径，同时设置了 `ANTHROPIC_FEDERATION_RULE_ID`、`ANTHROPIC_ORGANIZATION_ID`、`ANTHROPIC_SERVICE_ACCOUNT_ID` 和 `ANTHROPIC_WORKSPACE_ID`。有了这些，SDK 会在每次交换时从磁盘读取令牌，并自动刷新 Anthropic 访问令牌。

<CodeGroup>

```bash cURL nocheck
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'
```

```python Python nocheck
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)
```

```typescript TypeScript nocheck
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);
  }
}
```

```go Go nocheck hidelines={1..10,-1}
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)
}
```

```java Java nocheck hidelines={1..6,-1}
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());
}
```

```csharp C# nocheck hidelines={1..3}
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);
    }
}
```

```bash CLI nocheck
# 从 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 PHP nocheck hidelines={1..3}
<?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;
```

```ruby Ruby nocheck
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
```

</CodeGroup>

## 验证设置

成功交换会返回一个以 `sk-ant-oat01-` 开头的 `access_token` 和一个以秒为单位的 `expires_in` 值。如果出现 `400 invalid_grant`，请参阅[排查交换失败](/docs/en/manage-claude/wif-reference#troubleshoot-a-failed-exchange)；最常见的 Kubernetes 侧原因是 JWKS 密钥不匹配（对于 `inline` 模式，使用 `kubectl get --raw /openid/v1/jwks` 重新获取并更新签发方）。

## 限定规则范围

<Warning>
`subject_prefix` 为 `system:serviceaccount:*` 会匹配集群中的每个服务账号，因此任何 Pod 都可以获取联合 Anthropic 令牌。如果没有 `audience` 匹配器，规则还会匹配集群的默认受众令牌，而每个 Pod 已经投影了该令牌。
</Warning>

将规则的 `match` 块锁定到适合你用例的最窄范围：

- **固定命名空间和服务账号名称：** 使用完整的 `system:serviceaccount:<namespace>:<name>` 值，不带末尾的 `*`。
- **始终设置受众：** 在规则上要求 `audience`，并在 Pod 的 `serviceAccountToken` 投影上设置相同的值，这样默认受众令牌会被拒绝。
- **为每个命名空间使用单独的规则：** 为每个命名空间创建不同的规则和 Anthropic 服务账号，而不是放宽一个规则。
- **将 inline-JWKS 签发方限定到一个集群：** 当多个集群共享一个签发方 URL 时，将每个集群的 JWKS 注册为自己的联合签发方，并将规则仅绑定到该签发方。

## 后续步骤

- [Workload Identity Federation](/docs/en/manage-claude/workload-identity-federation)：概念、令牌交换流程和 SDK 配置选项。
- [WIF 参考文档](/docs/en/manage-claude/wif-reference)：环境变量、JWKS 源模式和规则匹配模式。
