Workload Identity Federation

使用您自己身份提供商的短期身份令牌而非长期静态 API 密钥,向 Claude API 进行工作负载认证。


Workload Identity Federation (WIF) 允许您的工作负载使用由您已运营的身份提供商 (IdP)(如 AWS IAM、Google Cloud 或任何符合标准的 OIDC 发行方(如 GitHub Actions、Kubernetes 服务账户、SPIFFE、Microsoft Entra ID 或 Okta))签发的短期 OpenID Connect (OIDC) 令牌,而非长期 sk-ant-... API 密钥,来向 Claude API 进行认证。

您的工作负载出示来自身份提供商的签名 JWT。Anthropic 根据您在 Claude Console 中配置的信任规则进行验证,并返回绑定到您组织中服务账户的短期 Anthropic 访问令牌。无需创建、存储在 CI 中、轮换或担心泄露静态密钥。

Workload Identity Federation 通过从 Anthropic 的攻击面移除静态凭据并替换为在几分钟内过期而非永不过期的令牌来增强您的安全态势。它本身并非完整的安全方案:联合认证的强度取决于签署 JWT 的上游身份提供商。请将 Workload Identity Federation 与您的 IdP 已支持的控制措施(工作负载身份绑定、条件访问、审计日志)配合使用以实现纵深防御。

工作原理

  1. 您的 IdP 向工作负载签发 JWT。 在大多数平台上这是环境自带的:Kubernetes 预测的服务账户令牌、Google Cloud 元数据服务器、Azure IMDS 或 GitHub Actions OIDC 端点。JWT 的 iss 声明标识提供商,其 sub 和其他声明标识特定的工作负载。
  2. SDK 将 JWT 交换为 Anthropic 访问令牌。 给定联合环境变量(或配置文件)和 JWT(通常从文件读取),SDK 使用 RFC 7523 jwt-bearer 授权将 JWT 发送到 POST /v1/oauth/token。Anthropic 根据您为发行方注册的 JWKS 验证签名,检查标准的 exp/nbf/iat 声明,并将 JWT 的声明与您指定的联合规则进行匹配。响应是标准的 OAuth 2.0 令牌响应(access_tokentoken_typeexpires_inscope),包含一个短期 sk-ant-oat01-... 令牌,该令牌代表匹配规则所指向的服务账户执行操作。
  3. SDK 在每个请求中发送令牌并在过期前刷新它。 您的应用程序代码构造客户端时无需设置 api_key,照常调用 API。SDK 会在令牌过期前重新执行交换。

概念

在任何工作负载可以进行联合之前,您需要在 Claude Console 中配置三个资源。它们共同表达"由发行方 X 签名的令牌,具有类似 Y 的声明,可以充当服务账户 Z。"

服务账户

服务账户svac_...)是您 Anthropic 组织内的一个命名的非人类身份。它是联合令牌所代表的主体。服务账户存在于组织级别,当您将其添加到工作空间的成员中时,它在该工作空间中变为活跃状态。在交换时,Anthropic 检查联合规则的工作空间是否与服务账户的工作空间成员身份之一匹配;铸造的令牌随后遵循该工作空间的速率限制和使用归因,与 API 密钥相同。与人类用户不同,服务账户没有电子邮件、密码和 Console 登录。

与 API 密钥的关键区别:API 密钥本身就是凭据,而服务账户拥有按需为其铸造的凭据。您可以审计哪些工作负载充当了哪个服务账户。

联合发行方

联合发行方fdis_...)向您的组织注册一个 OIDC 身份提供商。注册发行方告诉 Anthropic"此提供商签名的 JWT 可以为我的组织声明工作负载身份。"

发行方有两项配置:

  • 发行方 URL: 出现在提供商 JWT 中的确切 iss 声明值,例如 https://token.actions.githubusercontent.comhttps://oidc.eks.us-west-2.amazonaws.com/id/EXAMPLE
  • JWKS 来源: Anthropic 如何获取公钥以验证 JWT 签名。对于任何在其发行方 URL 提供 /.well-known/openid-configuration 的提供商,使用 discovery(默认)。使用 explicit_url 直接指向 JWKS 端点,或使用 inline 上传密钥集,适用于无法从公共互联网访问的发行方(例如私有 Kubernetes 集群)。

