Despliegas Claude Code para un equipo de plataforma. Configuras la workload identity federation de Anthropic para que cada sesión intercambie una aserción OIDC atestada por un token de corta vida. La revisión de seguridad sale limpia: sin claves estáticas, identidad por sesión, tokens que expiran. A la mañana siguiente, un ingeniero añade export ANTHROPIC_API_KEY=sk-ant-... a su perfil de shell porque un script lo necesita. La federación queda anulada silenciosamente en cada sesión que ese ingeniero ejecuta. Sin error, sin warning, sin entrada de log. La clave estática toma precedencia y la ruta atestada nunca se invoca.
No es una condición de carrera teórica. Es el orden de resolución de credenciales documentado por Anthropic, y es la forma más común en la que los despliegues de federación fallan en silencio.
El problema de las claves estáticas
La forma por defecto de autenticar Claude Code es una API key estática: una cadena sk-ant- configurada como ANTHROPIC_API_KEY. Funciona, y tiene tres propiedades que chocan con la gestión de identidades en entornos corporativos.
Sin expiración, sin señal de rotación. Una clave estática es válida hasta que alguien la revoca. No tiene tiempo de vida integrado, ni recordatorio de rotación, ni mecanismo que fuerce una reautenticación. Una clave emitida durante una prueba de concepto puede seguir autenticando cargas de producción meses después.
Identidad compartida. Cada sesión que usa la misma clave se autentica como el mismo principal. La traza de auditoría muestra qué workspace se usó, pero no puede distinguir qué ingeniero, qué máquina o qué automatización ejecutó una petición concreta. La atribución por sesión es estructuralmente imposible.
Precedencia silenciosa sobre la federación. Este es el footgun. La resolución de credenciales de Anthropic sitúa la clave estática (ANTHROPIC_API_KEY, tier 2) por encima de la ruta de federación (tier 4). Cuando ambas existen en el mismo entorno, la clave estática gana. El intercambio de federación nunca se intenta. No se lanza ningún error. La carga se ejecuta con éxito bajo la identidad de la clave estática, y cada suposición de gobernanza construida sobre la federación — identidad con scope de sesión, aserciones atestadas, expiración de tokens — queda silenciosamente invalidada.
Incluso una variable vacía (ANTHROPIC_API_KEY="") ocupa su posición de precedencia. Fallará la autenticación, pero impedirá que el runtime alcance la ruta de federación. El modo de fallo es “error de autenticación”, no “caer hacia la federación”.
Cómo funciona workload identity federation
WIF sustituye la clave estática por un intercambio: entra una aserción verificada, sale un token de corta vida. La aserción es un JWT — bien un JWT-SVID de un proveedor de identidad SPIFFE, bien un token OIDC estándar de cualquier emisor en el que la organización de Anthropic confíe.
El intercambio sigue RFC 7523 (JWT bearer grant). La carga presenta su aserción al endpoint de tokens de Anthropic junto con tres identificadores: contra qué regla de federación hacer el match (fdrl_), como qué cuenta de servicio actuar (svac_) y a qué organización pertenece el intercambio. En código, el núcleo del intercambio es un POST con un body JSON:
type exchangeRequest struct {
GrantType string `json:"grant_type"` // "urn:ietf:params:oauth:grant-type:jwt-bearer"
Assertion string `json:"assertion"` // el JWT-SVID o token OIDC verificado
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"`
}
La respuesta es un token response RFC 6749. El token emitido lleva el prefijo sk-ant-oat (OAT = OAuth Access Token), un scope declarado (workspace:developer para acceso completo no administrativo a la API, u org:manage_tunnels para gestión de túneles MCP), y un tiempo de vida entre 60 segundos y 24 horas.
No hay refresh token. Cuando el token emitido expira, la carga debe volver a presentar su aserción y ejecutar el intercambio de nuevo. Esto es deliberado: la propia aserción está atestada (verificada aguas arriba por la workload API de SPIFFE o por el proveedor OIDC), de modo que cada re-intercambio es una re-atestación. Un token comprometido solo es útil durante el tiempo de vida que le quede, y no existe una ruta de refresh que un atacante pueda explotar para extenderlo.
El resultado es identidad por sesión. Cada sesión de Claude Code intercambia su propia aserción, recibe su propio token de corta vida y se autentica como un principal diferenciado. La traza de auditoría registra qué cuenta de servicio actuó, vinculada a qué regla de federación, con scope a qué workspace.
Detectar el footgun de clave-estática-anula-federación
Desplegar WIF no es suficiente. También necesitas detectar cuando algo en el entorno lo sortea silenciosamente. El conector inspecciona el entorno de ejecución en busca de la presencia de una credencial estática (ANTHROPIC_API_KEY o ANTHROPIC_AUTH_TOKEN) y comprueba si la federación está simultáneamente en uso (ya sea mediante reglas de federación declaradas o a través de la señal ANTHROPIC_IDENTITY_TOKEN_FILE). Cuando ambas condiciones se cumplen, emite un hallazgo de gobernanza de severidad alta.
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 huellea QUE variable anula — nunca el valor
}, true
}
El hash del hallazgo registra qué variable estática está presente y qué señal de federación anula, sin incrustar jamás el valor de la clave. Ni siquiera una forma enmascarada. El hash es estable entre ejecuciones, de modo que el motor de gobernanza lo deduplica y un SIEM puede consultarlo por caso.
Una clave estática sin federación en uso es simplemente una clave estática — el conector no la señala. El hallazgo solo se dispara cuando ambas existen, porque esa es la configuración concreta en la que el operador cree que la federación está activa y no lo está.
El bucle de reconciliación: declarado vs. real
Detectar el footgun de la clave estática es la mitad del cuadro. La otra mitad es verificar que la propia configuración de federación no ha derivado. El conector mantiene una línea base declarada — las reglas de federación que el operador declara explícitamente como gobernadas — y la compara contra el estado en vivo de la configuración WIF de la organización de Anthropic.
El estado en vivo procede de tres endpoints del WIF Admin API:
GET /v1/organizations/service_accounts— las cuentas de servicio (svac_) a las que apuntan las reglasGET /v1/organizations/federation_issuers— los emisores OIDC/SPIFFE (fdis_) en los que la organización confíaGET /v1/organizations/federation_rules— las reglas (fdrl_) que vinculan emisores a cuentas de servicio
Estos endpoints requieren un bearer token OAuth org:admin — una credencial distinta de la clave sk-ant-admin del Admin API que usan las lecturas del roster. El WIF Admin API rechaza explícitamente las Admin API keys, por lo que el conector usa un cliente autenticado separado para la reconciliación.
El diff entre declarado y en vivo produce siete categorías de hallazgo:
| Caso de drift | Qué significa | Severidad |
|---|---|---|
undeclared_live_rule | Una regla en vivo que el operador nunca declaró ni gobierna | Alta |
declared_rule_not_live | Una regla declarada que ya no existe aguas arriba | Media |
scope_drift | El scope en vivo divergió del scope declarado | Media (Alta si se amplió a org-wide) |
lifetime_drift | El tiempo de vida del token en vivo divergió del declarado | Media (Alta si es más largo que el declarado) |
over_broad_subject | Una regla en vivo sin restricción real de subject | Media |
orphan_rule | Una regla que referencia un emisor o cuenta de servicio inexistente | Media |
orphan_issuer | Un emisor no referenciado por ninguna regla | Baja |
Dos casos escalan a severidad alta automáticamente: un scope en vivo que se amplió a un scope org-wide o admin que el operador no declaró, y un tiempo de vida de token en vivo más largo que la línea base gobernada. Ambos amplían el radio de explosión más allá de lo que el operador aprobó. La normalización de scopes (recortar, deduplicar, ordenar) previene falsos positivos por diferencias de espacios u orden.
Cuando no se configura un token org:admin, la pasada de reconciliación simplemente no se ejecuta. El conector trabaja con la línea base solo-declarada y es honesto sobre su cobertura: nunca fabrica un roster en vivo. Cuando no se puede alcanzar la API en vivo (error de red, token expirado), emite un único hallazgo reconciliation_unavailable y continúa — los grants del roster y la detección del footgun no deben estar acoplados a la salud del token org:admin.
Read-first y datos mínimos
El conector sigue un contrato estricto de minimización de datos. Cada llamada a la API es un GET. Nunca crea, actualiza ni elimina un objeto de Anthropic. Transporta solo metadatos de identidad: IDs, nombres, emails, roles, indicios de clave. Nunca un secreto de clave. Nunca una clave privada. Nunca un token emitido en reposo.
El token emitido por el intercambio WIF se devuelve al llamante y nunca se registra, persiste ni emite por el conector. El único registro que llega al ledger de gobernanza es la estructura ExchangeAudit — que deliberadamente no contiene ningún token:
type ExchangeAudit struct {
FederationRuleID string
OrganizationID string
ServiceAccountID string
WorkspaceID string
Scope string
TokenType string
ExpiresAt time.Time
}
La misma minimización se aplica a la reconciliación en vivo. Cuando el conector lee la configuración JWKS de un emisor de federación, reduce la respuesta a dos booleanos: el modo de descubrimiento (discovery, explicit_url o inline) y si hay un certificado CA personalizado anclado. El material JWK inline — claves públicas, pero voluminosas y nunca necesarias para decisiones de gobernanza — nunca se decodifica en un campo almacenado o emitido. El PEM del certificado CA se decodifica solo para derivar un flag de presencia y se descarta inmediatamente.
Las condiciones CEL de las reglas de federación se transportan para análisis de postura (la expresión CEL de una regla forma parte de su frontera de seguridad), pero nunca se evalúan. El conector no tiene dependencia de motor CEL — la evaluación es una preocupación separada.
Lo que esto posibilita
Las claves estáticas mezclan autenticación con identidad. Cada sesión es el mismo principal, cada token vive para siempre, y la presencia de una clave en un dotfile anula silenciosamente cualquier federación que creías que te estaba protegiendo.
WIF las separa. La autenticación se produce a través de una aserción atestada que prueba que la carga es quien dice ser. La identidad tiene scope de sesión: un token de corta vida, una cuenta de servicio concreta, un workspace concreto, un scope declarado. Cuando el token expira, la carga se re-atesta. Cuando la configuración deriva, el bucle de reconciliación saca la divergencia a la superficie. Cuando una clave estática anula todo el mecanismo, el conector lo detecta y lo reporta antes de que se convierta en un hallazgo de incidente.
El resultado es un modelo de identidad donde las credenciales expiran, las sesiones son atribuibles y las suposiciones de gobernanza se verifican continuamente contra el estado real de la organización — no solo contra el estado que alguien pretendió.
El conector de identidad, el intercambio WIF y la detección del footgun forman parte del módulo de gobernanza de identidad. Para el modelo de seguridad más amplio — el ledger, el mapa de accesos, la arquitectura de recopilación read-first — consulta seguridad.