Repository navigation
feat: add OpenAICredentials with workload identity federation - #2815
Merged
aabchoo merged 17 commits intoOct 27, 2026
Merged
Conversation
✅ Deploy Preview for theagentrouter ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
aabchoo
approved these changes
Oct 19, 2026
aabchoo
left a comment
Contributor
There was a problem hiding this comment.
Just minor comments but everything looks good to me!
Contributor
|
/retest |
aabchoo
reviewed
Oct 19, 2026
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
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>
junfeiliu531
commented
Oct 26, 2026
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>
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
force-pushed
the
spiffe-token-provider-3
branch
from
October 26, 2026 23:55
6f7cf8f to
63a7a6f
Compare
Contributor
|
/retest |
aabchoo
approved these changes
Oct 27, 2026
Contributor
|
Thanks for addressing my requests + for the contribution! |
aabchoo
enabled auto-merge (squash)
October 27, 2026 19:13
aabchoo
disabled auto-merge
October 27, 2026 19:24
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Fixes #2791: secretless access to OpenAI using OpenAI Workload Identity Federation. A new
OpenAICredentialsBackendSecurityPolicy 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)workloadIdentityFederationConfig(OpenAIWorkloadIdentityFederationConfig) mirrorsgcpCredentials.workloadIdentityFederationConfig. It holds the parameters of OpenAI's token exchange request:identityProviderIDandserviceAccountIDare sent asidentity_provider_idandservice_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 asurn:ietf:params:oauth:token-type:jwt, and anoidcExchangeTokenprovider must return a JWT.oidcExchangeTokenreusesBackendSecurityPolicyOIDC, so the OIDC option has the same name and shape as inAWSCredentials,AzureCredentialsandGCPCredentials.spiffeJWTSVIDis 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 (unlikeoidcExchangeToken, 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.tokenURLfield: the controller always exchanges athttps://auth.openai.com/oauth/token, so a policy cannot make the controller send subject tokens to an arbitrary URL.audienceorscopeparameters: OpenAI's exchange does not accept them; permissions come from the service account mapping configured in OpenAI.subjectToken;socketPathmust beunix://ortcp://;openAICredentialsis mutually exclusive with the other credential fields.CredentialOverrideprecedent.Controller (
internal/controller)tokenprovider.NewSPIFFETokenProvider: aTokenProviderbacked 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 toSPIFFE_ENDPOINT_SOCKET.tokenprovider.NewOpenAIWorkloadIdentityTokenProviderwraps any subjectTokenProviderand sends OpenAI's documented JSON request body. The expiry comes fromexpires_at(Unix seconds) in the response; a response without it is rejected.{"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_URLroutes token exchange requests through a proxy, likeAI_GATEWAY_STS_PROXY_URL,AI_GATEWAY_AZURE_PROXY_URLandAI_GATEWAY_GCP_AUTH_PROXY_URL. When unset, the default transport's proxy settings (HTTPS_PROXY) still apply. Documented in the controllerextraEnvVarsexample in the Helm values.rotators.NewOpenAITokenRotatorstores the access token underopenAIAccessTokenin the generatedai-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.subjectToken.spiffeJWTSVIDorsubjectToken.oidcExchangeToken. OIDC client secrets referenced from other namespaces go through the same ReferenceGrant check as AWS/Azure/GCP.filterapi.OpenAIAuth.credentialOverridedefaults to thex-aigw-openai-access-tokenheader.Data plane (
internal/backendauth)OpenAIAuthhandler setsAuthorization: Bearer <token>, plusOpenAI-OrganizationandOpenAI-Projectwhen 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)
SPIFFE_ENDPOINT_SOCKETpointing at it) is left to the deployment. The identity provider and service account mapping must be configured in the OpenAI organization.tests/controllerruns 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.POST /v1/chat/completions, it was rotated before expiry, and a policy with the wrong SVID audience reported OpenAI'sinvalid_subject_tokenerror. The data plane path (gateway injecting the token) was not exercised on that cluster.spiffeJWTSVIDoption next to the existingoidcExchangeTokenin AWS, Azure and GCP, without deprecating anything.🤖 Generated with Claude Code [1]
1: https://claude.com/claude-code