Bug description
When an authServerConfig.upstreamProviders[] entry points at an upstream IdP served over HTTPS with a private/internal CA (e.g. an in-cluster Dex behind a cert-manager-issued cert), vMCP's embedded auth server fails the authorization-code exchange with:
tls: failed to verify certificate: x509: certificate signed by unknown authority
Neither upstreamProviders[].oauth2Config nor .oidcConfig has a field to supply a trusted CA bundle for the outbound HTTP client vMCP uses to talk to that upstream's authorizationEndpoint/tokenEndpoint (or OIDC-discovered equivalents). Confirmed via kubectl explain on both:
virtualmcpserver.spec.authServerConfig.upstreamProviders.oauth2Config
virtualmcpserver.spec.authServerConfig.upstreamProviders.oidcConfig
— neither lists a caBundle/caBundleRef-shaped field.
This is asymmetric within the same CRD: MCPOIDCConfig (used for incoming token validation on incomingAuth.oidcConfigRef) does support caBundleRef. Only the outbound leg — vMCP acting as an OAuth client against the upstream provider — has no equivalent.
Reproduction
- ToolHive
v0.44.0
- In-cluster Dex serving HTTPS via a cert-manager-issued certificate, private CA
VirtualMCPServer with:
authServerConfig:
upstreamProviders:
- name: dex
type: oauth2
oauth2Config:
authorizationEndpoint: https://dex.<ns>.svc.cluster.local:5556/auth
tokenEndpoint: https://dex.<ns>.svc.cluster.local:5556/token
allowPrivateIPs: true
...
incomingAuth.oidcConfigRef → an MCPOIDCConfig with caBundleRef set to the same CA — this part validates incoming tokens fine.
- Drive a full PKCE browser-style login (DCR →
/oauth/authorize → Dex login → callback). The redirect to Dex and the login itself succeed (TLS validates against the CA fine from any client that's given it explicitly). vMCP's own callback handler then fails when it calls Dex's /token endpoint server-to-server, because that HTTP client has no CA configured and falls back to the system root store only.
Expected behavior
oauth2Config/oidcConfig (or a shared parent struct) should accept a CA bundle reference (mirroring MCPOIDCConfig.caBundleRef) that scopes trust to that upstream provider's outbound calls, without requiring a process-wide SSL_CERT_FILE override on the vMCP pod.
Current workaround
Setting SSL_CERT_FILE in the vMCP pod's env to a mounted CA file makes Go's x509.SystemCertPool() pick it up for all outbound HTTPS calls the process makes — this works, but it's a full override (not additive to the system trust store), scoped to the whole process rather than the specific upstream, and undocumented as the sanctioned path for this. stacklok-enterprise-platform's connector-gateway (built on the same vMCP core) uses this exact same workaround for its own private-CA upstream IdP.
Scope
- Only affects
type: oauth2/type: oidc upstream providers with a private CA; public-CA upstreams (real Okta/Entra/etc.) are unaffected.
- Does not affect
incomingAuth token validation (MCPOIDCConfig.caBundleRef already covers that).
Bug description
When an
authServerConfig.upstreamProviders[]entry points at an upstream IdP served over HTTPS with a private/internal CA (e.g. an in-cluster Dex behind a cert-manager-issued cert), vMCP's embedded auth server fails the authorization-code exchange with:Neither
upstreamProviders[].oauth2Confignor.oidcConfighas a field to supply a trusted CA bundle for the outbound HTTP client vMCP uses to talk to that upstream'sauthorizationEndpoint/tokenEndpoint(or OIDC-discovered equivalents). Confirmed viakubectl explainon both:— neither lists a
caBundle/caBundleRef-shaped field.This is asymmetric within the same CRD:
MCPOIDCConfig(used for incoming token validation onincomingAuth.oidcConfigRef) does supportcaBundleRef. Only the outbound leg — vMCP acting as an OAuth client against the upstream provider — has no equivalent.Reproduction
v0.44.0VirtualMCPServerwith:incomingAuth.oidcConfigRef→ anMCPOIDCConfigwithcaBundleRefset to the same CA — this part validates incoming tokens fine./oauth/authorize→ Dex login → callback). The redirect to Dex and the login itself succeed (TLS validates against the CA fine from any client that's given it explicitly). vMCP's own callback handler then fails when it calls Dex's/tokenendpoint server-to-server, because that HTTP client has no CA configured and falls back to the system root store only.Expected behavior
oauth2Config/oidcConfig(or a shared parent struct) should accept a CA bundle reference (mirroringMCPOIDCConfig.caBundleRef) that scopes trust to that upstream provider's outbound calls, without requiring a process-wideSSL_CERT_FILEoverride on the vMCP pod.Current workaround
Setting
SSL_CERT_FILEin the vMCP pod's env to a mounted CA file makes Go'sx509.SystemCertPool()pick it up for all outbound HTTPS calls the process makes — this works, but it's a full override (not additive to the system trust store), scoped to the whole process rather than the specific upstream, and undocumented as the sanctioned path for this.stacklok-enterprise-platform'sconnector-gateway(built on the same vMCP core) uses this exact same workaround for its own private-CA upstream IdP.Scope
type: oauth2/type: oidcupstream providers with a private CA; public-CA upstreams (real Okta/Entra/etc.) are unaffected.incomingAuthtoken validation (MCPOIDCConfig.caBundleRefalready covers that).