Skip to content

WIF

Ephemeral credentials for Claude Code with workload identity federation

By Olivares AI 8 min read

You deploy Claude Code for a platform team. You configure Anthropic’s workload identity federation so every session exchanges an attested OIDC assertion for a short-lived token. The security review is clean: no static keys, per-session identity, tokens that expire. The next morning, one engineer adds export ANTHROPIC_API_KEY=sk-ant-... to their shell profile because a script needs it. Federation is now silently bypassed for every session that engineer runs. No error, no warning, no log entry. The static key takes precedence and the attested path is never invoked.

This is not a theoretical race condition. It is Anthropic’s documented credential resolution order, and it is the single most common way federation deployments fail quietly.

The problem with static keys

The default way to authenticate Claude Code is a static API key: an sk-ant- string set as ANTHROPIC_API_KEY. It works, and it has three properties that conflict with enterprise identity management.

No expiry, no rotation signal. A static key is valid until someone revokes it. There is no built-in lifetime, no rotation reminder, no mechanism that forces re-authentication. A key issued during a proof-of-concept can still authenticate production workloads months later.

Shared identity. Every session using the same key authenticates as the same principal. The audit trail shows which workspace was used, but it cannot distinguish which engineer, which machine, or which automation ran a given request. Per-session attribution is structurally impossible.

Silent precedence over federation. This is the footgun. Anthropic’s credential resolution places the static key (ANTHROPIC_API_KEY, tier 2) above the federation path (tier 4). When both exist in the same environment, the static key wins. The federation exchange is never attempted. No error is raised. The workload runs successfully under the static key’s identity, and every governance assumption built on federation — session-scoped identity, attested assertions, token expiry — is silently invalidated.

Even an empty variable (ANTHROPIC_API_KEY="") occupies its precedence slot. It will fail authentication, but it will still prevent the runtime from reaching the federation path. The failure mode is “authentication error”, not “falling through to federation”.

How workload identity federation works

WIF replaces the static key with an exchange: a verified assertion goes in, a short-lived token comes out. The assertion is a JWT — either a JWT-SVID from a SPIFFE identity provider, or a standard OIDC token from any issuer the Anthropic organization trusts.

The exchange follows RFC 7523 (JWT bearer grant). The workload presents its assertion to Anthropic’s token endpoint along with three identifiers: which federation rule to match against (fdrl_), which service account to act as (svac_), and which organization the exchange belongs to. In code, the core of the exchange is a POST with a JSON body:

type exchangeRequest struct {
    GrantType        string `json:"grant_type"`         // "urn:ietf:params:oauth:grant-type:jwt-bearer"
    Assertion        string `json:"assertion"`          // the verified JWT-SVID or OIDC token
    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"`
}

The response is an RFC 6749 token response. The minted token carries the sk-ant-oat prefix (OAT = OAuth Access Token), a declared scope (workspace:developer for full non-administrative API access, or org:manage_tunnels for MCP tunnel management), and a lifetime between 60 seconds and 24 hours.

There is no refresh token. When the minted token expires, the workload must re-present its assertion and run the exchange again. This is deliberate: the assertion itself is attested (verified upstream by the SPIFFE workload API or the OIDC provider), so every re-exchange is a re-attestation. A compromised token is useful only for its remaining lifetime, and there is no refresh path an attacker can exploit to extend it.

The result is per-session identity. Each Claude Code session exchanges its own assertion, receives its own short-lived token, and authenticates as a distinct principal. The audit trail records which service account acted, bound to which federation rule, scoped to which workspace.

Detecting the static-key-shadows-federation footgun

Deploying WIF is not enough. You also need to detect when something in the environment silently bypasses it. The connector inspects the runtime environment for the presence of a static credential (ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN) and checks whether federation is simultaneously in use (either through declared federation rules or through the ANTHROPIC_IDENTITY_TOKEN_FILE signal). When both conditions are true, it emits a high-severity governance finding.

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 fingerprints WHICH variable shadows — never the value
    }, true
}

The finding’s detail hash records which static variable is present and which federation signal it shadows, without ever embedding the key’s value. Not even a masked form. The hash is stable across runs, so the governance engine de-duplicates it and a SIEM can query it by case.

A static key with no federation in use is just a static key — the connector does not flag it. The finding fires only when both exist, because that is the specific configuration where the operator believes federation is active and it is not.

The reconciliation loop: declared vs. actual

Detecting the static-key footgun is half the picture. The other half is verifying that the federation configuration itself has not drifted. The connector maintains a declared baseline — the federation rules the operator explicitly declares as governed — and compares it against the live state of the Anthropic organization’s WIF configuration.

The live state comes from three WIF Admin API endpoints:

  • GET /v1/organizations/service_accounts — the service accounts (svac_) that federation rules target
  • GET /v1/organizations/federation_issuers — the OIDC/SPIFFE issuers (fdis_) the organization trusts
  • GET /v1/organizations/federation_rules — the rules (fdrl_) that bind issuers to service accounts

