Skip to content

feat: add OpenAICredentials with workload identity federation - #2815

Merged
aabchoo merged 17 commits into
theagentrouter:mainfrom
junfeiliu531:spiffe-token-provider-3
Oct 27, 2026
Merged

aabchoo merged 17 commits into
theagentrouter:mainfrom
junfeiliu531:spiffe-token-provider-3

Conversation

@junfeiliu531

@junfeiliu531 junfeiliu531 commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Description

Fixes #2791: secretless access to OpenAI using OpenAI Workload Identity Federation. A new OpenAICredentials BackendSecurityPolicy type has the controller obtain a subject token (SPIFFE JWT-SVID or OIDC), exchange it for a short-lived OpenAI access token, and rotate it before it expires. The data plane injects the token into requests to OpenAI.

OpenAI Workload Identity Federation reference: https://developers.openai.com/api/reference/workload-identity-federation

API (api/v1beta1)

spec:
  type: OpenAICredentials
  openAICredentials:
    organization: org-...   # optional, sent as OpenAI-Organization
    project: proj_...       # optional, sent as OpenAI-Project
    workloadIdentityFederationConfig:
      identityProviderID: idp_...   # required, OpenAI Workload Identity Provider ID
      serviceAccountID: user-...    # required, OpenAI service account ID
      subjectToken:                 # exactly one source
        spiffeJWTSVID:
          audience: <JWT-SVID aud claim, as configured for the identity provider>
          socketPath: unix:///run/spire/sockets/agent.sock   # optional
        # oidcExchangeToken:
        #   oidc: { ... }           # BackendSecurityPolicyOIDC, as in AWS/Azure/GCP
  • workloadIdentityFederationConfig (OpenAIWorkloadIdentityFederationConfig) mirrors gcpCredentials.workloadIdentityFederationConfig. It holds the parameters of OpenAI's token exchange request: identityProviderID and serviceAccountID are sent as identity_provider_id and service_account_id.
  • subjectToken (OpenAISubjectToken) groups the subject token sources; new sources are added there. OpenAI only accepts JWT subject tokens, so the subject token is always sent as urn:ietf:params:oauth:token-type:jwt, and an oidcExchangeToken provider must return a JWT.
  • oidcExchangeToken reuses BackendSecurityPolicyOIDC, so the OIDC option has the same name and shape as in AWSCredentials, AzureCredentials and GCPCredentials.
  • spiffeJWTSVID is named after what is fetched, leaving room for other SPIFFE sources later. Its API doc notes that the SPIFFE identity belongs to the controller, not the policy (unlike oidcExchangeToken, where the policy supplies its own credentials). Every such policy authenticates as the controller and can obtain tokens for any OpenAI service account mapped to the controller's SPIFFE ID, so only trusted cluster admins should create them.
  • No tokenURL field: the controller always exchanges at https://auth.openai.com/oauth/token, so a policy cannot make the controller send subject tokens to an arbitrary URL.
  • No audience or scope parameters: OpenAI's exchange does not accept them; permissions come from the service account mapping configured in OpenAI.
  • CEL rules: exactly one source in subjectToken; socketPath must be unix:// or tcp://; openAICredentials is mutually exclusive with the other credential fields.
  • v1beta1 only, following the CredentialOverride precedent.

Controller (internal/controller)

  • tokenprovider.NewSPIFFETokenProvider: a TokenProvider backed by the SPIFFE Workload API (go-spiffe/v2). It fetches a JWT-SVID for a fixed audience. The socket address is passed explicitly or falls back to SPIFFE_ENDPOINT_SOCKET.
  • tokenprovider.NewOpenAIWorkloadIdentityTokenProvider wraps any subject TokenProvider and sends OpenAI's documented JSON request body. The expiry comes from expires_at (Unix seconds) in the response; a response without it is rejected.
  • OpenAI error responses ({"error": {"message", "code"}}) are surfaced in the policy status, e.g. token exchange failed with status 401: invalid_subject_token: Invalid subject token. Other error bodies are included truncated to 512 bytes.
  • AI_GATEWAY_OPENAI_PROXY_URL routes token exchange requests through a proxy, like AI_GATEWAY_STS_PROXY_URL, AI_GATEWAY_AZURE_PROXY_URL and AI_GATEWAY_GCP_AUTH_PROXY_URL. When unset, the default transport's proxy settings (HTTPS_PROXY) still apply. Documented in the controller extraEnvVars example in the Helm values.
  • rotators.NewOpenAITokenRotator stores the access token under openAIAccessToken in the generated ai-eg-bsp-<name> secret, mirroring the Azure token rotator. Rotation uses the controller's configured pre-rotation window (CredentialPreRotationWindow, default 5 minutes), like the other providers.
  • The BSP controller builds the subject token provider from subjectToken.spiffeJWTSVID or subjectToken.oidcExchangeToken. OIDC client secrets referenced from other namespaces go through the same ReferenceGrant check as AWS/Azure/GCP.
  • The gateway translates the policy into filterapi.OpenAIAuth. credentialOverride defaults to the x-aigw-openai-access-token header.

