Skip to main content

Tutorial: OpenAI Workload Identity Federation

OpenAI's Workload Identity Federation (WIF) allows workloads to authenticate to the OpenAI 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, scoped to a single workload, and expire after a short, defined lifetime. OpenAI access tokens never outlive the subject token used for the exchange.

What you will set up

In this tutorial, you will configure a trust chain that allows your workloads to call the OpenAI API without any static credentials:

  1. Register your Defakto trust domain as a Workload Identity Provider with OpenAI, so OpenAI can verify JWT-SVIDs issued by your workloads.
  2. Create an OpenAI service account that defines the permissions and project access for your workloads.
  3. Create a service account mapping that maps a workload's SPIFFE ID to the service account. Only workloads whose identity matches the mapping can authenticate.
  4. Configure your workload to fetch a JWT-SVID from Defakto and exchange it for a short-lived OpenAI access token, which the SDK uses to call the OpenAI API.

The result is that each workload proves its identity through its SPIFFE ID, OpenAI verifies it against the service account mapping, 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 OpenAI 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 OpenAI.

  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 OpenAI.
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 Workload Identity Provider in the OpenAI Platform

In the OpenAI Platform, navigate to Organization Settings → Security → Workload Identity Provider.

  1. Click Create identity provider and fill in the following:

    FieldValue
    NameA descriptive label (e.g. defakto-example-com)
    OIDC Issuer URLThe JWT Issuer URL from Step 1 (e.g. https://fed.spirl.org/t-su8rvkjgix/td-bpepnha31b)
    Audiencehttps://api.openai.com/v1
  2. Leave Use uploaded JWKS for token verification disabled.

    Defakto publishes a standards-compliant /.well-known/openid-configuration endpoint. OpenAI automatically discovers and fetches the signing keys via OIDC discovery. You do not need to upload a JWKS manually.

  3. Skip Attribute transformations unless you need to derive a mapping value from one or more token claims using CEL expressions. Attribute transformations are not required when mapping directly on the sub claim (the SPIFFE ID).

  4. Copy the Workload Identity Provider ID. You will need it when configuring your workload in Step 4.

tip

For more detail on provider configuration options (uploaded JWKS, attribute transformations, key rotation), see the OpenAI WIF guide.

3. Create a Service Account Mapping

From the Workload Identity Provider details page, create a service account mapping that authorizes your workload's SPIFFE ID to mint tokens for an OpenAI service account.

  1. Click Create mapping and configure:

    FieldValue
    NameA descriptive label (e.g. defakto-inference-worker)
    Keysub
    ValueThe SPIFFE ID of the workload (e.g. spiffe://example.com/ns/inference/sa/worker)
    ProjectThe OpenAI project that owns the target service account
    Service accountThe service account the workload can use (create a new one or select existing)
    PermissionsOptional API permissions to further narrow access (e.g. api.model.request)

    Field details

    Key and Value — The key sub matches the JWT-SVID's sub claim, which is the workload's SPIFFE ID. String values may use one trailing wildcard: an exact value like spiffe://example.com/ns/inference/sa/worker matches only that workload, while spiffe://example.com/ns/inference/sa/* matches all workloads under that path. Be as specific as possible. The wildcard must have a non-empty prefix; * by itself is not supported.

    The SPIFFE ID assigned to each workload is determined by the path template configured for the cluster.

    Audience — The audience is configured on the Workload Identity Provider (Step 2), not on the mapping. OpenAI verifies that the JWT-SVID's aud claim matches the provider's configured audience. You must also configure your workload to request this same audience when fetching the JWT-SVID (shown in Step 4).

    Permissions — Optional API permissions that further narrow access tokens minted from this mapping. These permissions cannot grant access beyond the mapped service account. Leave blank to use the service account's full permissions. Token exchange responses expose restrictions as OAuth scopes in the scope property.

    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 matched via attribute transformations defined on the Workload Identity Provider.

    Multiple trust domains — Each Defakto trust domain has its own signing keys and federation endpoint. Register each as a separate Workload Identity Provider in OpenAI.

    For further details on mapping configuration, see the OpenAI documentation:

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

    • Workload Identity Provider ID
    • Service account ID

4. Configure Your Workload

Your workload exchanges its Defakto-issued JWT-SVID for an OpenAI access token at runtime. The OpenAI SDKs handle the exchange and automatic refresh.

tip

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

warning

If OPENAI_API_KEY is set in your environment, the SDK uses the API key instead of federation. Unset this variable before testing WIF.

Set the following environment variables on your workload. The Kubernetes examples below embed these in the pod spec.

OPENAI_IDENTITY_PROVIDER_ID=<your-provider-id>
OPENAI_SERVICE_ACCOUNT_ID=<your-service-account-id>

Replace the placeholder values above (and in the Kubernetes pod specs below) with the IDs you noted in Steps 2 and 3.

Choose one of the following approaches to provide the JWT-SVID to the OpenAI 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/openai/openai-go/v3"
"github.com/openai/openai-go/v3/auth"
"github.com/openai/openai-go/v3/option"
"github.com/openai/openai-go/v3/responses"
"github.com/spiffe/go-spiffe/v2/svid/jwtsvid"
"github.com/spiffe/go-spiffe/v2/workloadapi"
)

const audience = "https://api.openai.com/v1"

// spiffeTokenProvider implements auth.SubjectTokenProvider using the
// SPIFFE Workload API to fetch fresh JWT-SVIDs on demand.
type spiffeTokenProvider struct {
source *workloadapi.JWTSource
}

func (p *spiffeTokenProvider) TokenType() auth.SubjectTokenType {
return auth.SubjectTokenTypeJWT
}

func (p *spiffeTokenProvider) GetToken(ctx context.Context, _ auth.HTTPDoer) (string, error) {
svid, err := p.source.FetchJWTSVID(ctx, jwtsvid.Params{Audience: audience})
if err != nil {
return "", fmt.Errorf("failed to fetch JWT-SVID: %w", err)
}
return svid.Marshal(), nil
}

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()

client := openai.NewClient(
option.WithWorkloadIdentity(auth.WorkloadIdentity{
IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),
ServiceAccountID: os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),
Provider: &spiffeTokenProvider{source: source},
}),
)

