在 AWS 中使用 WIF

通过 Workload Identity Federation 和 STS 签发的身份令牌,将 AWS 上的 Lambda、EC2、ECS 或 EKS 工作负载认证到 Claude API。


AWS 工作负载可以通过交换 AWS 签名的 OIDC 身份令牌来认证到 Claude API,无需使用静态 API 密钥。推荐的方式是调用 AWS STS GetWebIdentityToken API,该方式适用于任何拥有 AWS 凭证的工作负载:Lambda、EC2、ECS 和 EKS。EKS 工作负载还可以使用 Kubernetes 投影令牌方式,该方式配置步骤更少,但仅适用于 Pod 内部。

本指南展示了两种方式。有关基础概念(服务账号、联合签发方和联合规则),请参阅 Workload Identity Federation

前置条件

  • 熟悉 WIF 概念:服务账号、联合签发方和联合规则。
  • 一个附加了 IAM 角色的 AWS 工作负载(EKS Pod、ECS 任务、Lambda 函数或 EC2 实例)。
  • 工作负载中可用的 aws CLI 或 AWS SDK。
  • 拥有在 Claude Console 中为你的 Anthropic 组织创建服务账号、联合签发方和联合规则的权限。

使用 STS Web 身份令牌(推荐)

AWS STS GetWebIdentityToken API 返回一个由 AWS 签名的 OIDC 令牌,用于声明调用者的 IAM 身份。由于它使用工作负载的环境 AWS 凭证,因此同一集成适用于 Lambda、EC2、ECS 和 EKS。

配置 AWS

  1. 为账户启用出站 Web 身份联合

    这是一个账户级别的标志,默认关闭。在 AWS 控制台中打开 IAM,选择 Account settings,然后启用 Outbound web identity federation。要通过编程方式启用:

    python3 -c "import boto3; boto3.client('iam').enable_outbound_web_identity_federation()"
    

    如果未启用,调用 GetWebIdentityToken 将失败并返回 OutboundWebIdentityFederationDisabledException

  2. 授予工作负载的 IAM 角色调用该 API 的权限

    将以下策略附加到你的 Lambda 函数、EC2 实例或 ECS 任务运行所使用的 IAM 角色:

    {
      "Version": "2012-10-17",
      "Statement": [
        {
          "Effect": "Allow",
          "Action": ["sts:GetWebIdentityToken"],
          "Resource": "*"
        }
      ]
    }
    
  3. 查找你账户的 STS 签发方 URL

    启用出站联合后,IAM > Account settings 页面会显示一个 Get Token Issuer URL 字段,其值形如 https://<uuid>.tokens.sts.global.api.aws。此 URL 对你的 AWS 账户是唯一的;复制它以供下一步使用。要通过编程方式获取:

    python3 -c "import boto3; print(boto3.client('iam').get_outbound_web_identity_federation_info())"
    

配置 Anthropic

按照设置指南在 Claude Console 中注册联合签发方、创建 Anthropic 服务账号并创建联合规则。使用以下 STS 特定的值。

联合签发方: 注册你在上一步中复制的每个账户的 STS 签发方 URL。它公开了一个公共 JWKS 端点,因此使用发现模式。

{
  "name": "aws-sts",
  "issuer_url": "https://<uuid>.tokens.sts.global.api.aws",
  "jwks_source": "discovery"
}

联合规则: 匹配你传递给 GetWebIdentityToken 的受众以及调用角色的 IAM 角色 ARN(在 sub 声明中)。sub 值是调用 API 的工作负载的 IAM 角色 ARN,格式为 arn:aws:iam::<account>:role/<role-name>。令牌还携带一个 https://sts.amazonaws.com/ 声明,其中包含 aws_accountorg_idprincipal_id 和你传递的任何 request_tags;你可以使用规则的 claims 映射或 CEL condition 来匹配这些值,以实现更精细的控制。

