Repository Public
1# Configuring workload identity federation for SPIFFE23> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.45Use SPIFFE as a Workload Identity Provider by exchanging a SPIFFE JWT-SVID for a short-lived OpenAI access token. This lets workloads authenticated by SPIRE or another SPIFFE-compatible identity provider call the OpenAI API without storing long-lived API keys.67OpenAI supports SPIFFE JWT-SVIDs that can be validated as JWT subject tokens with an issuer, audience, expiration, issued-at timestamp, and JWKS-backed signature. OpenAI doesn't support SPIFFE X.509-SVIDs as workload identity federation subject tokens.89The JWT-SVID specification requires the `sub`, `aud`, and `exp` claims. To use a JWT-SVID with OpenAI, the token must also include `iss` and `iat` claims and a `kid` header so OpenAI can validate the token against the Workload Identity Provider configuration.1011A JWT-SVID is not an OpenID Connect ID token. The SPIRE OIDC Discovery Provider supplies discovery metadata and JWKS keys so OpenAI can validate the JWT-SVID; it doesn't change the token's SPIFFE semantics or require an OIDC login flow.1213For SPIFFE terminology and token requirements, see the SPIFFE [JWT-SVID specification](https://spiffe.io/docs/latest/spiffe-specs/jwt-svid/) and [Workload API specification](https://spiffe.io/docs/latest/spiffe-specs/spiffe_workload_api/).1415## Setting up SPIFFE1617Configure your SPIFFE provider to issue JWT-SVIDs for workloads that need to call the OpenAI API. These instructions use SPIRE terminology, but the same OpenAI configuration applies to any SPIFFE-compatible provider that emits JWT-SVIDs with issuer and JWKS signing material that OpenAI can validate.1819Your SPIFFE setup must provide:2021- A stable SPIFFE ID for the workload, such as `spiffe://example.org/ns/production/sa/openai-wif`.22- A single JWT-SVID audience dedicated to OpenAI access, such as `https://api.openai.com/v1` or another opaque value you choose.23- A JWT issuer URL that appears in the JWT-SVID `iss` claim for OpenAI validation.24- A public JWKS for the JWT-SVID signing keys, either through OIDC discovery or an uploaded JWKS.25- A workload-side way to fetch fresh JWT-SVIDs from the SPIFFE Workload API.2627The audience is an exact-match identifier, not necessarily an endpoint that receives the JWT-SVID. You may use `https://api.openai.com/v1` or another service-specific value as long as the SPIFFE Workload API request and OpenAI provider configuration match.2829When possible, expose the SPIFFE issuer through your SPIRE OIDC Discovery Provider. Configure the SPIRE Server `jwt_issuer` and the OIDC Discovery Provider `jwt_issuer` to the same HTTPS issuer URL that you will configure in OpenAI.3031In the SPIRE Server configuration:3233```hcl34server {35 trust_domain = "example.org"36 jwt_issuer = "https://spire-oidc.example.org"37}38```3940In the separate SPIRE OIDC Discovery Provider configuration:4142```hcl43# Relevant issuer fields only44domains = ["spire-oidc.example.org"]45jwt_issuer = "https://spire-oidc.example.org"46```4748The OIDC Discovery Provider configuration also needs a key-material source, such as `server_api`, `workload_api`, or `file`, and a serving mechanism, such as ACME, a TLS certificate, or a Unix socket. See the [SPIRE OIDC Discovery Provider documentation](https://github.com/spiffe/spire/tree/main/support/oidc-discovery-provider) for the complete configuration options.4950The SPIFFE trust domain and JWT issuer are different concepts. In this example, the JWT-SVID subject is a SPIFFE ID in the `example.org` trust domain, while the issuer is the HTTPS issuer URL:5152```json53{54 "sub": "spiffe://example.org/ns/production/sa/openai-wif",55 "iss": "https://spire-oidc.example.org"56}57```5859The SPIRE OIDC Discovery Provider serves an OIDC discovery document and a JWKS endpoint that OpenAI can use when **Use uploaded JWKS for token verification** is disabled.6061If OpenAI can't reach your issuer discovery endpoint, use uploaded JWKS mode instead. In that mode, OpenAI still compares the Workload Identity Provider issuer with the JWT-SVID `iss` claim, but verifies signatures against the JWKS JSON you save on the Workload Identity Provider.6263> **Note:** The SPIFFE JWT-SVID specification makes the JWT header `kid` optional, but OpenAI requires JWT subject tokens to include a `kid` header so it can select the signing key from the configured JWKS. If your SPIFFE provider can omit `kid`, configure it to include one for OpenAI workload identity federation.6465To inspect a JWT-SVID from a workload that can call the SPIFFE Workload API, request one for the same audience you will configure in OpenAI. Run this command in the same workload context as the application, because Workload API authorization depends on the identity of the calling process.6667```bash68TOKEN=$(spire-agent api fetch jwt \69 -socketPath /run/spire/sockets/agent.sock \70 -audience "https://api.openai.com/v1" | sed -n '2p')71export TOKEN72```7374If your workload has more than one SPIFFE ID, request the specific identity:7576```bash77TOKEN=$(spire-agent api fetch jwt \78 -socketPath /run/spire/sockets/agent.sock \79 -spiffeID "spiffe://example.org/ns/production/sa/openai-wif" \80 -audience "https://api.openai.com/v1" | sed -n '2p')81export TOKEN82```8384## Verify the token8586Before configuring workload identity federation, export the JWT-SVID as `TOKEN`, then run one of these examples locally to inspect its header and claims:8788```javascript89const parts = process.env.TOKEN?.split(".") ?? [];90if (parts.length !== 3) {91 throw new Error("Expected a compact JWT with three segments");92}9394const decode = (segment) => {95 if (!/^[A-Za-z0-9_-]+$/.test(segment) || segment.length % 4 === 1) {96 throw new Error("JWT segment is not valid Base64URL");97 }98 const bytes = Buffer.from(segment, "base64url");99 if (bytes.toString("base64url") !== segment) {100 throw new Error("JWT segment is not valid Base64URL");101 }102 const decoded = new TextDecoder("utf-8", { fatal: true }).decode(bytes);103 const value = JSON.parse(decoded);104 if (value === null || Array.isArray(value) || typeof value !== "object") {105 throw new Error("JWT segment is not a JSON object");106 }107 return decoded;108};109110console.log("Header:");111console.log(decode(parts[0]));112console.log("\nPayload:");113console.log(decode(parts[1]));114```115116```python117import base64118import json119import os120import re121122123def reject_non_json_constant(value):124 raise ValueError(f"JWT segment contains non-JSON constant: {value}")125126127parts = os.environ.get("TOKEN", "").split(".")128if len(parts) != 3:129 raise ValueError("Expected a compact JWT with three segments")130131132def decode(segment):133 if re.fullmatch(r"[A-Za-z0-9_-]+", segment) is None or len(segment) % 4 == 1:134 raise ValueError("JWT segment is not valid Base64URL")135 padded_segment = segment + "=" * (-len(segment) % 4)136 decoded = base64.b64decode(padded_segment, altchars=b"-_", validate=True)137 if base64.urlsafe_b64encode(decoded).rstrip(b"=").decode("ascii") != segment:138 raise ValueError("JWT segment is not valid Base64URL")139 decoded_text = decoded.decode("utf-8")140 value = json.loads(decoded_text, parse_constant=reject_non_json_constant)141 if not isinstance(value, dict):142 raise ValueError("JWT segment is not a JSON object")143 return decoded_text144145146print("Header:")147print(decode(parts[0]))148print("\nPayload:")149print(decode(parts[1]))150```151152```go153package main154155import (156 "bytes"157 "encoding/base64"158 "encoding/json"159 "fmt"160 "os"161 "strings"162 "unicode/utf8"163)164165func decodeSegment(segment string) (json.RawMessage, error) {166 if !isBase64URLSegment(segment) {167 return nil, fmt.Errorf("JWT segment is not valid Base64URL")168 }169 decoded, err := base64.RawURLEncoding.DecodeString(segment)170 if err != nil {171 return nil, err172 }173 if base64.RawURLEncoding.EncodeToString(decoded) != segment {174 return nil, fmt.Errorf("JWT segment is not valid Base64URL")175 }176 if !utf8.Valid(decoded) {177 return nil, fmt.Errorf("JWT segment is not valid UTF-8")178 }179180 var value json.RawMessage181 if err := json.Unmarshal(decoded, &value); err != nil {182 return nil, err183 }184 if trimmed := bytes.TrimSpace(value); len(trimmed) == 0 || trimmed[0] != '{' {185 return nil, fmt.Errorf("JWT segment is not a JSON object")186 }187 return value, nil188}189190func isBase64URLSegment(segment string) bool {191 if segment == "" || len(segment)%4 == 1 {192 return false193 }194 for _, character := range segment {195 if !('A' <= character && character <= 'Z') &&196 !('a' <= character && character <= 'z') &&197 !('0' <= character && character <= '9') &&198 character != '-' &&199 character != '_' {200 return false201 }202 }203 return true204}205206func printJSON(label string, value json.RawMessage) error {207 formatted, err := json.MarshalIndent(value, "", " ")208 if err != nil {209 return err210 }211 fmt.Printf("%s:\n%s\n", label, formatted)212 return nil213}214215func main() {216 parts := strings.Split(os.Getenv("TOKEN"), ".")217 if len(parts) != 3 {218 panic("Expected a compact JWT with three segments")219 }220221 header, err := decodeSegment(parts[0])222 if err != nil {223 panic(err)224 }225 payload, err := decodeSegment(parts[1])226 if err != nil {227 panic(err)228 }229 if err := printJSON("Header", header); err != nil {230 panic(err)231 }232 fmt.Println()233 if err := printJSON("Payload", payload); err != nil {234 panic(err)235 }236}237```238239```java240// Add Jackson (com.fasterxml.jackson.core:jackson-databind) to your project.241import com.fasterxml.jackson.databind.DeserializationFeature;242import com.fasterxml.jackson.databind.JsonNode;243import com.fasterxml.jackson.databind.ObjectMapper;244import java.io.IOException;245import java.nio.ByteBuffer;246import java.nio.charset.CharacterCodingException;247import java.nio.charset.CodingErrorAction;248import java.nio.charset.StandardCharsets;249import java.util.Base64;250251public final class DecodeJwtExample {252 private static final ObjectMapper JSON =253 new ObjectMapper().enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);254255 private DecodeJwtExample() {}256257 static String decodeUtf8(byte[] bytes) throws IOException {258 try {259 return StandardCharsets.UTF_8260 .newDecoder()261 .onMalformedInput(CodingErrorAction.REPORT)262 .onUnmappableCharacter(CodingErrorAction.REPORT)263 .decode(ByteBuffer.wrap(bytes))264 .toString();265 } catch (CharacterCodingException exception) {266 throw new IOException("JWT segment is not valid UTF-8", exception);267 }268 }269270 static String decodeSegment(String segment) throws IOException {271 if (!isBase64UrlSegment(segment)) {272 throw new IllegalArgumentException("JWT segment is not valid Base64URL");273 }274 byte[] bytes = Base64.getUrlDecoder().decode(segment);275 if (!Base64.getUrlEncoder().withoutPadding().encodeToString(bytes).equals(segment)) {276 throw new IllegalArgumentException("JWT segment is not valid Base64URL");277 }278 String decoded = decodeUtf8(bytes);279 JsonNode value = JSON.readTree(decoded);280 if (value == null || value.isMissingNode() || !value.isObject()) {281 throw new IOException("JWT segment is not a JSON object");282 }283 return decoded;284 }285286 static boolean isBase64UrlSegment(String segment) {287 if (segment.isEmpty() || segment.length() % 4 == 1) {288 return false;289 }290 return segment291 .chars()292 .allMatch(293 character ->294 character >= 'A' && character <= 'Z'295 || character >= 'a' && character <= 'z'296 || character >= '0' && character <= '9'297 || character == '-'298 || character == '_');299 }300301 static String[] requireCompactJwt(String token) {302 if (token == null) {303 throw new IllegalArgumentException("Expected a compact JWT with three segments");304 }305 String[] parts = token.split("\\.", -1);306 if (parts.length != 3) {307 throw new IllegalArgumentException("Expected a compact JWT with three segments");308 }309 return parts;310 }311312 public static void main(String[] args) throws IOException {313 String[] parts = requireCompactJwt(System.getenv("TOKEN"));314 System.out.println("Header:");315 System.out.println(decodeSegment(parts[0]));316 System.out.println("\nPayload:");317 System.out.println(decodeSegment(parts[1]));318 }319}320```321322```csharp323using System.Text;324using System.Text.Json;325326static string DecodeSegment(string segment)327{328 if (329 segment.Length % 4 == 1 ||330 segment.Any(331 character =>332 !(333 character is >= 'A' and <= 'Z' ||334 character is >= 'a' and <= 'z' ||335 character is >= '0' and <= '9' ||336 character is '-' or '_'337 )338 )339 )340 {341 throw new FormatException("JWT segment is not valid Base64URL");342 }343344 byte[] decoded = Convert.FromBase64String(345 segment.Replace('-', '+').Replace('_', '/') +346 new string('=', (4 - segment.Length % 4) % 4)347 );348 string canonicalSegment = Convert349 .ToBase64String(decoded)350 .TrimEnd('=')351 .Replace('+', '-')352 .Replace('/', '_');353 if (canonicalSegment != segment)354 {355 throw new FormatException("JWT segment is not valid Base64URL");356 }357 string decodedJson = new UTF8Encoding(false, true).GetString(decoded);358 using JsonDocument document = JsonDocument.Parse(decodedJson);359 if (document.RootElement.ValueKind is not JsonValueKind.Object)360 {361 throw new FormatException("JWT segment is not a JSON object");362 }363 return decodedJson;364}365366string? token = Environment.GetEnvironmentVariable("TOKEN");367if (token is null)368{369 throw new InvalidOperationException(370 "Expected a compact JWT with three segments"371 );372}373string[] parts = token.Split('.');374if (parts.Length != 3)375{376 throw new InvalidOperationException(377 "Expected a compact JWT with three segments"378 );379}380381Console.WriteLine("Header:");382Console.WriteLine(DecodeSegment(parts[0]));383Console.WriteLine("\nPayload:");384Console.WriteLine(DecodeSegment(parts[1]));385```386387```ruby388require "base64"389require "json"390391parts = ENV.fetch("TOKEN", "").split(".", -1)392raise "Expected a compact JWT with three segments" unless parts.length == 3393394decode = lambda do |segment|395 unless segment.match?(/\A[A-Za-z0-9_-]+\z/) && segment.length % 4 != 1396 raise "JWT segment is not valid Base64URL"397 end398399 padded = segment.ljust((segment.length + 3) & ~3, "=")400 begin401 decoded = Base64.urlsafe_decode64(padded)402 rescue ArgumentError403 raise "JWT segment is not valid Base64URL"404 end405 unless Base64.urlsafe_encode64(decoded, padding: false) == segment406 raise "JWT segment is not valid Base64URL"407 end408409 decoded.force_encoding(Encoding::UTF_8)410 raise "JWT segment is not valid UTF-8" unless decoded.valid_encoding?411412 value = JSON.parse(decoded)413 raise "JWT segment is not a JSON object" unless value.is_a?(Hash)414415 decoded416end417418puts("Header:")419puts(decode.call(parts[0]))420puts("\nPayload:")421puts(decode.call(parts[1]))422```423424425Each example decodes the JWT without verifying the token signature. Use a local decoder for production tokens, and avoid pasting production tokens into third-party tools.426427A decoded SPIFFE JWT-SVID will look similar to:428429```json430{431 "alg": "ES256",432 "kid": "jwt-svid-key-1"433}434```435436```json437{438 "iss": "https://spire-oidc.example.org",439 "aud": ["https://api.openai.com/v1"],440 "sub": "spiffe://example.org/ns/production/sa/openai-wif",441 "iat": 1716235422,442 "exp": 1716235722443}444```445446Use the decoded token to compare the token you received with the OpenAI configuration before you exchange it. Check `alg` and `kid` in the header, and `iss`, `aud`, `sub`, `iat`, and `exp` in the payload. The exact `alg` value depends on your SPIRE Server JWT signing-key configuration.447448## Setting up workload identity federation449450Create a Workload Identity Provider in OpenAI for the SPIFFE JWT-SVID issuer, then add a service account mapping that matches the SPIFFE IDs you trust.451452### Set up the Workload Identity Provider4534541. **Create the Workload Identity Provider.** Set **Name** to a unique value, such as `spiffe-prod`. Use **Description**, such as `Production SPIFFE workloads`, to help admins identify the provider.4554562. **Set the issuer and audience.** Set **OIDC Issuer URL** to the exact value of the JWT-SVID `iss` claim, such as `https://spire-oidc.example.org`. Set **Audience** to the audience value requested from the SPIFFE Workload API. In this example, that value is `https://api.openai.com/v1`.4574583. **Choose the JWKS source.** Leave **Use uploaded JWKS for token verification** disabled when OpenAI can reach your SPIRE OIDC Discovery Provider. OpenAI uses OIDC discovery and the discovered JWKS to verify JWT-SVID signatures.459460 If the issuer isn't reachable from OpenAI, enable **Use uploaded JWKS for token verification**, then set **JWKS JSON** to the public key set for JWT-SVID signing keys. Upload the full public JWKS object, including the surrounding `keys` array. Do not include private key material.4614624. **Add attribute transformations only if you need derived mapping attributes.** Attribute transformations aren't required when mapping directly from `sub`. Use them only when you need to derive a mapping value from one or more token claims. See the [main workload identity federation guide](https://developers.openai.com/api/docs/guides/workload-identity-federation#transform-token-claims-with-cel) for transformation behavior.463464### Set up the service account mapping4654661. **Create a service account mapping.** Set **Name** to a unique value within the Workload Identity Provider, such as `production-openai-wif`. Use **Description**, such as `Production SPIFFE workload for OpenAI API access`, to explain which workload can use the mapping.4674682. **Match the SPIFFE ID.** Set **Key** to `sub` and **Value** to the workload's SPIFFE ID, such as `spiffe://example.org/ns/production/sa/openai-wif`.469470 Prefer exact SPIFFE ID matching for privileged workloads. Use a trailing wildcard only when every SPIFFE ID under that prefix should be able to mint OpenAI access tokens. For example, `spiffe://example.org/ns/production/sa/*` allows any matching production service account path.4714723. **Choose the OpenAI target.** Set **Project** to the OpenAI project that owns the target service account. Set **Service account** to the OpenAI service account the SPIFFE workload can use, such as `spiffe-prod-openai-wif`. Check `Create a new service account in this project` if you wish to create a new service account for this mapping rather than reuse an existing one.4734744. **Narrow API permissions if needed.** Select appropriate **Permissions** such as `api.model.request` and `api.vector_store.read` to further narrow access tokens minted from this mapping. Leave permissions blank to avoid adding a WIF-specific scope restriction; the token still authorizes as the mapped service account.475476## Using the token in code477478Configure your OpenAI SDK client to exchange a fresh SPIFFE JWT-SVID for an OpenAI-issued access token.479480The SDK samples below assume your SPIFFE integration refreshes a JWT-SVID and writes it to `/var/run/spiffe/openai.jwt`. Keep the file readable only by the workload. Because JWT-SVIDs are short lived, refresh the file before the token expires. As an alternative, use a language-specific SPIFFE library to fetch the JWT-SVID directly from the SPIFFE Workload API in the subject token provider when possible to avoid stale token files.481482Set `OPENAI_IDENTITY_PROVIDER_ID` and `OPENAI_SERVICE_ACCOUNT_ID` in the workload environment. The token file contains the external subject token. `OPENAI_IDENTITY_PROVIDER_ID` identifies the OpenAI Workload Identity Provider, and `OPENAI_SERVICE_ACCOUNT_ID` identifies the target OpenAI service account. OpenAI then finds a matching mapping for that provider and service account based on the token claims.483484Authenticate from a SPIFFE JWT-SVID485486```javascript487import { readFile } from "node:fs/promises";488import OpenAI from "openai";489490const tokenPath = "/var/run/spiffe/openai.jwt";491const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;492const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;493494if (!identityProviderId || !serviceAccountId) {495 throw new Error(496 "Set OPENAI_IDENTITY_PROVIDER_ID and OPENAI_SERVICE_ACCOUNT_ID"497 );498}499500function spiffeJwtSvidProvider(path) {501 return {502 tokenType: "jwt",503 getToken: async () => {504 const token = (await readFile(path, "utf8")).trim();505 if (!token) {506 throw new Error("The SPIFFE JWT-SVID file is empty.");507 }508 return token;509 },510 };511}512513const client = new OpenAI({514 workloadIdentity: {515 identityProviderId,516 serviceAccountId,517 provider: spiffeJwtSvidProvider(tokenPath),518 },519});520521const response = await client.responses.create({522 model: "gpt-5.6-terra",523 input: "Say hello from SPIFFE workload identity federation.",524});525526console.log(response.output_text);527```528529```python530import os531from pathlib import Path532533from openai import OpenAI534from openai.auth import SubjectTokenProvider535536TOKEN_PATH = "/var/run/spiffe/openai.jwt"537538539def spiffe_jwt_svid_provider(token_path: str) -> SubjectTokenProvider:540 def get_token() -> str:541 token = Path(token_path).read_text().strip()542 if not token:543 raise RuntimeError("The SPIFFE JWT-SVID file is empty.")544 return token545546 return {"token_type": "jwt", "get_token": get_token}547548549client = OpenAI(550 workload_identity={551 "identity_provider_id": os.environ["OPENAI_IDENTITY_PROVIDER_ID"],552 "service_account_id": os.environ["OPENAI_SERVICE_ACCOUNT_ID"],553 "provider": spiffe_jwt_svid_provider(TOKEN_PATH),554 },555)556557response = client.responses.create(558 model="gpt-5.6-terra",559 input="Say hello from SPIFFE workload identity federation.",560)561562print(response.output_text)563```564565```go566package main567568import (569 "context"570 "fmt"571 "log"572 "os"573 "strings"574575 "github.com/openai/openai-go/v3"576 "github.com/openai/openai-go/v3/auth"577 "github.com/openai/openai-go/v3/option"578 "github.com/openai/openai-go/v3/responses"579)580581const tokenPath = "/var/run/spiffe/openai.jwt"582583type spiffeJWTSVIDProvider struct {584 path string585}586587func (p spiffeJWTSVIDProvider) TokenType() auth.SubjectTokenType {588 return auth.SubjectTokenTypeJWT589}590591func (p spiffeJWTSVIDProvider) GetToken(ctx context.Context, _ auth.HTTPDoer) (string, error) {592 data, err := os.ReadFile(p.path)593 if err != nil {594 return "", &auth.SubjectTokenProviderError{595 Provider: "spiffe",596 Message: "failed to read SPIFFE JWT-SVID",597 Cause: err,598 }599 }600601 token := strings.TrimSpace(string(data))602 if token == "" {603 return "", &auth.SubjectTokenProviderError{604 Provider: "spiffe",605 Message: "SPIFFE JWT-SVID file is empty",606 }607 }608609 return token, nil610}611612func main() {613 client := openai.NewClient(614 option.WithWorkloadIdentity(auth.WorkloadIdentity{615 IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),616 ServiceAccountID: os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),617 Provider: spiffeJWTSVIDProvider{618 path: tokenPath,619 },620 }),621 )622623 response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{624 Model: openai.ChatModelGPT4_1Mini,625 Input: responses.ResponseNewParamsInputUnion{626 OfString: openai.String("Say hello from SPIFFE workload identity federation."),627 },628 })629 if err != nil {630 log.Fatal(err)631 }632633 fmt.Println(response.OutputText())634}635```636637```java638import com.fasterxml.jackson.databind.json.JsonMapper;639import com.openai.auth.SubjectTokenProvider;640import com.openai.auth.SubjectTokenType;641import com.openai.auth.WorkloadIdentity;642import com.openai.client.OpenAIClient;643import com.openai.client.okhttp.OpenAIOkHttpClient;644import com.openai.core.http.HttpClient;645import com.openai.errors.SubjectTokenProviderException;646import com.openai.models.responses.ResponseCreateParams;647import java.nio.file.Files;648import java.nio.file.Path;649import java.util.concurrent.CompletableFuture;650651public final class SpiffeWorkloadIdentityExample {652 private static final String TOKEN_PATH = "/var/run/spiffe/openai.jwt";653654 private SpiffeWorkloadIdentityExample() {}655656 static final class SpiffeJwtSvidProvider implements SubjectTokenProvider {657 private final Path tokenPath;658659 SpiffeJwtSvidProvider(String tokenPath) {660 this.tokenPath = Path.of(tokenPath);661 }662663 @Override664 public SubjectTokenType tokenType() {665 return SubjectTokenType.JWT;666 }667668 @Override669 public String getToken(HttpClient httpClient, JsonMapper jsonMapper) {670 String token;671 try {672 token = Files.readString(tokenPath).trim();673 } catch (Exception e) {674 throw new SubjectTokenProviderException("spiffe", "failed to read SPIFFE JWT-SVID", e);675 }676677 if (token.isEmpty()) {678 throw new SubjectTokenProviderException("spiffe", "SPIFFE JWT-SVID file is empty", null);679 }680681 return token;682 }683684 @Override685 public CompletableFuture<String> getTokenAsync(HttpClient httpClient, JsonMapper jsonMapper) {686 return CompletableFuture.supplyAsync(() -> getToken(httpClient, jsonMapper));687 }688 }689690 public static void main(String[] args) {691 WorkloadIdentity workloadIdentity =692 WorkloadIdentity.builder()693 .identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))694 .serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))695 .provider(new SpiffeJwtSvidProvider(TOKEN_PATH))696 .build();697698 OpenAIClient client = OpenAIOkHttpClient.builder().workloadIdentity(workloadIdentity).build();699700 ResponseCreateParams params =701 ResponseCreateParams.builder()702 .model("gpt-5.6-terra")703 .input("Say hello from SPIFFE workload identity federation.")704 .build();705706 client.responses().create(params).output().stream()707 .flatMap(item -> item.message().stream())708 .flatMap(message -> message.content().stream())709 .flatMap(content -> content.outputText().stream())710 .forEach(outputText -> System.out.println(outputText.text()));711 }712}713```714715```ruby716require "openai"717718TOKEN_PATH = "/var/run/spiffe/openai.jwt"719720class SpiffeJWTSVIDProvider721 include OpenAI::Auth::SubjectTokenProvider722723 def initialize(token_path:)724 @token_path = token_path725 end726727 def token_type728 OpenAI::Auth::TokenType::JWT729 end730731 def get_token732 token = File.read(@token_path).strip733 if token.empty?734 raise OpenAI::Errors::SubjectTokenProviderError.new(735 message: "SPIFFE JWT-SVID file is empty",736 provider: "spiffe"737 )738 end739 token740 rescue SystemCallError => e741 raise OpenAI::Errors::SubjectTokenProviderError.new(742 message: "Failed to read SPIFFE JWT-SVID: #{e.message}",743 provider: "spiffe",744 cause: e745 )746 end747end748749provider = SpiffeJWTSVIDProvider.new(token_path: TOKEN_PATH)750751workload_identity = OpenAI::Auth::WorkloadIdentity.new(752 identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),753 service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),754 provider: provider755)756757client = OpenAI::Client.new(workload_identity: workload_identity)758759response = client.responses.create(760 model: "gpt-5.6-terra",761 input: "Say hello from SPIFFE workload identity federation."762)763764puts(response.output_text)765```766767768## SPIFFE best practices769770- Use JWT-SVIDs for OpenAI workload identity federation. X.509-SVIDs are useful for mutual TLS but aren't accepted by the OpenAI token exchange endpoint.771- Use a single dedicated audience for OpenAI access. Avoid broad audiences such as a whole trust domain or environment name.772- Match exact SPIFFE IDs where possible. Use wildcard mappings only for intentionally shared trust boundaries.773- Keep JWT-SVID lifetimes short to reduce bearer-token replay risk. OpenAI access tokens never outlive the external subject token used for the exchange.774- Rotate signing keys carefully. Publish both old and new public keys through OIDC discovery during the rotation window, or update the uploaded public JWKS before issuing JWT-SVIDs with a new `kid`.775- Keep SPIRE Server and workload clocks synchronized. Significant clock skew can cause otherwise valid JWT-SVIDs to be rejected as not yet valid, too old, or expired.776- Protect the SPIFFE Workload API socket. A process that can fetch a workload's JWT-SVID can attempt to exchange it for OpenAI access.777- Align OpenAI service account boundaries with your application and environment permission boundaries. Don't share a highly privileged service account across unrelated SPIFFE workloads.778- Monitor token exchange failures for issuer, audience, signing key, and mapping mismatches.