Repository Public
1# Configure workload identity federation with X.509 certificates23> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.45X.509 workload identity federation lets a workload exchange an identity from a TLS client certificate for a short-lived OpenAI access token. The workload then calls the OpenAI API with both the access token and an accepted client certificate. This flow replaces the API key, not the client certificate.67X.509 workload identity federation is available for the OpenAI API. Codex does8 not support it. For Codex, use an OIDC token or SPIFFE JWT-SVID.910For token exchange request and response details, see the [workload identity token exchange reference](https://developers.openai.com/api/reference/workload-identity-federation#exchange-an-x509-certificate). For Mutual TLS permissions, certificate requirements, activation, mTLS hosts, and rotation, see the [Mutual TLS guide](https://developers.openai.com/api/docs/guides/mutual-tls).1112## How it works1314An X.509 workload identity exchange has five parts:15161. Your organization uploads and activates a trusted root certificate in its existing Mutual TLS settings.172. An X.509 Workload Identity Provider derives `openai.*` attributes from the verified client certificate. It must derive one non-empty `openai.subject` value.183. A service account mapping authorizes the derived identity to use one OpenAI service account within a project.194. The workload presents its certificate to the X.509 token endpoint on `mtls.auth.openai.com` and requests a short-lived bearer token. The certificate comes from the TLS connection; the request body doesn't contain a `subject_token`.205. The workload presents the bearer token and a client certificate to an API route on `mtls.api.openai.com` for API authorization.2122The bearer token and the certificate are authorized independently on the API request. A certificate by itself doesn't authorize an OpenAI API call.2324## Before you begin2526You need:2728- Permission to manage Mutual TLS certificates and Workload Identity Providers for your organization.29- A project and service account for the workload.30- A client certificate, its private key, and any intermediate certificates required to build a path to your trusted root.31- An active trusted root certificate at the organization or project level.3233Keep private keys outside source control and restrict access to the workload that uses them. Don't log private keys, certificate contents, or returned access tokens.3435## Configure Mutual TLS certificate trust3637X.509 Workload Identity Providers reuse your organization's existing Mutual TLS certificate configuration. They don't upload certificates or maintain a separate certificate trust store.3839Follow the [Mutual TLS guide](https://developers.openai.com/api/docs/guides/mutual-tls) to review certificate40requirements, mTLS hosts, certificate activation behavior, CEL filters, and41client configuration. Then open [Organization settings > Security > Mutual42TLS](https://platform.openai.com/settings/organization/security/mtls), upload43the trusted certificate in PEM format, and activate it for the organization or44for each project that will use X.509 workload identity federation.4546If your client certificate chains through an intermediate certificate, configure the stable trust anchor and present the leaf followed by the current intermediate certificates during the TLS handshake. OpenAI uses intermediates provided by the request and doesn't retrieve missing intermediates from certificate URLs.4748## Configure an X.509 provider4950To configure an X.509 provider:51521. Open [Organization settings > Security > Workload Identity Provider](https://platform.openai.com/settings/organization/security/workload-identity-provider), then select **Create identity provider**.532. Choose **X.509** for **Provider type**, then enter a name and optional description. X.509 providers don't use OIDC issuer, audience, discovery, or JWKS settings. You can't change the provider type after you create it.543. Under **Advanced**, optionally add an **Attribute conditions** CEL expression to reject certificates before mapping resolution.554. Under **Attribute transformations**, enter a non-empty expression for the required `openai.subject` transformation. The dashboard adds the `subject` row when you select X.509 and displays and applies the `openai.` prefix. Choose a stable certificate fact that identifies the workload.565. Optionally add transformations with other unique `openai.*` names, then select **Create**.5758For example, this configuration uses the certificate common name as the canonical subject and exposes the organizational unit as an additional mapping attribute:5960```json61[62 {63 "attribute": "openai.subject",64 "expression": "assertion.subject.common_name"65 },66 {67 "attribute": "openai.environment",68 "expression": "assertion.subject.organizational_unit"69 }70]71```7273Certificate facts are available under `assertion.subject` and `assertion.subject_alt_names`. Transformation results used for mappings must be scalar values. Additional transformations must have unique `openai.*` names.7475For example, an **Attribute conditions** expression can restrict the provider to production certificates:7677```text78assertion.subject.organizational_unit == "Production"79```8081## Create a service account mapping82831. From the X.509 provider details page, select **Create mapping**.842. Select the target project and service account, and grant only the API permissions the workload needs.853. In the **Key** and **Value** fields, require an exact `openai.subject` value. X.509 mappings support either no assertions, represented as an empty object (`{}`), or assertions whose keys start with `openai.`.864. Select **Create**.8788For example:8990| Key | Value |91| ---------------- | ----------------------- |92| `openai.subject` | `payments-service-prod` |9394X.509 mappings use derived `openai.*` attributes. They don't match raw JWT claims such as `sub`, `iss`, or `aud`.9596The provider list displays the provider ID, and the mapping details display the selected service account and its service account ID. Record both identifiers; the workload sends them during token exchange.9798## Use X.509 workload identity with an SDK99100Set environment variables for the certificate chain, private key, provider, and service account:101102```bash103export OPENAI_MTLS_CERT_CHAIN="/path/to/client-chain.pem"104export OPENAI_MTLS_KEY="/path/to/client-key.pem"105export OPENAI_IDENTITY_PROVIDER_ID="idp_example"106export OPENAI_SERVICE_ACCOUNT_ID="svc_acct_example"107```108109The certificate-chain file should contain the leaf certificate first, followed by any intermediate certificates. Don't include certificate material or a `subject_token` in the request body.110111Configure an OpenAI SDK client with these values. The SDK presents the client certificate during token exchange and API requests, routes API requests to the mTLS endpoint, and renews short-lived access tokens automatically.112113Authenticate with an X.509 client certificate114115```javascript116import { readFile } from "node:fs/promises";117118import OpenAI from "openai";119import { workloadIdentity } from "openai/auth/x509-transport";120121const certificatePath = process.env.OPENAI_MTLS_CERT_CHAIN;122const privateKeyPath = process.env.OPENAI_MTLS_KEY;123const identityProviderId = process.env.OPENAI_IDENTITY_PROVIDER_ID;124const serviceAccountId = process.env.OPENAI_SERVICE_ACCOUNT_ID;125126if (127 !certificatePath ||128 !privateKeyPath ||129 !identityProviderId ||130 !serviceAccountId131) {132 throw new Error(133 "Set OPENAI_MTLS_CERT_CHAIN, OPENAI_MTLS_KEY, OPENAI_IDENTITY_PROVIDER_ID, and OPENAI_SERVICE_ACCOUNT_ID"134 );135}136137const credential = workloadIdentity.fromX509({138 certificateChain: await readFile(certificatePath, "utf8"),139 privateKey: await readFile(privateKeyPath, "utf8"),140 identityProviderId,141 serviceAccountId,142});143144try {145 const client = new OpenAI({ credential });146 const response = await client.responses.create({147 model: "gpt-5.6-terra",148 input: "Say hello from X.509 workload identity federation.",149 });150151 console.log(response.output_text);152} finally {153 await credential.close();154}155```156157```python158import os159import ssl160161from openai import DefaultHttpx2Client, OpenAI162from openai.auth import x509_workload_identity163164tls_context = ssl.create_default_context()165tls_context.load_cert_chain(166 certfile=os.environ["OPENAI_MTLS_CERT_CHAIN"],167 keyfile=os.environ["OPENAI_MTLS_KEY"],168)169170with OpenAI(171 base_url="https://mtls.api.openai.com/v1",172 workload_identity=x509_workload_identity(173 identity_provider_id=os.environ["OPENAI_IDENTITY_PROVIDER_ID"],174 service_account_id=os.environ["OPENAI_SERVICE_ACCOUNT_ID"],175 ),176 http_client=DefaultHttpx2Client(verify=tls_context, follow_redirects=False),177) as client:178 response = client.responses.create(179 model="gpt-5.6-terra",180 input="Say hello from X.509 workload identity federation.",181 )182183 print(response.output_text)184```185186```go187package main188189import (190 "context"191 "crypto/tls"192 "fmt"193 "log"194 "net/http"195 "os"196197 "github.com/openai/openai-go/v3"198 "github.com/openai/openai-go/v3/auth"199 "github.com/openai/openai-go/v3/option"200 "github.com/openai/openai-go/v3/responses"201)202203func main() {204 certificate, err := tls.LoadX509KeyPair(205 os.Getenv("OPENAI_MTLS_CERT_CHAIN"),206 os.Getenv("OPENAI_MTLS_KEY"),207 )208 if err != nil {209 log.Fatal(err)210 }211212 transport, err := auth.NewX509Transport(&http.Transport{213 TLSClientConfig: &tls.Config{214 Certificates: []tls.Certificate{certificate},215 MinVersion: tls.VersionTLS12,216 },217 })218 if err != nil {219 log.Fatal(err)220 }221 defer transport.Close()222223 client := openai.NewClient(224 option.WithX509WorkloadIdentity(auth.X509WorkloadIdentity{225 IdentityProviderID: os.Getenv("OPENAI_IDENTITY_PROVIDER_ID"),226 ServiceAccountID: os.Getenv("OPENAI_SERVICE_ACCOUNT_ID"),227 Transport: transport,228 }),229 )230231 response, err := client.Responses.New(context.Background(), responses.ResponseNewParams{232 Model: "gpt-5.6-terra",233 Input: responses.ResponseNewParamsInputUnion{234 OfString: openai.String("Say hello from X.509 workload identity federation."),235 },236 })237 if err != nil {238 log.Fatal(err)239 }240241 fmt.Println(response.OutputText())242}243```244245```java246import com.openai.client.OpenAIClient;247import com.openai.client.okhttp.OpenAIOkHttpClient;248import com.openai.client.okhttp.X509Transport;249import com.openai.client.okhttp.X509WorkloadIdentity;250import com.openai.models.responses.ResponseCreateParams;251import java.io.InputStream;252import java.nio.file.Files;253import java.nio.file.Paths;254import java.security.KeyStore;255import java.time.Duration;256import java.util.Arrays;257import javax.net.ssl.KeyManagerFactory;258import javax.net.ssl.TrustManagerFactory;259import javax.net.ssl.X509ExtendedKeyManager;260import javax.net.ssl.X509TrustManager;261262char[] password = System.getenv("OPENAI_X509_KEYSTORE_PASSWORD").toCharArray();263try {264 KeyStore keyStore = KeyStore.getInstance("PKCS12");265 try (InputStream input =266 Files.newInputStream(Paths.get(System.getenv("OPENAI_X509_KEYSTORE_PATH")))) {267 keyStore.load(input, password);268 }269270 KeyManagerFactory keyManagers =271 KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());272 keyManagers.init(keyStore, password);273 X509ExtendedKeyManager keyManager =274 Arrays.stream(keyManagers.getKeyManagers())275 .filter(X509ExtendedKeyManager.class::isInstance)276 .map(X509ExtendedKeyManager.class::cast)277 .findFirst()278 .orElseThrow(() -> new IllegalStateException("No X.509 key manager available"));279280 TrustManagerFactory trustManagers =281 TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());282 trustManagers.init((KeyStore) null);283 X509TrustManager trustManager =284 Arrays.stream(trustManagers.getTrustManagers())285 .filter(X509TrustManager.class::isInstance)286 .map(X509TrustManager.class::cast)287 .findFirst()288 .orElseThrow(() -> new IllegalStateException("No X.509 trust manager available"));289290 X509Transport transport =291 X509Transport.builder()292 .keyManager(keyManager)293 .certificateAlias(System.getenv("OPENAI_X509_CERTIFICATE_ALIAS"))294 .trustManager(trustManager)295 .build();296 X509WorkloadIdentity identity =297 X509WorkloadIdentity.builder()298 .identityProviderId(System.getenv("OPENAI_IDENTITY_PROVIDER_ID"))299 .serviceAccountId(System.getenv("OPENAI_SERVICE_ACCOUNT_ID"))300 .transport(transport)301 .refreshBuffer(Duration.ofMinutes(10))302 .build();303304 OpenAIClient client = OpenAIOkHttpClient.builder().x509WorkloadIdentity(identity).build();305 try {306 ResponseCreateParams params =307 ResponseCreateParams.builder()308 .model("gpt-5.6-terra")309 .input("Say hello from X.509 workload identity federation.")310 .build();311312 client.responses().create(params).output().stream()313 .flatMap(item -> item.message().stream())314 .flatMap(message -> message.content().stream())315 .flatMap(content -> content.outputText().stream())316 .forEach(outputText -> System.out.println(outputText.text()));317 } finally {318 client.close();319 }320} finally {321 Arrays.fill(password, '\0');322}323```324325```ruby326require "openai"327require "openssl"328329certificate_chain = OpenSSL::X509::Certificate.load(330 File.binread(ENV.fetch("OPENAI_MTLS_CERT_CHAIN"))331)332certificate, *intermediates = certificate_chain333private_key = OpenSSL::PKey.read(File.binread(ENV.fetch("OPENAI_MTLS_KEY")))334335http_client = OpenAI::NetHTTPClient.new do |connection|336 connection.cert = certificate337 connection.extra_chain_cert = intermediates338 connection.key = private_key339end340341workload_identity = OpenAI::Auth::X509WorkloadIdentity.new(342 identity_provider_id: ENV.fetch("OPENAI_IDENTITY_PROVIDER_ID"),343 service_account_id: ENV.fetch("OPENAI_SERVICE_ACCOUNT_ID"),344 http_client: http_client345)346347begin348 client = OpenAI::Client.new(api_key: nil, workload_identity: workload_identity)349 response = client.responses.create(350 model: "gpt-5.6-terra",351 input: "Say hello from X.509 workload identity federation."352 )353354 puts(response.output_text)355ensure356 http_client.close357end358```359360361These examples require OpenAI SDK versions that support the X.509 configuration shown here: JavaScript 7.8.0 or later with the `undici` peer dependency installed, Python 3.6.0 or later, Go 3.54.0 or later, Java 4.55.0 or later, and Ruby 0.83.0 or later.362363The Java example loads a PKCS12 keystore to construct its `X509ExtendedKeyManager` and uses the platform default trust store to construct its `X509TrustManager`. Set `OPENAI_X509_KEYSTORE_PATH`, `OPENAI_X509_KEYSTORE_PASSWORD`, and `OPENAI_X509_CERTIFICATE_ALIAS` for this example. You can instead supply PEM-backed or hardware-backed managers to the SDK.364365## Exchange the certificate manually366367To inspect or implement the token exchange protocol directly, present the certificate to the X.509 token endpoint:368369```bash370curl --cert "$OPENAI_MTLS_CERT_CHAIN" \371 --key "$OPENAI_MTLS_KEY" \372 --request POST "https://mtls.auth.openai.com/oauth/token" \373 --header "Content-Type: application/json" \374 --data @- <<JSON375{376 "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",377 "subject_token_type": "urn:openai:params:oauth:token-type:x509",378 "identity_provider_id": "${OPENAI_IDENTITY_PROVIDER_ID}",379 "service_account_id": "${OPENAI_SERVICE_ACCOUNT_ID}"380}381JSON382```383384A successful exchange returns an ordinary short-lived bearer token:385386```json387{388 "access_token": "eyJ...",389 "issued_token_type": "urn:ietf:params:oauth:token-type:access_token",390 "token_type": "Bearer",391 "expires_in": 3600,392 "expires_at": 1789045200,393 "scope": "api.model.read api.model.request"394}395```396397The `scope` property is returned only when the matching service account mapping has permissions.398399The expiration values are illustrative. The returned lifetime can be shorter when the verified client certificate expires sooner. See the [token exchange response fields](https://developers.openai.com/api/reference/workload-identity-federation#response) for the units and meaning of `expires_in` and `expires_at`.400401Read the `access_token` value from the successful response into your application's credential store or an environment variable such as `OPENAI_WIF_ACCESS_TOKEN`. Treat it as a secret and don't print, log, or commit it.402403## Call the OpenAI API manually404405Set `OPENAI_MODEL` to `gpt-6-astra`, the current default, or another model available to the target project. Then send the bearer token and an accepted client certificate to the API mTLS endpoint:406407```bash408curl --request POST \409 --cert "$OPENAI_MTLS_CERT_CHAIN" \410 --key "$OPENAI_MTLS_KEY" \411 --header "Authorization: Bearer $OPENAI_WIF_ACCESS_TOKEN" \412 --header "Content-Type: application/json" \413 --data "{\"model\":\"$OPENAI_MODEL\",\"input\":\"Say hello in one sentence.\"}" \414 "https://mtls.api.openai.com/v1/responses"415```416417Use the bearer token instead of an API key, and continue to present an accepted client certificate on the API request.418419The bearer isn't cryptographically bound to the certificate. Reusing the exchange certificate for the API request is the most direct configuration, but the API request can use another certificate that independently satisfies the same current API mTLS policy.420421## Token lifetime and renewal422423An X.509 workload identity token expires after at most one hour and never outlives the verified client certificate. The exchange doesn't return a refresh token. Repeat the certificate exchange to obtain another access token.424425For manual exchanges, keep `expires_at` with the access token and schedule another exchange before that timestamp. Allow for clock differences and request latency. See [token renewal guidance](https://developers.openai.com/api/docs/guides/workload-identity-federation#renew-the-access-token) for an example.426427Rotating an intermediate certificate doesn't require changing the configured root. Present the new complete chain on subsequent exchanges and API requests.428429## Troubleshoot token exchange430431X.509 token exchange returns generic OAuth errors and doesn't expose certificate, root, provider, or mapping details.432433| Result | Typical causes |434| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |435| HTTP `403` | The request used a method or path other than exact `POST /oauth/token` on `mtls.auth.openai.com`. |436| `invalid_subject_token` | The TLS client certificate is missing or invalid, the presented chain can't reach an active root, the certificate is outside its validity period, or a Mutual TLS certificate-admission rule rejects it. |437| `invalid_grant` | The provider or mapping is invalid or disabled, a provider **Attribute conditions** expression rejects the identity, no applicable roots are active, or no mapping matches. |438| Server error | OpenAI returned a temporary server error. Retry according to your normal transient-error policy. |439440An X.509 exchange never falls back to an OIDC or ordinary OAuth flow.441442## Limitations443444- X.509 Workload Identity Providers don't maintain a separate certificate trust store.445- The bearer token isn't certificate-bound and doesn't use DPoP or a `cnf` claim.446- The certificate exchange isn't certificate-only API authorization. API requests still require the bearer token and an accepted client certificate.447- OpenAI doesn't fetch missing intermediate certificates from AIA URLs. Present the complete chain during TLS negotiation.448- OpenAI doesn't perform certificate revocation list (CRL) or OCSP checks during this flow. Plan certificate incident response around Mutual TLS root, provider, and mapping controls and the short lifetime of issued tokens.449- This flow doesn't add support for SPIFFE X.509-SVIDs. The [SPIFFE guide](https://developers.openai.com/api/docs/guides/workload-identity-federation/spiffe) continues to use JWT-SVIDs.