发行方和 JWKS URL 必须是 https,端口为 443,并使用解析为公共 IP 地址的公共 DNS 主机名;不接受 IP 字面量。这些约束仅适用于 Anthropic 获取的 URL;在 explicit_urlinline 模式下,issuer_url 作为字符串进行比较,可以引用内部主机名。

您通常为每个环境注册一个发行方:您的生产 EKS 集群、暂存集群和 GitHub Actions 是三个独立的发行方。

联合规则

联合规则fdrl_...)是发行方和服务账户之间的桥梁:"当来自发行方 X 的 JWT 具有类似 Y 的声明时,为服务账户 Z 铸造具有作用域 S 的令牌。"

规则定义了匹配条件、目标以及规则匹配时适用的授权作用域和令牌有效期:

  • 匹配: 传入 JWT 必须满足的条件。您可以匹配 subject_prefix(例如 system:serviceaccount:prod:worker,或带有尾部 * 进行前缀匹配)、精确的 audience、精确声明值的映射、用于复杂逻辑的 CEL condition 表达式,或它们的任意组合。至少必须设置 subject_prefixclaimscondition 之一,并且所有配置的匹配器都必须通过,JWT 才会被接受。
  • 目标: 匹配的 JWT 映射到的服务账户。
  • 授权: 铸造令牌上授予的 OAuth scope。默认为 workspace:developer,授予与为该工作空间签发的 API 密钥相同的访问权限。某些产品在您从其流程创建规则时会锁定作用域;例如,MCP 隧道 的创建隧道模态框会创建作用域为 org:manage_tunnels 的规则。参见 OAuth 作用域。规则还设置 token_lifetime_seconds(60 到 86400,默认 3600)。

一个发行方可以有多个规则:每个团队、命名空间或权限级别一个。规则按 ID 评估:客户端在交换请求中指定使用哪个规则,Anthropic 验证 JWT 是否满足该规则的匹配条件。没有隐式规则搜索。

设置联合

您需要 Anthropic 组织的管理员访问权限、具有可访问 JWKS 端点的 OIDC 身份提供商(或可粘贴的 JWKS 文档,适用于气隙集群),以及能够从该提供商获取身份令牌的工作负载。

在 Claude Console 中,前往 Settings → Workload identity

  1. 注册发行方

    Issuers 选项卡上,选择 Create issuer

    字段
    Name供您参考的标签,例如 prod-eksgha。小写字母、数字和连字符。
    Issuer URL您的 IdP 在其 JWT 中放入的确切 iss 声明。如果不确定,请解码示例令牌:jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson | .iss' token
    JWKS 来源大多数托管 IdP 使用 discovery。仅在 discovery 不可用时选择 explicit_urlinline
    Discovery base / JWKS URL / Inline keys模式特定。当 IdP 在发行方 URL 提供 .well-known 时,discovery 模式留空。
    CA cert PEM仅当您的 IdP 使用私有 CA 提供 TLS 时填写。大多数托管 IdP 使用公共 CA,因此留空。

    Console 包含 AWS 和 Google Cloud 的预设,可预填发行方 URL 模式和合理的默认规则,以及适用于任何其他符合标准的提供商(如 GitHub Actions、Kubernetes 服务账户发行方、Microsoft Entra ID 或 Okta)的通用 OIDC 选项。

  2. 创建服务账户

    前往 Settings → Service accounts → Create service account。提供名称(例如 inference-workerci-deploy)和可选描述。

    这是您铸造的令牌所代表的身份。从该工作空间的 Members 页面将服务账户添加到每个它应该参与的工作空间。下一步中的联合规则指向一个工作空间,铸造的令牌作用域为该工作空间的速率限制和使用归因。记下服务账户 ID(svac_...)。

  3. 创建联合规则

    返回 Workload identity 页面,打开 Federation rules 选项卡并选择 Create rule

    部分
    Basic info名称和可选描述。选择您在第 1 步中注册的发行方。
    Match选择 Static 进行主题前缀、受众和精确声明匹配,或选择 CEL 使用表达式。尽可能具体,匹配您的 IdP 允许的声明:匹配过于宽泛的规则会授予超出预期的访问权限。
    Target选择您在第 2 步中创建的服务账户。
    AuthorizationOAuth 作用域(默认为 workspace:developer,或产品特定的作用域如 org:manage_tunnels;参见 OAuth 作用域)和令牌有效期(秒)。

    记下规则的 ID(fdrl_...)。您的工作负载在每次令牌交换请求中传递此 ID。