{
  "name": "prod-inference",
  "issuer_id": "fdis_...",
  "match": {
    "subject_prefix": "arn:aws:iam::123456789012:role/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
}

尽可能精确地匹配工作负载。使用精确的角色 ARN,只有在多个 IAM 角色需要映射到同一个 Anthropic 服务账号时才放宽 subject_prefix(例如,改为 arn:aws:iam::123456789012:role/*)。

获取并使用令牌

使用 https://api.anthropic.com 作为受众调用 GetWebIdentityToken,然后将结果传递给 SDK 的联合凭证。令牌提供者是一个可调用对象,因此 SDK 在每次刷新时会重新调用 STS。

Note

GetWebIdentityToken 仅在区域 STS 端点上可用。如果你收到 'STS' object has no attribute 'get_web_identity_token' 或类似错误,请将你的 STS 客户端固定到一个区域(例如 boto3.client("sts", region_name="us-east-1")),并确保你的 AWS SDK 版本足够新以包含该 API。

JWT=$(aws sts get-web-identity-token \
  --region us-east-1 \
  --audience "https://api.anthropic.com" \
  --signing-algorithm RS256 \
  --duration-seconds 900 \
  --query WebIdentityToken --output text)

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": "$ANTHROPIC_FEDERATION_RULE_ID",
  "organization_id": "$ANTHROPIC_ORGANIZATION_ID",
  "service_account_id": "$ANTHROPIC_SERVICE_ACCOUNT_ID",
  "workspace_id": "$ANTHROPIC_WORKSPACE_ID"
}
JSON
)

ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token)

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 from AWS"}]
  }' | jq -r '.content[0].text'
import os

import anthropic
import boto3
from anthropic import WorkloadIdentityCredentials


def get_sts_web_identity_token() -> str:
    sts = boto3.client("sts", region_name="us-east-1")
    resp = sts.get_web_identity_token(
        Audience=["https://api.anthropic.com"],
        SigningAlgorithm="RS256",
        DurationSeconds=900,
    )
    return resp["WebIdentityToken"]


client = anthropic.Anthropic(
    credentials=WorkloadIdentityCredentials(
        identity_token_provider=get_sts_web_identity_token,
        federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"],
        organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"],
        service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"],
        workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"),
    ),
)

message = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello from AWS"}],
)
print(message.content[0].text)
import Anthropic from "@anthropic-ai/sdk";
import { oidcFederationProvider } from "@anthropic-ai/sdk/lib/credentials/oidc-federation";
import { STSClient, GetWebIdentityTokenCommand } from "@aws-sdk/client-sts";

const sts = new STSClient({ region: "us-east-1" });

async function getStsWebIdentityToken(): Promise<string> {
  const out = await sts.send(
    new GetWebIdentityTokenCommand({
      Audience: ["https://api.anthropic.com"],
      SigningAlgorithm: "RS256",
      DurationSeconds: 900
    })
  );
  return out.WebIdentityToken!;
}

const client = new Anthropic({
  credentials: oidcFederationProvider({
    identityTokenProvider: getStsWebIdentityToken,
    federationRuleId: process.env.ANTHROPIC_FEDERATION_RULE_ID!,
    organizationId: process.env.ANTHROPIC_ORGANIZATION_ID!,
    serviceAccountId: process.env.ANTHROPIC_SERVICE_ACCOUNT_ID,
    workspaceId: process.env.ANTHROPIC_WORKSPACE_ID,
    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 from AWS" }]
});
for (const block of message.content) {
  if (block.type === "text") {
    console.log(block.text);
  }
}
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/anthropics/anthropic-sdk-go"
	"github.com/anthropics/anthropic-sdk-go/option"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/sts"
)

func main() {
	ctx := context.TODO()
	cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion("us-east-1"))
	if err != nil {
		panic(err)
	}
	stsClient := sts.NewFromConfig(cfg)

	getStsToken := option.IdentityTokenFunc(func(ctx context.Context) (string, error) {
		out, err := stsClient.GetWebIdentityToken(ctx, &sts.GetWebIdentityTokenInput{
			Audience:         []string{"https://api.anthropic.com"},
			SigningAlgorithm: "RS256",
			DurationSeconds:  aws.Int32(900),
		})
		if err != nil {
			return "", err
		}
		return *out.WebIdentityToken, nil
	})

	client := anthropic.NewClient(
		option.WithFederationTokenProvider(getStsToken, option.FederationOptions{
			FederationRuleID: os.Getenv("ANTHROPIC_FEDERATION_RULE_ID"),
			OrganizationID:   os.Getenv("ANTHROPIC_ORGANIZATION_ID"),
			ServiceAccountID: os.Getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"),
			WorkspaceID:      os.Getenv("ANTHROPIC_WORKSPACE_ID"),
		}),
	)

	message, err := client.Messages.New(ctx, anthropic.MessageNewParams{
		Model:     anthropic.ModelClaudeSonnet4_6,
		MaxTokens: 1024,
		Messages: []anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock("Hello from AWS")),
		},
	})
	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.credentials.IdentityTokenProvider;
import com.anthropic.models.messages.MessageCreateParams;
import com.anthropic.models.messages.Model;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.sts.StsClient;
import software.amazon.awssdk.services.sts.model.GetWebIdentityTokenRequest;

void main() {
    StsClient sts = StsClient.builder().region(Region.US_EAST_1).build();

    IdentityTokenProvider getStsToken = () -> sts.getWebIdentityToken(
                    GetWebIdentityTokenRequest.builder()
                            .audience("https://api.anthropic.com")
                            .signingAlgorithm("RS256")
                            .durationSeconds(900)
                            .build())
            .webIdentityToken();

    AnthropicClient client = AnthropicOkHttpClient.builder()
            .federationTokenProvider(
                    getStsToken,
                    System.getenv("ANTHROPIC_FEDERATION_RULE_ID"),
                    System.getenv("ANTHROPIC_ORGANIZATION_ID"),
                    System.getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"))
            .build();

    var message = client.messages().create(MessageCreateParams.builder()
            .model(Model.CLAUDE_SONNET_4_6)
            .maxTokens(1024)
            .addUserMessage("Hello from AWS")
            .build());

    IO.println(message.content());
}
using Amazon.SecurityToken;
using Amazon.SecurityToken.Model;
using Anthropic.Models.Messages;
using Anthropic.Oidc;

var credentials = new WorkloadIdentityCredentials(new WorkloadIdentityOptions
{
    FederationRuleId = Environment.GetEnvironmentVariable("ANTHROPIC_FEDERATION_RULE_ID")!,
    OrganizationId = Environment.GetEnvironmentVariable("ANTHROPIC_ORGANIZATION_ID"),
    ServiceAccountId = Environment.GetEnvironmentVariable("ANTHROPIC_SERVICE_ACCOUNT_ID"),
    WorkspaceId = Environment.GetEnvironmentVariable("ANTHROPIC_WORKSPACE_ID"),
    IdentityTokenProvider = new StsTokenProvider(),
});
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 from AWS" }],
});
foreach (var block in message.Content)
{
    if (block.Value is TextBlock textBlock)
    {
        Console.WriteLine(textBlock.Text);
    }
}

class StsTokenProvider : IIdentityTokenProvider
{
    private readonly AmazonSecurityTokenServiceClient _sts = new(Amazon.RegionEndpoint.USEast1);

    public async Task<string> GetIdentityTokenAsync(CancellationToken ct = default)
    {
        var resp = await _sts.GetWebIdentityTokenAsync(new GetWebIdentityTokenRequest
        {
            Audience = ["https://api.anthropic.com"],
            SigningAlgorithm = "RS256",
            DurationSeconds = 900,
        }, ct);
        return resp.WebIdentityToken;
    }
}
TOKEN_FILE=$(mktemp)
aws sts get-web-identity-token \
  --region us-east-1 \
  --audience "https://api.anthropic.com" \
  --signing-algorithm RS256 \
  --duration-seconds 900 \
  --query WebIdentityToken --output text > "$TOKEN_FILE"

export ANTHROPIC_IDENTITY_TOKEN_FILE="$TOKEN_FILE"
# ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID, and
# ANTHROPIC_SERVICE_ACCOUNT_ID, and ANTHROPIC_WORKSPACE_ID are read from the environment
ant messages create \
  --model claude-sonnet-4-6 \
  --max-tokens 1024 \
  --message '{role: user, content: "Hello from AWS"}'
<?php
require 'vendor/autoload.php';

use Anthropic\Client;
use Anthropic\Credentials\WorkloadIdentityCredentials;
use Aws\Sts\StsClient;

$sts = new StsClient(['region' => 'us-east-1', 'version' => 'latest']);
$client = new Client(credentials: new WorkloadIdentityCredentials(
    identityTokenProvider: fn() => $sts->getWebIdentityToken([
        'Audience' => ['https://api.anthropic.com'],
        'SigningAlgorithm' => 'RS256',
        'DurationSeconds' => 900,
    ])['WebIdentityToken'],
    federationRuleId: getenv('ANTHROPIC_FEDERATION_RULE_ID'),
    organizationId: getenv('ANTHROPIC_ORGANIZATION_ID'),
    serviceAccountId: getenv('ANTHROPIC_SERVICE_ACCOUNT_ID'),
    workspaceId: getenv('ANTHROPIC_WORKSPACE_ID') ?: null,
));

$message = $client->messages->create(
    model: 'claude-sonnet-4-6',
    maxTokens: 1024,
    messages: [['role' => 'user', 'content' => 'Hello from AWS']],
);
echo $message->content[0]->text, PHP_EOL;
require "anthropic"
require "aws-sdk-sts"

sts = Aws::STS::Client.new(region: "us-east-1")
client = Anthropic::Client.new(
  credentials: Anthropic::WorkloadIdentityCredentials.new(
    identity_token_provider: -> {
      sts.get_web_identity_token(
        audience: ["https://api.anthropic.com"],
        signing_algorithm: "RS256",
        duration_seconds: 900,
      ).web_identity_token
    },
    federation_rule_id: ENV.fetch("ANTHROPIC_FEDERATION_RULE_ID"),
    organization_id: ENV.fetch("ANTHROPIC_ORGANIZATION_ID"),
    service_account_id: ENV.fetch("ANTHROPIC_SERVICE_ACCOUNT_ID"),
    workspace_id: ENV["ANTHROPIC_WORKSPACE_ID"],
  ),
)

message = client.messages.create(
  model: "claude-sonnet-4-6",
  max_tokens: 1024,
  messages: [{role: "user", content: "Hello from AWS"}]
)
puts message.content.first.text

验证设置

在工作负载内部,直接交换 STS 签发的令牌并检查响应:

JWT=$(aws sts get-web-identity-token \
  --region us-east-1 \
  --audience "https://api.anthropic.com" \
  --signing-algorithm RS256 \
  --duration-seconds 900 \
  --query WebIdentityToken --output text)

curl -sS https://api.anthropic.com/v1/oauth/token \
  -H "content-type: application/json" \
  -d "{
    \"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_...\"
  }" | jq

成功交换会返回一个以 sk-ant-oat01- 开头的 access_token 和一个以秒为单位的 expires_in 值。如果出现 400 invalid_grant,请参阅排查交换失败;最常见的 AWS 侧原因是 iss 不匹配(每个账户的 STS 签发方 URL 必须与注册的 issuer_url 完全匹配)。

使用 EKS 投影的服务账号令牌

如果你的工作负载运行在 EKS Pod 中,可以跳过 STS 调用,直接从磁盘读取 Kubernetes 投影的服务账号令牌。Kubernetes 原生地将 OIDC 兼容的令牌投影到 Pod 中,SDK 可以从文件路径读取它,因此不需要令牌提供者的可调用对象。此方式比 STS 方式少两个 AWS 配置步骤,但仅适用于 Pod 内部;底层机制与通用 Kubernetes 集成相同。

此方式还需要一个启用了 IAM OIDC 提供者的 EKS 集群和对集群的 kubectl 访问权限。

配置你的 EKS 集群

  1. 查找集群的 OIDC 签发方 URL

    每个 EKS 集群都有一个唯一的 OIDC 签发方。使用 AWS CLI 获取它:

    aws eks describe-cluster \
      --name <cluster-name> \
      --query "cluster.identity.oidc.issuer" \
      --output text
    

    输出形如 https://oidc.eks.us-west-2.amazonaws.com/id/6FA42E7BFDE8549CB...。你将在下一节中将此 URL 注册为联合签发方。

  2. 创建服务账号并投影一个 Anthropic 受众的令牌

    EKS Pod 身份 Webhook 会检测 eks.amazonaws.com/role-arn 注解,并自动投影一个带有 aud: sts.amazonaws.com 的令牌,将其路径暴露为 AWS_WEB_IDENTITY_TOKEN_FILE。该令牌用于 AWS 角色假设。对于 Anthropic 交换,需要投影第二个带有 audience: https://api.anthropic.com 的令牌,并将其挂载到专用路径。

    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: inference-worker
      namespace: inference
      annotations:
        eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/inference-worker
    
    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
    
  3. 注意令牌的声明结构

    投影的令牌是一个由集群 OIDC 签发方签名的 JSON Web Token (JWT)。其 sub 声明遵循 Kubernetes 约定 system:serviceaccount:<namespace>:<service-account-name>

    {
      "iss": "https://oidc.eks.us-west-2.amazonaws.com/id/6FA42E7BFDE8549CB...",
      "sub": "system:serviceaccount:inference:inference-worker",
      "aud": ["https://api.anthropic.com"],
      "kubernetes.io": {
        "namespace": "inference",
        "serviceaccount": { "name": "inference-worker", "uid": "..." }
      },
      "exp": 1775527120,
      "iat": 1775523520
    }
    

    serviceAccountToken 投影将 aud 设置为 https://api.anthropic.com。在 AWS_WEB_IDENTITY_TOKEN_FILE 处单独注入的 IRSA 令牌携带 aud: sts.amazonaws.com,用于 AWS API 调用,而非此交换。

配置 Anthropic

按照设置指南在 Claude Console 中注册联合签发方、创建 Anthropic 服务账号并创建联合规则。使用以下 EKS 特定的值。

联合签发方: EKS 签发方公开了一个公共 JWKS 端点,因此使用发现模式。签发方 URL 必须与令牌的 iss 声明完全匹配。每个集群注册一个签发方。

{
  "name": "prod-eks-uswest2",
  "issuer_url": "https://oidc.eks.us-west-2.amazonaws.com/id/6FA42E7BFDE8549CB...",
  "jwks_source": "discovery"
}

联合规则: 匹配 Kubernetes 的 sub 声明和 Anthropic 受众 https://api.anthropic.com。(投影一个具有该受众的专用服务账号令牌;不要重用 IRSA 默认的 sts.amazonaws.com 令牌。)

{
  "name": "prod-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:*(末尾的 * 使其成为前缀匹配)。

获取并使用令牌

在 Pod 内部,投影的令牌位于 /var/run/secrets/anthropic.com/token(在 Pod 规范中暴露为 ANTHROPIC_IDENTITY_TOKEN_FILE)。将该文件传递给 SDK 的联合凭证,SDK 会处理交换和刷新。

JWT=$(cat "$ANTHROPIC_IDENTITY_TOKEN_FILE")

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": "$ANTHROPIC_FEDERATION_RULE_ID",
  "organization_id": "$ANTHROPIC_ORGANIZATION_ID",
  "service_account_id": "$ANTHROPIC_SERVICE_ACCOUNT_ID",
  "workspace_id": "$ANTHROPIC_WORKSPACE_ID"
}
JSON
)

ACCESS_TOKEN=$(echo "$RESPONSE" | jq -r .access_token)

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 from EKS"}]
  }' | jq -r '.content[0].text'
import os

import anthropic
from anthropic import IdentityTokenFile, WorkloadIdentityCredentials

client = anthropic.Anthropic(
    credentials=WorkloadIdentityCredentials(
        identity_token_provider=IdentityTokenFile(
            os.environ["ANTHROPIC_IDENTITY_TOKEN_FILE"]
        ),
        federation_rule_id=os.environ["ANTHROPIC_FEDERATION_RULE_ID"],
        organization_id=os.environ["ANTHROPIC_ORGANIZATION_ID"],
        service_account_id=os.environ["ANTHROPIC_SERVICE_ACCOUNT_ID"],
        workspace_id=os.environ.get("ANTHROPIC_WORKSPACE_ID"),
    ),
)

message = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello from EKS"}],
)
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(process.env.ANTHROPIC_IDENTITY_TOKEN_FILE!),
    federationRuleId: process.env.ANTHROPIC_FEDERATION_RULE_ID!,
    organizationId: process.env.ANTHROPIC_ORGANIZATION_ID!,
    serviceAccountId: process.env.ANTHROPIC_SERVICE_ACCOUNT_ID,
    workspaceId: process.env.ANTHROPIC_WORKSPACE_ID,
    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 from EKS" }]
});
for (const block of message.content) {
  if (block.type === "text") {
    console.log(block.text);
  }
}
package main

import (
	"context"
	"fmt"
	"os"

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

func main() {
	tokenPath := os.Getenv("ANTHROPIC_IDENTITY_TOKEN_FILE")

	readToken := option.IdentityTokenFunc(func(ctx context.Context) (string, error) {
		raw, err := os.ReadFile(tokenPath)
		if err != nil {
			return "", fmt.Errorf("read identity token: %w", err)
		}
		return string(raw), nil
	})

	client := anthropic.NewClient(
		option.WithFederationTokenProvider(readToken, option.FederationOptions{
			FederationRuleID: os.Getenv("ANTHROPIC_FEDERATION_RULE_ID"),
			OrganizationID:   os.Getenv("ANTHROPIC_ORGANIZATION_ID"),
			ServiceAccountID: os.Getenv("ANTHROPIC_SERVICE_ACCOUNT_ID"),
			WorkspaceID:      os.Getenv("ANTHROPIC_WORKSPACE_ID"),
		}),
	)

	message, err := client.Messages.New(context.TODO(), anthropic.MessageNewParams{
		Model:     anthropic.ModelClaudeSonnet4_6,
		MaxTokens: 1024,
		Messages: []anthropic.MessageParam{
			anthropic.NewUserMessage(anthropic.NewTextBlock("Hello from EKS")),
		},
	})
	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 from EKS")
            .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 from EKS" }],
});
foreach (var block in message.Content)
{
    if (block.Value is TextBlock textBlock)
    {
        Console.WriteLine(textBlock.Text);
    }
}
# Reads ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
# ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, and ANTHROPIC_IDENTITY_TOKEN_FILE
ant messages create \
  --model claude-sonnet-4-6 \
  --max-tokens 1024 \
  --message '{role: user, content: "Hello from EKS"}'
<?php
require 'vendor/autoload.php';

use Anthropic\Client;

// Reads ANTHROPIC_FEDERATION_RULE_ID, ANTHROPIC_ORGANIZATION_ID,
// ANTHROPIC_SERVICE_ACCOUNT_ID, ANTHROPIC_WORKSPACE_ID, and ANTHROPIC_IDENTITY_TOKEN_FILE
$client = new Client();

$message = $client->messages->create(
    model: 'claude-sonnet-4-6',
    maxTokens: 1024,
    messages: [['role' => 'user', 'content' => 'Hello from EKS']],
);
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 from EKS"}]
)
puts message.content.first.text
Tip

Pod 规范已经设置了 ANTHROPIC_IDENTITY_TOKEN_FILEANTHROPIC_FEDERATION_RULE_IDANTHROPIC_ORGANIZATION_IDANTHROPIC_SERVICE_ACCOUNT_IDANTHROPIC_WORKSPACE_ID,因此你可以在不传入任何参数的情况下构造客户端,SDK 会自动读取联合环境变量。

验证设置

在 Pod 内部,直接交换投影的令牌并检查响应:

JWT=$(cat "$ANTHROPIC_IDENTITY_TOKEN_FILE")

curl -sS https://api.anthropic.com/v1/oauth/token \
  -H "content-type: application/json" \
  -d "{
    \"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\"
  }" | jq

成功交换会返回一个以 sk-ant-oat01- 开头的 access_token 和一个以秒为单位的 expires_in 值。如果出现 400 invalid_grant,请参阅排查交换失败;最常见的 EKS 侧原因是投影令牌的 aud 与规则不匹配(投影一个带有 audience: https://api.anthropic.com 的令牌,而不是 IRSA 默认的 sts.amazonaws.com)。

限定规则范围

Warning

subject_prefixarn:aws:iam::123456789012:role/* 会匹配账户中的每个 IAM 角色。任何能够承担匹配角色的主体都可以获取联合 Anthropic 令牌。

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

  • 固定完整的角色 ARN: 使用 subject_prefix: "arn:aws:iam::<account>:role/<role-name>",不带末尾的 *,这样账户中的其他角色不会匹配。
  • 固定账户 ID: 通过 claims 映射或 CEL condition 匹配令牌的 https://sts.amazonaws.com/ 声明中的 aws_account 字段,作为防御前缀配置错误的深度防御检查。
  • 在 EKS 上固定命名空间和服务账号: 使用精确的 system:serviceaccount:<namespace>:<name> 值,在 system:serviceaccount: 前缀后不带 *
  • 为每个环境使用单独的规则: 为生产、预发布和开发工作负载创建不同的规则,而不是放宽一个前缀来覆盖所有环境。

后续步骤