在 GitHub Actions 中使用 WIF
使用短期身份令牌而非长期 API 密钥,将 GitHub Actions 工作流认证到 Claude API。
每个 GitHub Actions 工作流运行都可以从 GitHub 托管的签发方 https://token.actions.githubusercontent.com 请求一个签名的身份令牌。通过 Workload Identity Federation,你的工作流将该令牌交换为短期的 Anthropic 访问令牌,这样你的 CI 任务无需在仓库中存储 ANTHROPIC_API_KEY 密钥即可调用 Claude API。
令牌的 sub 声明编码了仓库和触发上下文。对于推送到分支的操作,其格式为 repo:<owner>/<repo>:ref:refs/heads/<branch>。拉取请求运行使用 repo:<owner>/<repo>:pull_request,环境门控的部署使用 repo:<owner>/<repo>:environment:<name>。你的联合规则匹配此声明(以及其他声明,如 repository_owner 和 ref)来决定允许哪些工作流运行进行认证。
前置条件
- 熟悉 WIF 概念:服务账号、联合签发方和联合规则。
- 一个可以编辑工作流文件并授予
id-token: write权限的 GitHub 仓库。 - 拥有在 Claude Console 中为你的 Anthropic 组织创建服务账号、联合签发方和联合规则的权限。
- 你的 Anthropic 组织 ID。你可以在 Claude Console 的 Settings -> Organization 下找到它。
配置你的工作流
GitHub 仅向明确请求它的作业签发身份令牌。在工作流或作业级别添加 id-token: write 权限:
permissions:
id-token: write
contents: read
在作业内部,运行器暴露两个环境变量:ACTIONS_ID_TOKEN_REQUEST_URL 和 ACTIONS_ID_TOKEN_REQUEST_TOKEN。使用请求令牌作为 Bearer 凭证调用请求 URL,并将你选择的受众作为查询参数,然后将返回的 JSON Web Token (JWT) 写入文件:
- name: Fetch GitHub OIDC token
run: |
curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" \
| jq -r .value > /tmp/gha-jwt
如果你更喜欢 JavaScript,actions/github-script 通过 core.getIDToken(audience) 提供了相同的功能:
- name: Fetch GitHub OIDC token
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const token = await core.getIDToken('https://api.anthropic.com');
fs.writeFileSync('/tmp/gha-jwt', token);
解码后的令牌携带描述工作流运行的声明。你的联合规则匹配这些声明:
{
"iss": "https://token.actions.githubusercontent.com",
"sub": "repo:your-org/your-repo:ref:refs/heads/main",
"aud": "https://api.anthropic.com",
"repository": "your-org/your-repo",
"repository_owner": "your-org",
"ref": "refs/heads/main",
"sha": "abc123...",
"workflow": "CI",
"actor": "octocat",
"event_name": "push"
}
参阅 GitHub 的 OIDC 主体声明参考了解完整的 sub 格式列表。
配置 Anthropic
按照设置指南在 Claude Console 中注册联合签发方、创建 Anthropic 服务账号并创建联合规则。使用以下 GitHub Actions 特定的值。
联合签发方: GitHub 公开发布了其 OIDC 发现文档和 JWKS,因此使用发现模式。Anthropic 会在 GitHub 轮换密钥时自动刷新密钥。
{
"name": "github-actions",
"issuer_url": "https://token.actions.githubusercontent.com",
"jwks_source": "discovery"
}
联合规则: 仅匹配你打算信任的工作流运行。有关如何安全地限定这些声明的范围,请参阅限制哪些工作流可以认证。
{
"name": "gha-main",
"issuer_id": "fdis_...",
"match": {
"subject_prefix": "repo:your-org/your-repo:ref:refs/heads/main",
"audience": "https://api.anthropic.com",
"claims": {
"repository_owner": "your-org"
}
},
"target": {
"type": "service_account",
"service_account_id": "svac_..."
},
"workspace_id": "wrkspc_...",
"oauth_scope": "workspace:developer",
"token_lifetime_seconds": 600
}
尽可能精确地匹配工作负载。只有当规则必须匹配来自同一仓库的多种事件类型时,才将 subject_prefix 放宽为 repo:your-org/your-repo:*(配合 claims.ref 约束),因为 sub 的末尾段在 ref:...、environment:... 和 pull_request 事件之间有所不同。
获取并使用令牌
在作业上设置联合环境变量并正常调用 SDK。Anthropic() 读取 ANTHROPIC_IDENTITY_TOKEN_FILE,在首次请求时交换 JWT,并在过期前自动刷新访问令牌。
name: Call Claude
on: push
permissions:
id-token: write
contents: read
jobs:
call-claude:
runs-on: ubuntu-latest
env:
ANTHROPIC_FEDERATION_RULE_ID: fdrl_...
ANTHROPIC_ORGANIZATION_ID: 00000000-0000-0000-0000-000000000000
ANTHROPIC_SERVICE_ACCOUNT_ID: svac_...
ANTHROPIC_WORKSPACE_ID: wrkspc_... # required when the rule covers multiple workspaces
ANTHROPIC_IDENTITY_TOKEN_FILE: /tmp/gha-jwt
steps:
- uses: actions/checkout@v5
- name: Fetch GitHub OIDC token
run: |
curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=https://api.anthropic.com" \
| jq -r .value > "$ANTHROPIC_IDENTITY_TOKEN_FILE"
- name: Run your script
run: |
pip install anthropic
python your_script.py
JWT=$(cat /tmp/gha-jwt)
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, Claude"}]
}' | jq -r '.content[0].text'
import anthropic
# 从作业环境中读取 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
# ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID 和 ANTHROPIC_IDENTITY_TOKEN_FILE。
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";
// 从作业环境中读取 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
// ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID 和 ANTHROPIC_IDENTITY_TOKEN_FILE。
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() {
// 从作业环境中读取 ANTHROPIC_FEDERATION_RULE_ID、ANTHROPIC_ORGANIZATION_ID、
// ANTHROPIC_SERVICE_ACCOUNT_ID、ANTHROPIC_WORKSPACE_ID 和 ANTHROPIC_IDENTITY_TOKEN_FILE。
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);
}
}
# 从作业环境中读取 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;
// 从作业环境中读取 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"
# 从作业环境中读取 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
每个 GitHub 签发的身份令牌大约在签发五分钟后过期。令牌请求端点(ACTIONS_ID_TOKEN_REQUEST_URL)在整个作业期间保持有效,因此你可以在任何时候获取新令牌。SDK 在首次使用时交换令牌并缓存得到的 Anthropic 访问令牌。对于运行时间超过 Anthropic 令牌生命周期的作业,SDK 在每次刷新时重新读取 ANTHROPIC_IDENTITY_TOKEN_FILE,因此请定期重新运行获取步骤(或将其包装在后台循环中)以保持文件最新。或者,向 SDK 传递一个令牌提供者回调,直接调用 ACTIONS_ID_TOKEN_REQUEST_URL 而不是使用文件路径。
验证设置
成功交换会返回一个以 sk-ant-oat01- 开头的 access_token 和一个以秒为单位的 expires_in 值。如果出现 400 invalid_grant,请参阅排查交换失败;最常见的 GitHub Actions 侧原因是 sub 声明格式不匹配(其末尾段在 ref:...、environment:... 和 pull_request 事件之间有所不同)。
限制哪些工作流可以认证
仅使用 repo:your-org/* 的 subject_prefix 会匹配你组织中的每个仓库,且没有 ref 约束时还会匹配从 fork 触发的 pull_request 运行。任何能向匹配仓库提交拉取请求的人都可以获取联合 Anthropic 令牌。
将规则的 match 块锁定到适合你用例的最窄范围:
- 固定到单个仓库: 使用
subject_prefix: "repo:your-org/your-repo:*",这样组织中的其他仓库不会匹配。 - 固定到受保护的分支: 在
claims下添加"ref": "refs/heads/main"(或你的发布分支),这样拉取请求运行和功能分支不会匹配。 - 显式固定所有者: 在
claims下添加"repository_owner": "your-org",作为防御sub解析边缘情况的深度防御检查。 - 固定到部署环境: 对于部署作业,匹配
subject_prefix: "repo:your-org/your-repo:environment:production",并在 GitHub 中使用必需的审查者来门控该环境。
后续步骤
- Workload Identity Federation:完整的设置指南、环境变量和凭证优先级。
- 认证:联合与 API 密钥的比较。