These endpoints require an org:admin OAuth bearer token — a distinct credential from the sk-ant-admin Admin API key that the roster reads use. The WIF Admin API explicitly rejects Admin API keys, which is why the connector uses a separate authenticated client for reconciliation.

The diff between declared and live produces seven categories of finding:

Drift caseWhat it meansSeverity
undeclared_live_ruleA live rule the operator never declared or governsHigh
declared_rule_not_liveA declared rule that no longer exists upstreamMedium
scope_driftThe live scope diverged from the declared scopeMedium (High if broadened to org-wide)
lifetime_driftThe live token lifetime diverged from declaredMedium (High if longer than declared)
over_broad_subjectA live rule with no real subject constraintMedium
orphan_ruleA rule referencing a missing issuer or service accountMedium
orphan_issuerAn issuer referenced by no ruleLow

Two cases escalate to high severity automatically: a live scope that broadened to an org-wide or admin scope the operator did not declare, and a live token lifetime that is longer than the governed baseline. Both widen the blast radius beyond what the operator signed off on. Scope normalization (trim, deduplicate, sort) prevents false positives from whitespace or ordering differences.

When no org:admin token is configured, the reconciliation pass simply does not run. The connector works with the declared-only baseline and is honest about its coverage: it never fabricates a live roster. When the live API cannot be reached (network error, token expiry), it emits a single reconciliation_unavailable finding and continues — the roster grants and the footgun detection must not be coupled to the org:admin token’s health.

Read-first and minimal data

The connector follows a strict data-minimisation contract. Every API call is a GET. It never creates, updates, or deletes an Anthropic object. It carries identity metadata only: IDs, names, emails, roles, key hints. Never a key secret. Never a private key. Never a minted token at rest.

The minted token from the WIF exchange is returned to the caller and is never logged, persisted, or emitted by the connector. The only record that reaches the governance ledger is the ExchangeAudit struct — which deliberately carries no token:

type ExchangeAudit struct {
    FederationRuleID string
    OrganizationID   string
    ServiceAccountID string
    WorkspaceID      string
    Scope            string
    TokenType        string
    ExpiresAt        time.Time
}

The same minimisation applies to the live reconciliation. When the connector reads a federation issuer’s JWKS configuration, it reduces the response to two booleans: the discovery mode (discovery, explicit_url, or inline) and whether a custom CA certificate is pinned. The inline JWK material — public keys, but bulky and never needed for governance decisions — is never decoded into a stored or emitted field. The CA certificate PEM is decoded only to derive a presence flag and is immediately discarded.

CEL conditions on federation rules are carried for posture analysis (a rule’s CEL expression is part of its security boundary), but they are never evaluated. The connector has no CEL engine dependency — evaluation is a separate concern.

What this enables

Static keys conflate authentication with identity. Every session is the same principal, every token lives forever, and the presence of a key in a dotfile silently disables any federation you thought was protecting you.

WIF separates the two. Authentication happens through an attested assertion that proves the workload is who it claims. Identity is scoped to the session: a short-lived token, a specific service account, a specific workspace, a declared scope. When the token expires, the workload re-attests. When the configuration drifts, the reconciliation loop surfaces the gap. When a static key shadows the entire mechanism, the connector detects it and reports it before it becomes an incident finding.

The result is an identity model where credentials expire, sessions are attributable, and governance assumptions are continuously verified against the actual state of the organization — not just the state someone intended.


The identity connector, the WIF exchange, and the footgun detection are part of the identity governance module. For the broader security model — the ledger, the access map, the read-first collection architecture — see security.

Related posts

Frequently asked

Does the static key have to contain a valid API key to shadow federation?

No. Anthropic's credential precedence is based on presence, not validity. An empty ANTHROPIC_API_KEY="" wins its precedence slot just as a populated key would: it sits above the federation tiers in the resolution order, so the runtime never falls through to the attested path. The connector detects this by checking whether the environment variable is set (os.LookupEnv), not whether its value is non-empty. An empty key will fail authentication, but it will shadow federation just as thoroughly as a real one.

What happens if I do not have an org:admin OAuth token for live reconciliation?

The connector works without it. It models exactly what the operator declares in the federation configuration, emits the governed NHI roster and the permitted-grant edges from those declarations, and runs the static-key footgun detection. It simply skips the live reconciliation pass, because the WIF Admin API endpoints reject anything other than an org:admin OAuth bearer token. You get a declared-only baseline that is honest about its coverage: it never fabricates a live roster.

See what your agents can reach

Olivares AI is the open, self-hosted platform for your AI estate. Deploy it on your own infrastructure and get the access map your security and platform teams have been asking for.