Tutorial: Anthropic Claude Federation
Anthropic's Workload Identity Federation (WIF) allows workloads to authenticate to the Claude API using short-lived identity tokens from an external identity provider instead of static API keys.
Static API keys act like a password. Anyone who has one can access a resource indefinitely until the key is explicitly revoked. Workload Identity Federation removes this risk entirely: Credentials are issued on demand and scoped to a single workload, with a defined short expiry time. Anthropic also supports JTI replay protection, so a JWT-SVID can only be used once to gain access to Claude.
What you will set up
In this tutorial, you will configure a trust chain that allows your workloads to call the Claude API without any static credentials:
- Register your Defakto trust domain as a trusted identity provider with Anthropic, so Anthropic can verify JWT-SVIDs issued by your workloads.
- Create an Anthropic service account that defines the permissions, workspace access, and rate limits for your workloads.
- Create a federation rule that maps a workload's SPIFFE ID to the service account. Only workloads whose identity matches the rule can authenticate.
- Configure your workload to fetch a JWT-SVID from Defakto and exchange it for a short-lived Anthropic access token, which the SDK uses to call the Claude API.
The result is that each workload proves its identity through its SPIFFE ID, Anthropic verifies it against the federation rule, and grants access as the mapped service account. There are no API keys to distribute, rotate, or revoke.
Defakto-issued JWT-SVIDs are a natural fit for this model. Every Defakto trust domain includes a publicly reachable OIDC Discovery endpoint, so Anthropic can automatically discover and validate JWT-SVIDs issued to your workloads.
Preconditions:
- Active SPIFFE trust domain
- Access to view trust domain settings via one of:
- A runtime environment for workloads via one of:
- A cluster with the Defakto Agent installed
- A local machine using Developer Identity
- Anthropic organization with admin access to the Claude Console
1. Determine the OIDC Discovery Endpoint
Defakto automatically publishes an OIDC Discovery document for all trust domains. You need the JWT Issuer URL to register with Anthropic.
- CLI
- Console
- Ensure that
spirlctlis installed, and usespirlctl loginto log in via SSO. - Run
spirlctl trust-domain info <TRUST_DOMAIN>to find the JWT Issuer URI for your trust domain:$ spirlctl trust-domain info example.comGetting Trust Domain Info⠼ID td-bpepnha31bName: example.comStatus: availableSelf-Managed: falseSPIRL Agent Endpoint: td-bpepnha31b.agent.spirl.com:443SPIFFE Bundle Endpoint: https://fed.spirl.org/t-su8rvkjgix/td-bpepnha31b/bundleJWT Issuer: https://fed.spirl.org/t-su8rvkjgix/td-bpepnha31bJWKS Endpoint: https://fed.spirl.org/t-su8rvkjgix/td-bpepnha31b/jwksOIDC Discovery Endpoint: https://fed.spirl.org/t-su8rvkjgix/td-bpepnha31b/.well-known/openid-configurationCreated At: 2024-09-05 00:41:36.716 +0000 UTCLast Updated At: 2025-07-07 15:15:14.718 +0000 UTC - Copy the JWT Issuer URI (e.g.
https://fed.spirl.org/t-su8rvkjgix/td-bpepnha31b). This is the issuer URL you will register with Anthropic.
- Log in to the Defakto Console.
- Navigate to your trust domain, then go to Settings → Trust domain URLs.
- Copy the JWT Issuer URL that is displayed.

