Skip to main content

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:

  1. Register your Defakto trust domain as a trusted identity provider with Anthropic, so Anthropic can verify JWT-SVIDs issued by your workloads.
  2. Create an Anthropic service account that defines the permissions, workspace access, and rate limits for your workloads.
  3. Create a federation rule that maps a workload's SPIFFE ID to the service account. Only workloads whose identity matches the rule can authenticate.
  4. 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:

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.

  1. Ensure that spirlctl is installed, and use spirlctl login to log in via SSO.
  2. Run spirlctl trust-domain info <TRUST_DOMAIN> to find the JWT Issuer URI for your trust domain:
    $ spirlctl trust-domain info example.com
    Getting Trust Domain Info⠼
    ID td-bpepnha31b
    Name: example.com
    Status: available
    Self-Managed: false
    SPIRL Agent Endpoint: td-bpepnha31b.agent.spirl.com:443
    SPIFFE Bundle Endpoint: https://fed.spirl.org/t-su8rvkjgix/td-bpepnha31b/bundle
    JWT Issuer: https://fed.spirl.org/t-su8rvkjgix/td-bpepnha31b
    JWKS Endpoint: https://fed.spirl.org/t-su8rvkjgix/td-bpepnha31b/jwks
    OIDC Discovery Endpoint: https://fed.spirl.org/t-su8rvkjgix/td-bpepnha31b/.well-known/openid-configuration

    Created At: 2024-09-05 00:41:36.716 +0000 UTC
    Last Updated At: 2025-07-07 15:15:14.718 +0000 UTC
  3. 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.
note

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.

  1. Click Create issuer and fill in the following:

    FieldValue
    NameA descriptive label (e.g. defakto example.com)
    Issuer URLThe JWT Issuer URL from Step 1 (e.g. https://fed.spirl.org/t-su8rvkjgix/td-bpepnha31b)
    JWKS SourceSelect OIDC Discovery
  2. Skip Discovery base URL and CA certificate (PEM). Both are optional.

    Defakto publishes a standards-compliant /.well-known/openid-configuration endpoint; 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.

  3. Enable Enforce single-use tokens (JTI replay protection). This prevents token replay attacks by ensuring each JWT-SVID can only be exchanged once.

    caution

    If 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.

  4. 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 lifetime

    To issue shorter-lived JWT-SVIDs (for example, to reduce the window of exposure), adjust the jwtSVIDTTL setting on your trust domain deployment. See Trust Domain Server Configuration for details on the trustDomainDeployment.jwtSVIDTTL Helm value.

    The Anthropic SDKs automatically re-exchange before the token expires, so shorter lifetimes work seamlessly.

  5. Copy the issuer ID (fdis_...). You will need the issuer ID when creating the federation rule in Step 4.

tip

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.

  1. Click Create service account.
  2. Provide a name (e.g. inference-worker) and optional description.
  3. Add the service account to the workspace(s) it should have access to from that workspace's Members page.
  4. 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.

  1. Click Create rule and configure:

    FieldValue
    NameA descriptive label (e.g. defakto-inference-worker)
    IssuerSelect the issuer you created in Step 2
    Subject patternThe SPIFFE ID of the workload (e.g. spiffe://example.com/ns/inference/sa/worker)
    Expected audiencehttps://api.anthropic.com (recommended — see below)
    Target service accountThe service account from Step 3
    WorkspaceThe workspace the token should be scoped to
    Token lifetimeValidity in seconds for the Anthropic access token (default: 3600)

    Field details

    Subject pattern — This matches the sub claim of the JWT-SVID, which is the workload's SPIFFE ID. Matching uses subject_prefix semantics: an exact value like spiffe://example.com/ns/inference/sa/worker matches only that workload, while a trailing * like spiffe://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 namespace or pod_service_account can 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:

  2. Note the following IDs. Your workload will need them:

    • Federation rule ID (fdrl_...)
    • Organization ID
    • Service account ID (svac_...)
    • Workspace ID (wrkspc_...)
    tip

    The 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 default and not have a wrkspc_ prefix.

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.

tip

For the full SDK reference including all supported languages, see the Anthropic WIF documentation.

warning

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:

Your application connects directly to the Defakto Agent socket via the SPIFFE Workload API to fetch JWT-SVIDs on demand.

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.

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.

  1. Deploy a pod with the k8s.spirl.com/spiffe-csi: enabled label so the admission controller injects the SPIFFE socket:

    claude-test.yaml
    apiVersion: v1
    kind: Pod
    metadata:
    name: claude-test
    namespace: default
    labels:
    k8s.spirl.com/spiffe-csi: "enabled"
    spec:
    restartPolicy: Never
    containers:
    - name: test
    image: golang:1.24
    command: ["sleep", "infinity"]
    env:
    - name: ANTHROPIC_FEDERATION_RULE_ID
    value: "fdrl_..."
    - name: ANTHROPIC_ORGANIZATION_ID
    value: "00000000-0000-0000-0000-000000000000"
    - name: ANTHROPIC_SERVICE_ACCOUNT_ID
    value: "svac_..."
    - name: ANTHROPIC_WORKSPACE_ID
    value: "wrkspc_..."
    kubectl apply -f claude-test.yaml
    kubectl wait --for=condition=Ready pod/claude-test
  2. Exec into the pod and set up the Go module:

    kubectl exec -it claude-test -- bash

    Inside the pod:

    mkdir -p /home/claude-test && cd /home/claude-test
    go mod init example.com/claude-test
  3. Create the test program:

    cat > main.go << 'EOF'
    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()

    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
  4. Download dependencies and run:

    go mod tidy
    go run main.go

    If everything is configured correctly, you should see Claude's response printed to the terminal.

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 codeMeaning
jti_reusedThe JWT-SVID's jti has already been exchanged. Enforce single-use tokens is enabled and the same token was presented twice.
jwt_audience_mismatchThe aud claim does not match the federation rule's expected audience.
jwt_expiredThe JWT-SVID's exp claim is in the past.
jwt_issuer_mismatchThe iss claim in the JWT-SVID does not match any registered issuer URL.
jwt_lifetime_too_longThe JWT-SVID's lifetime exceeds the maximum token lifetime configured on the issuer.
jwt_required_claim_missingA 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:

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

Verify that:

  • iss exactly matches the issuer URL registered in the Claude Console
  • sub is the workload's SPIFFE ID and matches the federation rule's subject pattern
  • aud contains https://api.anthropic.com
  • exp is 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.

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.

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_SOCKET is 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.