SPB Git

Repository Public

spiffe.md 28.0 KB 778 lines markdown

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.
Files main
Menu

Theme