你为平台团队部署了 Claude Code。你配置了 Anthropic 的工作负载身份联合,使每个会话都可以将经过验证的 OIDC 断言交换为短期令牌。安全审查结果良好:没有静态密钥,每个会话都有身份,令牌会过期。第二天早上,一名工程师将 export ANTHROPIC_API_KEY=sk-ant-... 添加到他们的 shell 配置文件中,因为一个脚本需要它。现在,这名工程师运行的每个会话都会悄无声息地绕过联合。没有错误,没有警告,也没有日志记录。静态密钥优先,经过验证的路径从未被调用。
这不是一个理论上的竞争条件。这是 Anthropic 的文档化凭证解析顺序,也是联合部署安静失败的最常见方式。
静态密钥的问题
默认的 Claude Code 认证方式是静态 API 密钥:一个 sk-ant- 字符串设置为 ANTHROPIC_API_KEY。它可以工作,但有三个特性与企业身份管理冲突。
无过期,无轮换信号。 静态密钥有效直到有人撤销它。没有内置的有效期、没有轮换提醒、没有强制重新认证的机制。在概念验证期间发放的密钥几个月后仍然可以认证生产工作负载。
共享身份。 每个使用相同密钥的会话都以相同身份进行认证。审计记录显示使用了哪个工作区,但无法区分哪个工程师、哪台机器或哪个自动化程序执行了某个请求。会话级归属从结构上来说是不可能的。
在联合身份之上默默优先。 这是一个踩雷点。Anthropic 的凭证解析将静态密钥(ANTHROPIC_API_KEY,等级 2)置于联合路径(等级 4)之上。当两者都存在于同一环境中时,静态密钥胜出。联合交换根本不会尝试。不会产生错误。工作负载在静态密钥的身份下成功运行,而建立在联合之上的每个治理假设——会话范围的身份、可验证的声明、令牌过期——都被默默地作废。
即使是空变量(ANTHROPIC_API_KEY="")也会占据其优先位置。它将认证失败,但仍会阻止运行时访问联合路径。失败模式是“认证错误”,而不是“回退到联合”。
工作负载身份联合是如何工作的
WIF 用兑换取代静态密钥:一个已验证的断言输入,产生一个短期令牌。该断言是一个 JWT —— 来自 SPIFFE 身份提供者的 JWT-SVID,或来自 Anthropic 组织信任的任何发行者的标准 OIDC 令牌。
该兑换遵循 RFC 7523(JWT 承载令牌授权)。工作负载将其断言提交到 Anthropic 的令牌端点,同时提供三个标识符:要匹配的联盟规则(fdrl_)、要扮演的服务账户(svac_)以及兑换所属的组织。在代码中,兑换的核心是一个带有 JSON 请求体的 POST:
type exchangeRequest struct {
GrantType string `json:"grant_type"` // "urn:ietf:params:oauth:grant-type:jwt-bearer"
Assertion string `json:"assertion"` // 已验证的 JWT-SVID 或 OIDC 令牌
FederationRuleID string `json:"federation_rule_id"` // fdrl_...
OrganizationID string `json:"organization_id"`
ServiceAccountID string `json:"service_account_id"` // svac_...
WorkspaceID string `json:"workspace_id,omitempty"`
}
响应是一个 RFC 6749 令牌响应。生成的令牌带有 sk-ant-oat 前缀(OAT = OAuth 访问令牌)、声明的作用域(workspace:developer 用于完全非管理 API 访问,或 org:manage_tunnels 用于 MCP 隧道管理)以及 60 秒到 24 小时之间的有效期。
没有刷新令牌。当生成的令牌过期时,工作负载必须重新提交其断言并再次运行交换。这是故意为之:断言本身已被证明(由 SPIFFE 工作负载 API 或 OIDC 提供者在上游验证),因此每次重新交换都是重新认证。被泄露的令牌仅在其剩余有效期内有用,并且没有攻击者可以利用的刷新路径来延长其有效期。
结果是每会话身份。每个 Claude Code 会话交换其自己的断言,接收其自己的短期令牌,并以不同的主体进行认证。审计日志记录了哪个服务账户执行了操作,绑定到哪个联合规则,并限定到哪个工作区。
检测 static-key-shadows-federation 隐患
仅部署 WIF 不够。您还需要检测环境中何时有东西悄悄绕过它。连接器会检查运行时环境中是否存在静态凭证(ANTHROPIC_API_KEY 或 ANTHROPIC_AUTH_TOKEN),并检查是否同时使用联合(通过已声明的联合规则或 ANTHROPIC_IDENTITY_TOKEN_FILE 信号)。当两个条件都满足时,它会发出高严重性治理发现。
func (s *Source) detectShadowing(at time.Time) (model.FindingReport, bool) {
_, hasKey := s.envLookup(envAPIKey)
_, hasAuth := s.envLookup(envAuthToken)
if !hasKey && !hasAuth {
return model.FindingReport{}, false
}
_, hasTokenFile := s.envLookup(envIdentityTokenFile)
federationInUse := len(s.federation) > 0 || hasTokenFile
if !federationInUse {
return model.FindingReport{}, false
}
// ...
return model.FindingReport{
Kind: "governance",
Severity: model.SeverityHigh,
Title: "Static Anthropic key shadows Workload Identity Federation",
// DetailHash 标识哪个变量具有优先级——从不记录值
}, true
}
发现的 detail hash 记录了哪个静态变量存在以及它遮蔽了哪个联合信号,但从不嵌入密钥的值。甚至不会以掩码形式出现。该 hash 在多次运行中是稳定的,因此治理引擎可以去重,而 SIEM 可以按案例查询它。
未使用联合的静态密钥只是一个静态密钥——连接器不会标记它。只有当两者都存在时,发现才会触发,因为这是操作员认为联合已激活但实际上未激活的特定配置。
对账循环:声明的 vs. 实际的
检测静态密钥脚枪只是问题的一半。另一半是验证联盟配置本身没有发生漂移。连接器保持一个声明的基线——操作员明确声明受管理的联盟规则——并将其与 Anthropic 组织的 WIF 配置的实时状态进行比较。
实时状态来自三个 WIF 管理 API 端点:
GET /v1/organizations/service_accounts—— 联盟规则针对的服务账户 (svac_)GET /v1/organizations/federation_issuers—— 组织信任的 OIDC/SPIFFE 发行者 (fdis_)GET /v1/organizations/federation_rules—— 将发行者绑定到服务账户的规则 (fdrl_)
这些端点需要 org:admin OAuth 承载令牌 —— 与名册读取使用的 sk-ant-admin 管理 API 密钥不同。WIF 管理 API 明确拒绝管理 API 密钥,这就是连接器使用单独认证客户端进行对账的原因。
声明与实际之间的差异产生七类发现:
| 偏差情况 | 含义 | 严重性 |
|---|---|---|
undeclared_live_rule | 操作员从未声明或管理的实际规则 | 高 |
declared_rule_not_live | 声明的规则在上游已不存在 | 中等 |
scope_drift | 实际范围与声明的范围不一致 | 中等(如果扩展到整个组织则为高) |
lifetime_drift | 实际令牌的有效期与声明的有效期不一致 | 中等(如果比声明的时间更长则为高) |
over_broad_subject | 没有实际主体约束的实时规则 | 中等 |
orphan_rule | 引用缺失的发行者或服务账户的规则 | 中等 |
orphan_issuer | 没有规则引用的发行者 | 低 |
有两种情况会自动升级为高严重性:一是实时范围扩展到组织范围或管理员范围,而操作员未声明;二是实时令牌的有效期超过了受控基线。两者都会扩大影响范围,超出操作员批准的范围。范围规范化(修剪、去重、排序)可以防止由空格或顺序差异引起的误报。
当未配置 org:admin 令牌时,对账过程将根本不会运行。连接器仅使用声明的基线,并且对其覆盖情况保持诚实:它从不伪造实时名册。当无法访问实时 API(网络错误、令牌过期)时,会发出单个 reconciliation_unavailable 发现,并继续运行——名册权限和自伤检测不得与 org:admin 令牌的状态挂钩。
先读数据和最小化数据
该连接器遵循严格的数据最小化协议。每个 API 调用都是 GET。它从不创建、更新或删除 Anthropic 对象。它只携带身份元数据:ID、姓名、电子邮件、角色、密钥提示。绝不会包含密钥秘密。绝不会包含私钥。绝不会在静态状态下包含已铸造的令牌。
来自 WIF 交换的已铸造令牌会返回给调用者,连接器不会记录、持久化或发出它。唯一到达治理账本的记录是 ExchangeAudit 结构——它刻意不携带任何令牌:
type ExchangeAudit struct {
FederationRuleID string
OrganizationID string
ServiceAccountID string
WorkspaceID string
Scope string
TokenType string
ExpiresAt time.Time
}
相同的最小化原则也适用于实时对账。当连接器读取联合发行者的 JWKS 配置时,它将响应简化为两个布尔值:发现模式(discovery、explicit_url 或 inline)以及是否绑定了自定义 CA 证书。内联 JWK 材料——公钥,但体积大且从未用于治理决策——从未被解码成存储或发出的字段。CA 证书 PEM 只被解码以生成存在标志,并立即被丢弃。
联合规则的 CEL 条件用于姿态分析(规则的 CEL 表达式是其安全边界的一部分),但从未被评估。连接器不依赖 CEL 引擎——评估是一个独立的问题。
这使得
静态密钥将身份验证与身份混为一谈。每个会话都是相同的主体,每个令牌都是永久存在的,并且在点文件中存在密钥会默默地禁用你以为在保护你的任何联合认证。
WIF 分隔了两者。身份验证是通过经认证的声明来进行的,该声明证明工作负载是其声称的身份。身份被限定在会话范围内:一个短期有效的令牌、一个特定的服务账号、一个特定的工作区、一个声明的范围。当令牌过期时,工作负载会重新认证。当配置发生漂移时,对账循环会显现 gap。当静态密钥覆盖整个机制时,连接器会检测到它并在其成为事件发现之前报告。
结果是一个身份模型,其中凭证会过期,会话可追溯,并且治理假设会持续根据组织的实际状态进行验证——而不仅仅是某人所期望的状态。
身份连接器、WIF 交换以及脚本错误检测是身份治理模块的一部分。有关更广泛的安全模型——账本、访问地图、先读集合架构——请参见安全。