resp, err := client.Responses.New(ctx, responses.ResponseNewParams{
Model: openai.ChatModelGPT4_1Mini,
Input: responses.ResponseNewParamsInputUnion{
OfString: openai.String("Hello, OpenAI"),
},
})
if err != nil {
log.Fatalf("API call failed: %v", err)
}

fmt.Println(resp.OutputText())
}

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:

    openai-test.yaml
    apiVersion: v1
    kind: Pod
    metadata:
    name: openai-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: OPENAI_IDENTITY_PROVIDER_ID
    value: "<your-provider-id>"
    - name: OPENAI_SERVICE_ACCOUNT_ID
    value: "<your-service-account-id>"
    kubectl apply -f openai-test.yaml
    kubectl wait --for=condition=Ready pod/openai-test
  2. Exec into the pod and set up the Go module:

    kubectl exec -it openai-test -- bash

    Inside the pod:

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

    cat > main.go << 'EOF'
    package main

    import (
    "context"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/openai/openai-go/v3"
    "github.com/openai/openai-go/v3/auth"
    "github.com/openai/openai-go/v3/option"
    "github.com/openai/openai-go/v3/responses"
    "github.com/spiffe/go-spiffe/v2/svid/jwtsvid"
    "github.com/spiffe/go-spiffe/v2/workloadapi"
    )

    const audience = "https://api.openai.com/v1"

    type spiffeTokenProvider struct {
    source *workloadapi.JWTSource
    }

    func (p *spiffeTokenProvider) TokenType() auth.SubjectTokenType {
    return auth.SubjectTokenTypeJWT
    }

    func (p *spiffeTokenProvider) GetToken(ctx context.Context, _ auth.HTTPDoer) (string, error) {
    svid, err := p.source.FetchJWTSVID(ctx, jwtsvid.Params{Audience: audience})
    if err != nil {
    return "", fmt.Errorf("failed to fetch JWT-SVID: %w", err)
    }
    return svid.Marshal(), nil
    }

    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()

    client := openai.NewClient(
    option.WithWorkloadIdentity(auth.WorkloadIdentity{
    IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),
    ServiceAccountID: os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),
    Provider: &spiffeTokenProvider{source: source},
    }),
    )

    resp, err := client.Responses.New(ctx, responses.ResponseNewParams{
    Model: openai.ChatModelGPT4_1Mini,
    Input: responses.ResponseNewParamsInputUnion{
    OfString: openai.String("Hello, OpenAI"),
    },
    })
    if err != nil {
    log.Fatalf("API call failed: %v", err)
    }

    fmt.Println(resp.OutputText())
    }
    EOF
  4. Download dependencies and run:

    go mod tidy
    go run main.go

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

5. Troubleshooting

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.openai.com/v1 --json | jq

Verify that:

  • iss exactly matches the issuer URL registered in the OpenAI Platform
  • sub is the workload's SPIFFE ID and matches the service account mapping's sub value
  • aud contains https://api.openai.com/v1
  • exp is in the future
  • The JWT header includes a kid field (required by OpenAI)

Common Errors

The token exchange endpoint returns a generic HTTP 500 error for most configuration problems without indicating the specific cause. Because of this, you cannot rely on the error message alone to diagnose the issue. Instead, decode the JWT-SVID (see above) and verify each field against your OpenAI configuration:

CheckWhat to verify
IssuerThe iss claim exactly matches the OIDC Issuer URL on the Workload Identity Provider.
AudienceThe aud claim contains the audience configured on the Workload Identity Provider (default: https://api.openai.com/v1).
ExpiryThe exp claim is in the future.
SignatureThe kid in the JWT header exists in the JWKS served by your trust domain's JWKS endpoint.
MappingThe sub claim matches the Value field of an enabled service account mapping for the requested service account.

Token Exchange Request

If you need to manually test the token exchange, use the OpenAI token endpoint directly:

curl https://auth.openai.com/oauth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"subject_token": "'"$JWT_SVID"'",
"identity_provider_id": "'"$OPENAI_IDENTITY_PROVIDER_ID"'",
"service_account_id": "'"$OPENAI_SERVICE_ACCOUNT_ID"'"
}'

A successful response returns a short-lived bearer token:

{
"access_token": "eyJ...",
"issued_token_type": "urn:ietf:params:oauth:token-type:access_token",
"token_type": "Bearer",
"expires_in": 3600
}

For the full list of error codes and resolution steps, see the OpenAI WIF reference and the SPIFFE setup guide.