You can verify the contents of the discovery endpoint by fetching the well-known configuration document:
$ curl -s https://fed.spirl.org/t-su8rvkjgix/td-bpepnha31b/.well-known/openid-configuration | jq
{
"issuer": "https://fed.spirl.org/t-su8rvkjgix/td-bpepnha31b",
"jwks_uri": "https://fed.spirl.org/t-su8rvkjgix/td-bpepnha31b/jwks",
"authorization_endpoint": "",
"response_types_supported": [
"id_token"
],
"subject_types_supported": [],
"id_token_signing_alg_values_supported": [
"RS256",
"ES256"
]
}
2. Register a Federation Issuer in the Claude Console
In the Claude Console, navigate to Settings → Workload identity and open the Issuers tab.
-
Click Create issuer and fill in the following:
Field Value Name A descriptive label (e.g. defakto example.com)Issuer URL The JWT Issuer URL from Step 1 (e.g. https://fed.spirl.org/t-su8rvkjgix/td-bpepnha31b)JWKS Source Select OIDC Discovery -
Skip Discovery base URL and CA certificate (PEM). Both are optional.
Defakto publishes a standards-compliant
/.well-known/openid-configurationendpoint; No inline JWKS or explicit JWKS URL is needed. Anthropic automatically discovers and fetches the signing keys.The CA certificate is not required and we recommend leaving it blank. Defakto federation endpoints use publicly trusted HTTPS certificates, and the issuing CA may change during routine certificate rotation.
-
Enable Enforce single-use tokens (JTI replay protection). This prevents token replay attacks by ensuring each JWT-SVID can only be exchanged once.
cautionIf you plan to use Developer Identity or SPIRL Bridge for testing, leave this option disabled.
Developer Identity authenticates the user once and serves the same JWT-SVID for subsequent requests rather than minting a fresh token each time, so single-use enforcement will reject the repeated token. Once the token expires, re-authenticate with the identity provider to get a new one.
SPIRL Bridge writes a JWT-SVID to a file on a rotation schedule, but the workload may re-read and exchange the same token multiple times before Bridge writes a new one. Neither Bridge nor the workload has a way to know the token has already been used and request a new one early, so the exchange fails silently with
jti_reused.You can enable this setting later when you move to production with the Workload API.
-
Set the Maximum token lifetime to 24 hours to match the default JWT-SVID lifetime issued by Defakto.
This value must match or exceed the JWT-SVID lifetime configured on your trust domain. Defakto issues JWT-SVIDs with a 24-hour lifetime by default. If the issuer's maximum token lifetime is shorter than the JWT-SVID lifetime, Anthropic rejects the token with error
jwt_lifetime_too_long.Adjusting JWT-SVID lifetimeTo issue shorter-lived JWT-SVIDs (for example, to reduce the window of exposure), adjust the
jwtSVIDTTLsetting on your trust domain deployment. See Trust Domain Server Configuration for details on thetrustDomainDeployment.jwtSVIDTTLHelm value.The Anthropic SDKs automatically re-exchange before the token expires, so shorter lifetimes work seamlessly.
-
Copy the issuer ID (
fdis_...). You will need the issuer ID when creating the federation rule in Step 4.
For more detail on issuer configuration options (explicit JWKS URL, inline keys, CA certificates), see the Anthropic setup walkthrough.
3. Create an Anthropic Service Account
In the Claude Console, go to Settings → Service accounts.
- Click Create service account.
- Provide a name (e.g.
inference-worker) and optional description. - Add the service account to the workspace(s) it should have access to from that workspace's Members page.
- Note the service account ID (
svac_...).
The service account is the identity that your workloads will acquire when authenticating with your federated tokens. The service account follows the workspace's rate limits and usage attribution.
4. Create a Federation Rule
In the Claude Console, go to Settings → Workload identity, open the Federation rules tab.
-
Click Create rule and configure:
Field Value Name A descriptive label (e.g. defakto-inference-worker)Issuer Select the issuer you created in Step 2 Subject pattern The SPIFFE ID of the workload (e.g. spiffe://example.com/ns/inference/sa/worker)Expected audience https://api.anthropic.com(recommended — see below)Target service account The service account from Step 3 Workspace The workspace the token should be scoped to Token lifetime Validity in seconds for the Anthropic access token (default: 3600) Field details
Subject pattern — This matches the
subclaim of the JWT-SVID, which is the workload's SPIFFE ID. Matching usessubject_prefixsemantics: an exact value likespiffe://example.com/ns/inference/sa/workermatches only that workload, while a trailing*likespiffe://example.com/ns/inference/*matches all workloads under that path. Be as specific as possible.Expected audience — Although optional in the Claude Console, we recommend always setting this to
https://api.anthropic.com. This ensures only JWT-SVIDs explicitly requested for the Anthropic audience are accepted, preventing SVIDs issued for other relying parties from being used. You must also configure your workload to request this same audience when fetching the JWT-SVID (shown in Step 5).Additional claims — For more granular matching beyond the SPIFFE ID, you can add custom claims to your JWT-SVIDs using the JWT-SVID Customization feature. Custom claims such as
namespaceorpod_service_accountcan then be used in the federation rule's claim matchers or CEL conditions.Multiple trust domains — Each Defakto trust domain has its own signing keys and federation endpoint. Register each as a separate issuer in the Claude Console.
For further details on these fields, see the Anthropic documentation:
- Scoping your rule — best practices for
subject_prefixand audience matching - Advanced match options — CEL conditions and exact claim matching
- Token lifetime and refresh — how token lifetimes interact with the SDK refresh loop
- Scoping your rule — best practices for
-
Note the following IDs. Your workload will need them:
- Federation rule ID (
fdrl_...) - Organization ID
- Service account ID (
svac_...) - Workspace ID (
wrkspc_...)
tipThe above IDs can be found in the Claude Console at platform.claude.com. The workspace ID can be found in the URL when viewing a workspace.
Example: https://platform.claude.com/settings/workspaces/wrkspc_111111111111111111/keys. When using the Default workspace, the workspace ID will be
defaultand not have a wrkspc_ prefix. - Federation rule ID (
5. Configure Your Workload
Your workload exchanges its Defakto-issued JWT-SVID for an Anthropic access token at runtime. The Anthropic SDKs handle the exchange and automatic refresh.
For the full SDK reference including all supported languages, see the Anthropic WIF documentation.
If ANTHROPIC_API_KEY or ANTHROPIC_AUTH_TOKEN is set in your environment,
the SDK uses the API key instead of federation. Unset these variables before
testing WIF. See
Credential precedence
for the full resolution order.
Set the following environment variables on your workload. The Kubernetes examples below embed these in the pod spec.
ANTHROPIC_FEDERATION_RULE_ID=fdrl_...
ANTHROPIC_ORGANIZATION_ID=00000000-0000-0000-0000-000000000000
ANTHROPIC_SERVICE_ACCOUNT_ID=svac_...
ANTHROPIC_WORKSPACE_ID=wrkspc_...
Replace the placeholder values above (and in the Kubernetes pod specs below) with the IDs you noted in Steps 3 and 4.
Choose one of the following approaches to provide the JWT-SVID to the Anthropic SDK:
- Agent (Workload API)
- Agent (SPIRL Bridge)
- Developer Identity
Your application connects directly to the Defakto Agent socket via the SPIFFE Workload API to fetch JWT-SVIDs on demand.
- Go
- Python
Use go-spiffe to fetch JWT-SVIDs
from the Defakto Agent socket:
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/anthropics/anthropic-sdk-go"
"github.com/anthropics/anthropic-sdk-go/option"
"github.com/spiffe/go-spiffe/v2/svid/jwtsvid"
"github.com/spiffe/go-spiffe/v2/workloadapi"
)
const audience = "https://api.anthropic.com"
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// Connect to the Defakto Agent socket via the SPIFFE Workload API.
// Uses the SPIFFE_ENDPOINT_SOCKET env var to locate the Agent socket.
source, err := workloadapi.NewJWTSource(ctx)
if err != nil {
log.Fatalf("failed to create JWT source: %v", err)
}
defer source.Close()
fetchJWTSVID := func(ctx context.Context) (string, error) {
svid, err := source.FetchJWTSVID(ctx, jwtsvid.Params{Audience: audience})
if err != nil {
return "", fmt.Errorf("failed to fetch JWT-SVID: %w", err)
}
return svid.Marshal(), nil
}
client := anthropic.NewClient(
option.WithFederationTokenProvider(fetchJWTSVID, 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, Claude")),
},
})
if err != nil {
log.Fatalf("API call failed: %v", err)
}
fmt.Println(message.Content[0].Text)
}
Ensure SPIFFE_ENDPOINT_SOCKET is set to the Defakto Agent socket address. When using Kubernetes webhooks to inject the socket, SPIFFE_ENDPOINT_SOCKET is set automatically.
Use py-spiffe to fetch
JWT-SVIDs from the Defakto Agent socket:
import os
import anthropic
from anthropic import WorkloadIdentityCredentials
from spiffe import JwtSource
AUDIENCE = "https://api.anthropic.com"
# Connects to the Defakto Agent socket at SPIFFE_ENDPOINT_SOCKET.
jwt_source = JwtSource()
def fetch_jwt_svid() -> str:
svid = jwt_source.fetch_svid(audience={AUDIENCE})
return svid.token
client = anthropic.Anthropic(
credentials=WorkloadIdentityCredentials(
identity_token_provider=fetch_jwt_svid,
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, Claude"}],
)
print(message.content[0].text)
Kubernetes Quickstart
If you already have the Defakto Agent running on your Kubernetes cluster, you can quickly test the integration by launching a pod with the Workload API socket injected and running a script inside it.
- Go
- Python
-
Deploy a pod with the
k8s.spirl.com/spiffe-csi: enabledlabel so the admission controller injects the SPIFFE socket:claude-test.yamlapiVersion: v1kind: Podmetadata:name: claude-testnamespace: defaultlabels:k8s.spirl.com/spiffe-csi: "enabled"spec:restartPolicy: Nevercontainers:- name: testimage: golang:1.24command: ["sleep", "infinity"]env:- name: ANTHROPIC_FEDERATION_RULE_IDvalue: "fdrl_..."- name: ANTHROPIC_ORGANIZATION_IDvalue: "00000000-0000-0000-0000-000000000000"- name: ANTHROPIC_SERVICE_ACCOUNT_IDvalue: "svac_..."- name: ANTHROPIC_WORKSPACE_IDvalue: "wrkspc_..."kubectl apply -f claude-test.yamlkubectl wait --for=condition=Ready pod/claude-test -
Exec into the pod and set up the Go module:
kubectl exec -it claude-test -- bashInside the pod:
mkdir -p /home/claude-test && cd /home/claude-testgo mod init example.com/claude-test -
Create the test program:
cat > main.go << 'EOF'package mainimport ("context""fmt""log""os""time""github.com/anthropics/anthropic-sdk-go""github.com/anthropics/anthropic-sdk-go/option""github.com/spiffe/go-spiffe/v2/svid/jwtsvid""github.com/spiffe/go-spiffe/v2/workloadapi")const audience = "https://api.anthropic.com"func main() {ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)defer cancel()source, err := workloadapi.NewJWTSource(ctx)if err != nil {log.Fatalf("failed to create JWT source: %v", err)}defer source.Close()fetchJWTSVID := func(ctx context.Context) (string, error) {svid, err := source.FetchJWTSVID(ctx, jwtsvid.Params{Audience: audience})if err != nil {return "", fmt.Errorf("failed to fetch JWT-SVID: %w", err)}return svid.Marshal(), nil}client := anthropic.NewClient(option.WithFederationTokenProvider(fetchJWTSVID, 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, Claude")),},})if err != nil {log.Fatalf("API call failed: %v", err)}fmt.Println(message.Content[0].Text)}EOF -
Download dependencies and run:
go mod tidygo run main.goIf everything is configured correctly, you should see Claude's response printed to the terminal.
-
Deploy a pod with the
k8s.spirl.com/spiffe-csi: enabledlabel so the admission controller injects the SPIFFE socket:claude-test.yamlapiVersion: v1kind: Podmetadata:name: claude-testnamespace: defaultlabels:k8s.spirl.com/spiffe-csi: "enabled"spec:restartPolicy: Nevercontainers:- name: testimage: python:3.12-slimcommand: ["sleep", "infinity"]env:- name: ANTHROPIC_FEDERATION_RULE_IDvalue: "fdrl_..."- name: ANTHROPIC_ORGANIZATION_IDvalue: "00000000-0000-0000-0000-000000000000"- name: ANTHROPIC_SERVICE_ACCOUNT_IDvalue: "svac_..."- name: ANTHROPIC_WORKSPACE_IDvalue: "wrkspc_..."kubectl apply -f claude-test.yamlkubectl wait --for=condition=Ready pod/claude-test -
Exec into the pod and install dependencies:
kubectl exec -it claude-test -- bashInside the pod:
pip install anthropic spiffe spiffe-tls -
Create the test program and run it:
cat > test_claude.py << 'EOF'import osimport anthropicfrom anthropic import WorkloadIdentityCredentialsfrom spiffe import JwtSourceAUDIENCE = "https://api.anthropic.com"jwt_source = JwtSource()def fetch_jwt_svid() -> str:svid = jwt_source.fetch_svid(audience={AUDIENCE})return svid.tokenclient = anthropic.Anthropic(credentials=WorkloadIdentityCredentials(identity_token_provider=fetch_jwt_svid,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, Claude"}],)print(message.content[0].text)EOFpython test_claude.pyIf everything is configured correctly, you should see Claude's response printed to the terminal.
Use SPIRL Bridge as a sidecar to maintain an active JWT-SVID in a file. The Anthropic SDK reads the token from this file on each exchange and your application does not need to embed a SPIFFE library.
When ANTHROPIC_IDENTITY_TOKEN_FILE is set, the Anthropic SDKs automatically
read the JWT-SVID from the file and handle token exchange and refresh.
SPIRL Bridge writes tokens to a file on a rotation schedule. The Bridge does not know when the SDK reads or exchanges a token. Because of this, Enforce single-use tokens (JTI replay protection) must be disabled on your Anthropic federation rule when using SPIRL Bridge. Several scenarios can cause the SDK to re-read and re-exchange a token that has already been used:
- Application crash and restart — On Kubernetes, only the crashed container
restarts; the Bridge sidecar continues running and does not mint a fresh
token. The restarted application reads the same file and the exchange fails
with
jti_reused. - Multiple SDK client instances — If more than one process or client initialization reads the same token file, each instance exchanges the same JWT-SVID independently.
- Anthropic access token refresh — The SDK caches the minted Anthropic access token (default lifetime 3600 s) and re-reads the file to perform a new exchange when it expires. Because Bridge rotates based on the JWT-SVID lifetime (default 24 h), the file may still contain the same JWT-SVID across multiple refresh cycles.
Because tokens are pre-fetched rather than issued on demand, the Defakto audit logs will show JWT-SVIDs that were issued and written to disk but never exchanged with Anthropic.
If your workload requires single-use token enforcement, use the Workload API approach instead, which fetches a fresh JWT-SVID for each exchange.
- Go
- Python
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/anthropics/anthropic-sdk-go"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// The SDK reads ANTHROPIC_IDENTITY_TOKEN_FILE and the federation
// env vars automatically — no explicit credentials needed.
client := anthropic.NewClient()
message, err := client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.ModelClaudeSonnet4_6,
MaxTokens: 1024,
Messages: []anthropic.MessageParam{
anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")),
},
})
if err != nil {
log.Fatalf("API call failed: %v", err)
}
fmt.Println(message.Content[0].Text)
}
import anthropic
# The SDK reads ANTHROPIC_IDENTITY_TOKEN_FILE and the federation
# env vars automatically — no explicit credentials needed.
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)
Standalone
Run SPIRL Bridge to write a JWT-SVID to a file, then set the environment variables. See SPIRL Bridge installation for setup instructions.
spirl-bridge \
--jwt-audience https://api.anthropic.com \
--jwt-token-path /var/run/secrets/anthropic.com/token
export ANTHROPIC_IDENTITY_TOKEN_FILE=/var/run/secrets/anthropic.com/token
export ANTHROPIC_FEDERATION_RULE_ID=fdrl_...
export ANTHROPIC_ORGANIZATION_ID=00000000-0000-0000-0000-000000000000
export ANTHROPIC_SERVICE_ACCOUNT_ID=svac_...
export ANTHROPIC_WORKSPACE_ID=wrkspc_...
Then run your application as normal.
Kubernetes Quick-Start
Use pod annotations to inject the SPIRL Bridge sidecar automatically. The Defakto Controller injects the sidecar container and creates the shared volume mount.
The Anthropic SDK re-reads ANTHROPIC_IDENTITY_TOKEN_FILE on every token
exchange, so it transparently picks up rotated tokens written by SPIRL Bridge.
- Go
- Python
-
Deploy a pod with SPIRL Bridge annotations:
claude-test.yamlapiVersion: v1kind: Podmetadata:name: claude-testnamespace: defaultlabels:k8s.spirl.com/spiffe-csi: "enabled"annotations:bridge.spirl.com/inject: "*"bridge.spirl.com/jwt-audience: "https://api.anthropic.com"bridge.spirl.com/jwt-token-path: "/var/run/secrets/anthropic.com/token"spec:restartPolicy: Nevercontainers:- name: testimage: golang:1.24command: ["sleep", "infinity"]env:- name: ANTHROPIC_IDENTITY_TOKEN_FILEvalue: "/var/run/secrets/anthropic.com/token"- name: ANTHROPIC_FEDERATION_RULE_IDvalue: "fdrl_..."- name: ANTHROPIC_ORGANIZATION_IDvalue: "00000000-0000-0000-0000-000000000000"- name: ANTHROPIC_SERVICE_ACCOUNT_IDvalue: "svac_..."- name: ANTHROPIC_WORKSPACE_IDvalue: "wrkspc_..."kubectl apply -f claude-test.yamlkubectl wait --for=condition=Ready pod/claude-test -
Exec into the pod and set up the Go module:
kubectl exec -it claude-test -c test -- bashInside the pod:
mkdir -p /home/claude-test && cd /home/claude-testgo mod init example.com/claude-test -
Create the test program:
cat > main.go << 'EOF'package mainimport ("context""fmt""log""time""github.com/anthropics/anthropic-sdk-go")func main() {ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)defer cancel()client := anthropic.NewClient()message, err := client.Messages.New(ctx, anthropic.MessageNewParams{Model: anthropic.ModelClaudeSonnet4_6,MaxTokens: 1024,Messages: []anthropic.MessageParam{anthropic.NewUserMessage(anthropic.NewTextBlock("Hello, Claude")),},})if err != nil {log.Fatalf("API call failed: %v", err)}fmt.Println(message.Content[0].Text)}EOF -
Download dependencies and run:
go mod tidygo run main.goIf everything is configured correctly, you should see Claude's response printed to the terminal.
-
Deploy a pod with SPIRL Bridge annotations:
claude-test.yamlapiVersion: v1kind: Podmetadata:name: claude-testnamespace: defaultlabels:k8s.spirl.com/spiffe-csi: "enabled"annotations:bridge.spirl.com/inject: "*"bridge.spirl.com/jwt-audience: "https://api.anthropic.com"bridge.spirl.com/jwt-token-path: "/var/run/secrets/anthropic.com/token"spec:restartPolicy: Nevercontainers:- name: testimage: python:3.12-slimcommand: ["sleep", "infinity"]env:- name: ANTHROPIC_IDENTITY_TOKEN_FILEvalue: "/var/run/secrets/anthropic.com/token"- name: ANTHROPIC_FEDERATION_RULE_IDvalue: "fdrl_..."- name: ANTHROPIC_ORGANIZATION_IDvalue: "00000000-0000-0000-0000-000000000000"- name: ANTHROPIC_SERVICE_ACCOUNT_IDvalue: "svac_..."- name: ANTHROPIC_WORKSPACE_IDvalue: "wrkspc_..."kubectl apply -f claude-test.yamlkubectl wait --for=condition=Ready pod/claude-test -
Exec into the pod and install the Anthropic SDK:
kubectl exec -it claude-test -c test -- bashInside the pod:
pip install anthropic -
Create the test program and run it:
cat > test_claude.py << 'EOF'import anthropicclient = 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)EOFpython test_claude.pyIf everything is configured correctly, you should see Claude's response printed to the terminal.
Developer Identity lets you test the Claude WIF integration
from your local machine without a running cluster. It issues JWT-SVIDs scoped to
your developer identity (e.g. spiffe://example.com/users/acme.com/alice).
The SPIFFE ID for Developer Identity tokens follows the pattern
spiffe://<trust-domain>/users/<email.domain>/<email.username>. Your Anthropic
federation rule's subject pattern must match this path (or use a wildcard like
spiffe://example.com/users/*).
Developer Identity authenticates the user once and continues to serve the same JWT-SVID rather than minting a fresh token for each request. Because of this, Enforce single-use tokens (JTI replay protection) must be disabled on your Anthropic federation rule when testing with Developer Identity.
- Workload API
- File-based
Use spirlctl dev-id serve to expose a SPIFFE Workload API locally. The Go or
Python examples from the Workload API tab work without modification. Point
SPIFFE_ENDPOINT_SOCKET at the dev-id socket:
spirlctl dev-id serve \
--trust-domain example.com \
--jwt \
--audience https://api.anthropic.com
The command opens a browser for OIDC login, then serves the Workload API at the default socket path. In a separate terminal:
export SPIFFE_ENDPOINT_SOCKET=unix:/tmp/spirl/devid/workload.sock
export ANTHROPIC_FEDERATION_RULE_ID=fdrl_...
export ANTHROPIC_ORGANIZATION_ID=00000000-0000-0000-0000-000000000000
export ANTHROPIC_SERVICE_ACCOUNT_ID=svac_...
export ANTHROPIC_WORKSPACE_ID=wrkspc_...
Then run your application as normal.
Fetch a single JWT-SVID and point the Anthropic SDK at the file:
spirlctl dev-id fetch jwt-svid \
--trust-domain example.com \
--audience https://api.anthropic.com
export ANTHROPIC_IDENTITY_TOKEN_FILE=$HOME/.spirl/dev-id/example.com/svids/jwt-svid
export ANTHROPIC_FEDERATION_RULE_ID=fdrl_...
export ANTHROPIC_ORGANIZATION_ID=00000000-0000-0000-0000-000000000000
export ANTHROPIC_SERVICE_ACCOUNT_ID=svac_...
export ANTHROPIC_WORKSPACE_ID=wrkspc_...
Then run your application as normal. The file-based Go and Python examples from the SPIRL Bridge tab apply here as well.
The fetched token has a fixed lifetime and is not automatically refreshed. Re-run the command to get a fresh token before the previous one expires.
See the Developer Identity setup guide for prerequisites and administrator configuration steps.
6. Troubleshooting
Check Authentication Events
The most effective way to diagnose federation failures is the Authentication events view in the Claude Console. Navigate to Settings → Workload identity → Authentication events to see a log of every token exchange attempt. Each event includes a result code that pinpoints the failure reason, such as:
| Result code | Meaning |
|---|---|
jti_reused | The JWT-SVID's jti has already been exchanged. Enforce single-use tokens is enabled and the same token was presented twice. |
jwt_audience_mismatch | The aud claim does not match the federation rule's expected audience. |
jwt_expired | The JWT-SVID's exp claim is in the past. |
jwt_issuer_mismatch | The iss claim in the JWT-SVID does not match any registered issuer URL. |
jwt_lifetime_too_long | The JWT-SVID's lifetime exceeds the maximum token lifetime configured on the issuer. |
jwt_required_claim_missing | A required claim (e.g. sub, aud) is missing from the JWT-SVID. |
Inspect a JWT-SVID
Decode the JWT-SVID to check its claims. How you obtain the token depends on which approach you are using:
- Workload API
- SPIRL Bridge / File-based
If your workload connects to the Defakto Agent socket, use
spirldbg to fetch and decode a JWT-SVID directly:
spirldbg svid-jwt --audience https://api.anthropic.com --json | jq
If you are using SPIRL Bridge or Developer Identity file-based mode, the token is already on disk:
- SPIRL Bridge (Kubernetes):
/var/run/secrets/anthropic.com/token - Developer Identity file-based:
$HOME/.spirl/dev-id/example.com/svids/jwt-svid
Decode it in place:
cat /path/to/token | jq -rR 'split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson'
Verify that:
issexactly matches the issuer URL registered in the Claude Consolesubis the workload's SPIFFE ID and matches the federation rule's subject patternaudcontainshttps://api.anthropic.comexpis in the future
Common Errors
The SDKs raise an error when the token exchange fails. The error message
includes a request_id you can cross-reference with the authentication events
log in the Claude Console to find the specific result code.
- Go
- Python
API call failed: failed to get credentials token: oauth token request failed
(status 401); request id req_...;
{"error":{"message":"Authentication failed","type":"authentication_error"}}.
Ensure your federation rule matches your identity token.
View your authentication events in the Workload identity page of Claude Console for more details.
WorkloadIdentityError: Token exchange failed (HTTP 401):
{'error': {'type': 'authentication_error', 'message': 'Authentication failed'}}
Ensure your federation rule matches your identity token.
View your authentication events in the Workload identity page of Claude Console for more details. [request_id=req_...]
The error includes a request_id (e.g. req_...) and an HTTP status code.
Look for:
- HTTP 401 with
authentication_error— The token exchange was rejected. Check the Authentication events view in the Claude Console (see above) to find the specific result code for this request. - Timeout or hang — The workload cannot reach the Defakto Agent socket.
Verify that
SPIFFE_ENDPOINT_SOCKETis set correctly and the Agent is running.
For the full list of error codes and resolution steps, see the Anthropic WIF reference and Troubleshoot a failed exchange.