从您的工作负载进行认证

配置联合后,您的工作负载在运行时将其 IdP 签发的 JWT 交换为 Anthropic 令牌。SDK 为您处理交换和刷新循环。cURL 选项卡展示了 shell 脚本、调试或无 SDK 支持的语言的底层 HTTP 交换。

构造 SDK 客户端

您可以使用显式凭据或无参数构造客户端。无参数时,SDK 从环境变量或活动配置文件解析凭据,如凭据优先级所述。无参数形式是生产工作负载的推荐模式:在各处部署相同的容器镜像,并按环境注入 ANTHROPIC_FEDERATION_RULE_IDANTHROPIC_ORGANIZATION_IDANTHROPIC_SERVICE_ACCOUNT_IDANTHROPIC_WORKSPACE_IDANTHROPIC_IDENTITY_TOKEN_FILE

# 1. Acquire your IdP's JWT (platform-specific; see the per-provider guides).
JWT=$(cat /var/run/secrets/anthropic.com/token)

# 2. Exchange it for a short-lived Anthropic access token.
RESPONSE=$(curl -sS https://api.anthropic.com/v1/oauth/token \
  -H "content-type: application/json" \
  --data @- <<JSON
{
  "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
  "assertion": "$JWT",
  "federation_rule_id": "fdrl_...",
  "organization_id": "00000000-0000-0000-0000-000000000000",
  "service_account_id": "svac_...",
  "workspace_id": "wrkspc_..."
}
JSON
)

ACCESS_TOKEN=$(jq -r .access_token <<<"$RESPONSE")
EXPIRES_IN=$(jq -r .expires_in <<<"$RESPONSE")  # seconds; re-exchange before this elapses

# 3. Call the API with the access token in the Authorization: Bearer header.
curl -sS https://api.anthropic.com/v1/messages \
  -H "authorization: Bearer $ACCESS_TOKEN" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  --data @- <<'JSON' | jq -r '.content[0].text'
{
  "model": "claude-sonnet-4-6",
  "max_tokens": 1024,
  "messages": [{"role": "user", "content": "Hello, Claude"}]
}
JSON
from anthropic import Anthropic, WorkloadIdentityCredentials, IdentityTokenFile

client = Anthropic(
    credentials=WorkloadIdentityCredentials(
        identity_token_provider=IdentityTokenFile(
            "/var/run/secrets/anthropic.com/token"
        ),
        federation_rule_id="fdrl_...",
        organization_id="00000000-0000-0000-0000-000000000000",
        service_account_id="svac_...",
        workspace_id="wrkspc_...",
    ),
)

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";
import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation";
import { identityTokenFromFile } from "@anthropic-ai/sdk/lib/credentials/identity-token";

const client = new Anthropic({
  credentials: oidcFederationProvider({
    identityTokenProvider: identityTokenFromFile("/var/run/secrets/anthropic.com/token"),
    federationRuleId: "fdrl_...",
    organizationId: "00000000-0000-0000-0000-000000000000",
    serviceAccountId: "svac_...",
    workspaceId: "wrkspc_...",
    baseURL: "https://api.anthropic.com",
    fetch
  })
});

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"
	"log"

	"github.com/anthropics/anthropic-sdk-go"
	"github.com/anthropics/anthropic-sdk-go/option"
)