Data plane (internal/backendauth)

  • An OpenAIAuth handler sets Authorization: Bearer <token>, plus OpenAI-Organization and OpenAI-Project when configured. The same headers are set when the token comes from a credential override.

Example

  • examples/basic/openai-spiffe.yaml.

Related Issues/PRs (if applicable)

Fixes #2791

Special notes for reviewers (if applicable)

  • Mounting the SPIFFE Workload API socket into the controller (with SPIFFE_ENDPOINT_SOCKET pointing at it) is left to the deployment. The identity provider and service account mapping must be configured in the OpenAI organization.
  • Tests: unit tests for the SPIFFE and OpenAI token providers (including the proxy and OpenAI error handling), rotator, handler and controller wiring; CEL tests for the API rules. An envtest test in tests/controller runs the BSP controller against a fake SPIFFE Workload API and a fake OpenAI token exchange endpoint. There is no kind e2e: that would require SPIRE in CI, and no other provider's token rotation has one.
  • Tested manually against OpenAI on a cluster with SPIRE: the controller exchanged a real JWT-SVID for an OpenAI access token (lifetime about 1 hour), the token worked for POST /v1/chat/completions, it was rotated before expiry, and a policy with the wrong SVID audience reported OpenAI's invalid_subject_token error. The data plane path (gateway injecting the token) was not exercised on that cluster.
  • Possible follow-up: a spiffeJWTSVID option next to the existing oidcExchangeToken in AWS, Azure and GCP, without deprecating anything.

🤖 Generated with Claude Code [1]

1: https://claude.com/claude-code

@junfeiliu531
junfeiliu531 requested a review from a team as a code owner October 8, 2026 03:44
@netlify

netlify Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for theagentrouter ready!

Name Link
🔨 Latest commit 63a7a6f
🔍 Latest deploy log https://app.netlify.com/projects/theagentrouter/deploys/6adfe875accbb4271cfd706b
😎 Deploy Preview https://deploy-preview-2815--theagentrouter.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@aabchoo aabchoo self-assigned this Oct 8, 2026
@aabchoo aabchoo added the area/controller Controller and reconciliation label Oct 8, 2026
@junfeiliu531 junfeiliu531 changed the title controller: rotate OpenAICredentials access tokens openai: add OpenAICredentials with workload identity federation Oct 13, 2026
@junfeiliu531 junfeiliu531 changed the title openai: add OpenAICredentials with workload identity federation feat: add OpenAICredentials with workload identity federation Oct 13, 2026
@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.36735% with 13 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...ternal/controller/rotators/openai_token_rotator.go 87.93% 7 Missing ⚠️
internal/controller/backend_security_policy.go 84.61% 4 Missing ⚠️
...rovider/openai_workload_identity_token_provider.go 96.82% 2 Missing ⚠️

📢 Thoughts on this report? Let us know!

@aabchoo aabchoo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just minor comments but everything looks good to me!

Comment thread internal/backendauth/openai.go Outdated
Comment thread internal/controller/tokenprovider/openai_workload_identity_token_provider.go Outdated
@aabchoo

aabchoo commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

/retest

@aabchoo

aabchoo commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

We should add a note that the controller "owns" the SPIFFE identity not the backend security policies. This is different from OIDC where the policy makers / tenants sets the credentials.

