OpenAI Workload Identity Federation
Let a VM call the OpenAI API without storing an OpenAI API key. The VM gets a short-lived exe.dev token, and OpenAI exchanges it for a short-lived access token for one of your project's service accounts.
Usage is billed to your OpenAI organization. To use exe.dev's built-in models instead, see the LLM integration.
OpenAI's documentation says that OIDC issuers other than the providers in its setup guides aren't supported yet, and asks you to contact OpenAI support if your provider isn't listed. exe.dev is a custom OIDC issuer, so your organization may need OpenAI to enable it.
Setup uses two browser tabs: the exe.dev Integrations page and OpenAI's Workload Identity Provider settings. You need permission to manage Workload Identity Providers in your OpenAI organization.
1. Start the integration in exe.dev
On the Integrations page, add an Identity Federation
integration and choose OpenAI. Enter a name, such as openai-wif. To use
it through an LLM integration (step 4), you don't need to attach it to any VMs.
The dialog shows an Issuer and a Subject. You will paste both into OpenAI in the next step. Keep the dialog open; the subject is reserved for 15 minutes.
The integration's exe.dev tokens last 15 minutes, the longest allowed. An OpenAI access token never outlives the token it was exchanged for, so a shorter lifetime only means more frequent exchanges.
2. Create the provider and mapping in OpenAI
In OpenAI, open Workload Identity Provider settings and create a provider:
| OpenAI field | Value |
|---|---|
| Name | Any unique name, such as exe-dev |
| OIDC Issuer URL | The exe.dev Issuer |
| Audience | https://api.openai.com/v1 |
| Custom URL for OIDC discovery, uploaded JWKS | Leave both off; OpenAI uses the issuer's OIDC discovery |
Then, on the provider's details page, add a service account mapping:
| OpenAI field | Value |
|---|---|
| Key | sub |
| Value | The exe.dev Subject, exactly. Do not use a wildcard. |
| Project | The project that receives the API usage |
| Service account | A service account in that project; the VM acts as it |
| Permissions | Optional. Leave empty to allow everything the service account can do, or narrow it, for example to api.model.request and api.model.read |
OpenAI shows the provider's ID on its details page and the service account's ID in the project's settings. You need both in the next step.
3. Enter the IDs and save
Back in the exe.dev dialog, fill in:
| Field | Where it comes from |
|---|---|
| Identity provider ID | The Workload Identity Provider you just created |
| Service account ID | The mapping's service account |
| Project ID (optional) | The mapping's project, proj_..., from the project's settings |
Click Run. If you don't have the IDs yet, you can save with the fields empty and edit the integration later to add them.
The access token is always bound to the mapping's project, so the project ID
is optional. When it's set, requests send it as the OpenAI-Project header.
OpenAI accepts that header only if it names the token's project and rejects
any other project with 401 mismatched_project. The header doesn't switch
projects; it makes requests fail instead of using a project you didn't expect,
for example after someone points the mapping at a different project.
4. Use it from an LLM integration
On the Integrations page, add an
LLM integration, set its OpenAI provider to
Workload identity, and pick this integration. Attach the LLM integration
to your VMs; the workload identity integration doesn't need to be attached.
exe.dev exchanges and renews OpenAI tokens for you and, if you set a project
ID, sends it as OpenAI-Project. Both integrations must be in the same scope:
personal for a personal LLM integration, team for a team one.
Or from the CLI:
ssh exe.dev integrations add llm --name gpt \
--openai=wif --openai-wif=openai-wif \
--anthropic=disabled --fireworks=disabled --attach vm:example-vm
On the VM, OpenAI SDKs and tools use the LLM integration's /v1 URL as their
base URL and need no API key:
curl https://gpt.int.exe.xyz/v1/responses \
-H 'content-type: application/json' \
-d '{"model":"gpt-5.5","input":"Hello from exe.dev"}'
For a team integration, use https://gpt.team.exe.xyz. To run Codex, see
Use with Codex.
Other uses: call OpenAI directly
To call OpenAI yourself instead of through an LLM integration, for example from code that does its own token exchange, attach this integration to the VM. Then run this on the VM. It reads the IDs from the integration, exchanges a fresh exe.dev token for an OpenAI access token, and sends a request:
EXE_WIF_URL=https://openai-wif.int.exe.xyz
META="$(curl -fsS "$EXE_WIF_URL/metadata")"
PROJECT_ID="$(echo "$META" | jq -r '.project_id // empty')"
JWT="$(curl -fsS "$EXE_WIF_URL/token" | jq -er .token)"
ACCESS_TOKEN="$(
jq -n --arg jwt "$JWT" --argjson meta "$META" '{
grant_type: "urn:ietf:params:oauth:grant-type:token-exchange",
subject_token_type: "urn:ietf:params:oauth:token-type:jwt",
subject_token: $jwt,
identity_provider_id: $meta.identity_provider_id,
service_account_id: $meta.service_account_id
}' |
curl -fsS https://auth.openai.com/oauth/token \
-H 'content-type: application/json' -d @- |
jq -er .access_token
)"
curl -fsS https://api.openai.com/v1/responses \
-H "authorization: Bearer $ACCESS_TOKEN" \
${PROJECT_ID:+-H "OpenAI-Project: $PROJECT_ID"} \
-H 'content-type: application/json' \
-d '{"model":"gpt-5.5","input":"Hello from exe.dev"}'
unset JWT ACCESS_TOKEN
Replace openai-wif with your integration's name. For a team integration,
use https://<name>.team.exe.xyz.
Tokens
- Fetch a new exe.dev token from
/tokenfor every exchange. - Exchange again before
expires_at(orexpires_inseconds after the exchange). OpenAI returns no refresh token. - Both tokens are credentials. Don't print, log, or commit them.
OpenAI's SDKs can do the exchange and renewal for you. See OpenAI's workload identity federation guide and its token exchange reference.
Use the CLI instead
The CLI prints the issuer and subject only after it creates the integration, so add the IDs with a second command:
ssh exe.dev integrations add wif --name openai-wif \
--audience https://api.openai.com/v1 --consumer openai --ttl 15m \
--attach vm:example-vm
The --attach is only needed to call OpenAI directly from the VM. Create the
OpenAI provider and mapping with the printed Issuer and Subject, then:
ssh exe.dev integrations edit openai-wif \
--metadata=identity_provider_id=IDENTITY_PROVIDER_ID \
--metadata=service_account_id=SERVICE_ACCOUNT_ID \
--metadata=project_id=PROJECT_ID
The project_id line is optional. Add --team to add for a team
integration. edit replaces all metadata, so include every ID each time.
Troubleshooting
- The exchange is rejected. Check that the provider's issuer and audience
and the mapping's
subvalue exactly match the integration, that the mapping is enabled, and that the request names the right provider and service account. OpenAI's error reference lists the causes. - The exchange works but API calls fail. The access token has the
service account's project access and the mapping's permissions. Check
those, and the project's IP allowlist if it has one.
401 mismatched_projectmeans the integration's project ID isn't the mapping's project. /tokenor/metadatadoesn't respond. The integration isn't attached to this VM.