func main() {
	client := anthropic.NewClient(
		option.WithFederationTokenProvider(
			option.IdentityTokenFile("/var/run/secrets/anthropic.com/token"),
			option.FederationOptions{
				FederationRuleID: "fdrl_...",
				OrganizationID:   "00000000-0000-0000-0000-000000000000",
				ServiceAccountID: "svac_...",
				WorkspaceID:      "wrkspc_...",
			},
		),
	)

	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 {
		log.Fatal(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() {
    // Reads ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
    // ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, and ANTHROPIC_IDENTITY_TOKEN_FILE
    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 credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions
{
    FederationRuleId = "fdrl_...",
    OrganizationId = "00000000-0000-0000-0000-000000000000",
    ServiceAccountId = "svac_...",
    WorkspaceId = "wrkspc_...",
    IdentityTokenProvider = new FileIdentityTokenProvider("/var/run/secrets/anthropic.com/token"),
});
using var client = new AnthropicOidcClient(credentials);

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);
    }
}
<?php

require_once __DIR__ . '/vendor/autoload.php';

// Reads ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
// ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, and ANTHROPIC_IDENTITY_TOKEN_FILE
$client = new Anthropic\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"

# Reads ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
# ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, and 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

令牌交换响应遵循 RFC 6749 §5.1。有关字段参考,请参阅令牌交换响应

凭据优先级

每个 SDK 按相同的五层顺序解析凭据:构造函数参数,然后 ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN,然后显式 ANTHROPIC_PROFILE,然后联合环境变量,然后隐式活动配置文件。第一个产生凭据的来源胜出。

Warning

ANTHROPIC_API_KEY 位于联合层之上,因此环境中残留的密钥会静默遮蔽联合。 将工作负载从 API 密钥迁移到 Workload Identity Federation 时,请确认在该工作负载运行的所有位置 (容器环境、CI 密钥、shell 配置文件)中 ANTHROPIC_API_KEY 已取消设置。CLI 的 ant auth status 命令报告哪个来源胜出。

有关完整的优先级表、每层语义和配置文件架构,请参阅 WIF 参考中的凭据优先级

从 API 密钥迁移

要在不停机的情况下将现有工作负载从静态 API 密钥切换到联合:

  1. 并行配置联合。 完成设置指南并确认联合规则匹配您工作负载的令牌。暂时保留现有的 ANTHROPIC_API_KEY
  2. 测试哪个凭据胜出。 在工作负载内部运行 ant auth status(或检查 SDK 调试日志)。由于 ANTHROPIC_API_KEY 在优先级链中位于联合层之上,此阶段 API 密钥仍然胜出。
  3. 在所有注入位置取消设置 ANTHROPIC_API_KEY 从 CI 密钥、容器环境和 shell 配置文件中移除它(参见前面的警告)。重新运行 ant auth status 并确认联合来源现在被选中。
  4. 吊销 API 密钥。 一旦工作负载在联合令牌上运行,在 Claude Console 的 Settings → API keys 中删除该密钥。

令牌有效期和刷新

铸造的 Anthropic 令牌的有效期是规则的 token_lifetime_seconds(默认 3600 秒)和您出示的 IdP JWT 剩余有效期的两倍中较小的一个,最低 60 秒。第二个限制防止 Anthropic 令牌的存活时间超过其派生的上游身份太多。

SDK 缓存令牌并按基于 botocore 的两层计划刷新它:

  • 建议刷新 在过期前 120 秒。SDK 尝试新的交换。如果令牌端点不可达,SDK 继续提供缓存的令牌,该令牌仍然有效约 90 秒。
  • 强制刷新 在过期前 30 秒。此时交换失败会引发错误。缓存的令牌太接近过期,不安全。

由于 SDK 在每次交换时重新读取 ANTHROPIC_IDENTITY_TOKEN_FILE,它会透明地获取轮换的预测令牌(例如 Kubernetes 服务账户令牌在 exp 之前很久就会轮换)。

身份提供商

每个指南涵盖该平台上 JWT 的来源、其声明的外观,以及要注册的发行方和规则配置。

另请参阅

  • WIF 参考:环境变量、配置文件架构、验证规则和错误代码
  • 认证:Anthropic SDK 的所有认证选项