@aabchoo
aabchoo self-requested a review October 19, 2026 20:59
yuzisun pushed a commit that referenced this pull request Oct 22, 2026
…rable (#2886)

**Description**

The current backend security policy rotation time is 5 minutes before
expiration. The value is fine, but flexibility would be even better.
Making this configurable on a global level with the default to 5
minutes. Decided to keep the implementation global instead of on a
per-backendSecurityPolicy level to maintain simplicity and there has
been no claims for varying pre-rotation times.

There should be no visible difference for existing users. Update
`credentialPreRotationWindow` in
manifests/charts/ai-gateway-helm/values.yaml to increase/decrease the
time to rotate. Duration accepts a decimal number followed by a unit:
ns, us/µs, ms, s, m, h. You can combine them and use fractions, e.g.
90s, 1.5m, 1h30m


May need to update
#2815 or this PR to
have change work across all.

Majority of the changed lines are adding codecov.

---------

Signed-off-by: achoo30 <achoo30@bloomberg.net>
Comment thread internal/controller/tokenprovider/openai_workload_identity_token_provider.go Outdated
Junfei Liu and others added 14 commits October 26, 2026 19:50
Add a tokenprovider.TokenProvider backed by the SPIFFE Workload API.
It fetches a JWT-SVID for a fixed audience; the socket address is
passed explicitly or falls back to SPIFFE_ENDPOINT_SOCKET.

First step of theagentrouter#2791. Not wired into any rotator yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
Add an OpenAICredentials type to v1beta1 for secretless access to
OpenAI via OAuth 2.0 Token Exchange (RFC 8693). It carries optional
organization/project IDs and a tokenExchange block (tokenURL,
subjectTokenType defaulting to ...:token-type:jwt, audience, scopes).

The subject token comes from exactly one of an OIDC client-credentials
flow or a SPIFFE JWT-SVID (audience plus optional socketPath).

v1beta1 only, following the CredentialOverride precedent. Part of
theagentrouter#2791; the controller does not act on the new type yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
Replace subjectToken.oidc (*egv1a1.OIDC) with oidcExchangeToken
(*BackendSecurityPolicyOIDC), matching the field name and nesting of
the AWS, Azure and GCP credential types. The controller can then read
the OIDC config the same way for all four types.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
Drop the subjectToken wrapper: oidcExchangeToken and the SPIFFE source
now sit directly under tokenExchange, so "OIDC vs SPIFFE" can be
spelled the same way if AWS/GCP/Azure gain a SPIFFE option later.

Rename spiffe to spiffeJWTSVID (type BackendSecurityPolicySPIFFEJWTSVID)
since nothing is exchanged when fetching it, and to leave room for an
X.509-SVID source. Clarify which hop each audience/scopes field
applies to.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
Group the subject token sources under tokenExchange.subjectToken
again, keeping the oidcExchangeToken and spiffeJWTSVID names. The
wrapper keeps the sources separate from the exchange parameters and
gives new sources an obvious home.

Write the one-of rule as a list count so a new source only adds an
entry to it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
The controller does not act on OpenAICredentials yet. Return a
"not yet supported" error from credential rotation and the gateway
auth translation instead of the generic unsupported-type errors, and
handle the type in getBSPGeneratedSecretName so it cannot panic once
rotation reaches it.

Document which hop each audience field applies to.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
Add filterapi.OpenAIAuth carrying an access token plus optional
organization and project IDs. The handler sets Authorization: Bearer
and, when configured, OpenAI-Organization and OpenAI-Project. The same
headers are set when the token comes from a credential override.

Part of theagentrouter#2791.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
Add an RFC 8693 token exchange provider that wraps a subject token
provider (SPIFFE JWT-SVID or OIDC), and an OpenAI token rotator that
stores the exchanged access token in the generated BSP secret. Rotation
follows the exchanged token's expires_in, falling back to the subject
token's expiry when absent.

Wire OpenAICredentials into the BSP controller and the gateway's
filterapi translation, replacing the "not yet supported" errors, and
default the credential override header to x-aigw-openai-access-token.

Part of theagentrouter#2791.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
Run the BackendSecurityPolicy controller against a fake SPIFFE
Workload API and a fake token exchange endpoint, and check that the
JWT-SVID is exchanged and the access token lands in the generated
secret.

Part of theagentrouter#2791.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
Add an OpenAICredentials example using a SPIFFE JWT-SVID, and note in
the API docs that such policies act as the controller's SPIFFE
identity. Mounting the Workload API socket into the controller is left
to the deployment.

Part of theagentrouter#2791.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
Model OpenAICredentials on OpenAI Workload Identity Federation instead of
a generic RFC 8693 token exchange, mirroring the GCP credentials API:

- tokenExchange becomes workloadIdentityFederationConfig
  (OpenAIWorkloadIdentityFederationConfig) with required
  identityProviderID and serviceAccountID, and tokenURL defaulting to
  https://auth.openai.com/oauth/token.
- Drop audience, scopes and subjectTokenType, which OpenAI does not
  accept; the subject token is always sent as a JWT.
- Send the documented JSON body and surface OpenAI's error response
  ({"error": {"message", "code"}}) in the policy status, falling back
  to a truncated body.

https://developers.openai.com/api/reference/workload-identity-federation

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
Route OpenAI Workload Identity Federation token exchange requests
through the proxy in AI_GATEWAY_OPENAI_PROXY_URL when set, like the
existing AWS, Azure and GCP proxy variables. Without it, the default
transport's proxy settings (HTTPS_PROXY) still apply.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
Junfei Liu and others added 3 commits October 26, 2026 19:50
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
Adapt OpenAI rotator wiring to theagentrouter#2886, which made the pre-rotation
window a controller field.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Signed-off-by: Junfei Liu <jliu531@bloomberg.net>
@junfeiliu531
junfeiliu531 force-pushed the spiffe-token-provider-3 branch from 6f7cf8f to 63a7a6f Compare October 26, 2026 23:55
@aabchoo

aabchoo commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

/retest

@aabchoo

aabchoo commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

Thanks for addressing my requests + for the contribution!

@aabchoo
aabchoo enabled auto-merge (squash) October 27, 2026 19:13
@aabchoo
aabchoo disabled auto-merge October 27, 2026 19:24
@aabchoo
aabchoo merged commit 3b8f839 into theagentrouter:main Oct 27, 2026
88 of 94 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/controller Controller and reconciliation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Proposal: SPIFFE workload identity + token exchange for OpenAI backends

2 participants