-
Notifications
You must be signed in to change notification settings - Fork 160
Description
Problem
Multiple OIDC configuration fields in MCPServer and VirtualMCPServer CRDs are defined but never actually used, preventing secure OIDC authentication in several scenarios:
thvCABundlePath: Not applied to token validator, preventing connections to OIDC providers with self-signed or internal CA certificatesprotectedResourceAllowPrivateIP: Not propagated through resolver, and incorrectly mapped from wrong source field
Impact
Impact of thvCABundlePath Bug
When using MCPServer or VirtualMCPServer with OIDC authentication against a provider with a non-publicly-trusted certificate (e.g., Keycloak with cert-manager internal cluster issuer), TLS verification fails even when:
- The CA certificate is mounted into the pod
- The
thvCABundlePathis correctly configured in the CRD
This affects:
- JWKS fetching
- OIDC discovery
- Token introspection
All OIDC HTTP connections will fail with TLS verification errors.
Impact of protectedResourceAllowPrivateIP Bug
The protectedResourceAllowPrivateIP field has multiple issues:
- Setting
protectedResourceAllowPrivateIP: truein the CRD has NO EFFECT (field is never read from CRD) - Setting
jwksAllowPrivateIP: trueINCORRECTLY enables private IPs for the protected resource endpoint (wrong field mapping) - There is NO WAY to independently control JWKS private IP allowance vs protected resource private IP allowance
This prevents proper security configuration when JWKS and protected resource endpoints have different network accessibility requirements.
Affected Resources
Both MCPServer and VirtualMCPServer are affected because they:
- Share identical
InlineOIDCConfigstructure definitions - Use the same OIDC resolver code path (
cmd/thv-operator/pkg/oidc/resolver.go) - Call the same
runner.WithOIDCConfig()function where the bugs exist
Root Causes
Bug 1: thvCABundlePath Not Applied to TokenValidatorConfig
Location: pkg/runner/config_builder.go lines 331-341
When creating the TokenValidatorConfig, the CACertPath and AuthTokenFile fields are not populated:
b.config.OIDCConfig = &auth.TokenValidatorConfig{
Issuer: oidcIssuer,
Audience: oidcAudience,
JWKSURL: oidcJwksURL,
IntrospectionURL: oidcIntrospectionURL,
ClientID: oidcClientID,
ClientSecret: oidcClientSecret,
AllowPrivateIP: jwksAllowPrivateIP,
InsecureAllowHTTP: insecureAllowHTTP,
Scopes: scopes,
// MISSING: CACertPath and AuthTokenFile are not set here!
}The values are stored on RunConfig.ThvCABundle (line 345) and RunConfig.JWKSAuthTokenFile (line 346) but never passed to the OIDCConfig structure that's actually used by the token validator.
Bug 2: protectedResourceAllowPrivateIP Missing from Resolver
Location 1: cmd/thv-operator/pkg/oidc/resolver.go lines 27-40
The OIDCConfig struct is missing the ProtectedResourceAllowPrivateIP field:
type OIDCConfig struct {
Issuer string
Audience string
JWKSURL string
IntrospectionURL string
ClientID string
ClientSecret string
ThvCABundlePath string
JWKSAuthTokenPath string
ResourceURL string
JWKSAllowPrivateIP bool
InsecureAllowHTTP bool
Scopes []string
// MISSING: ProtectedResourceAllowPrivateIP bool
}Location 2: cmd/thv-operator/pkg/oidc/resolver.go line 234
The resolveInlineConfig() function doesn't copy the field:
return &OIDCConfig{
Issuer: config.Issuer,
Audience: config.Audience,
JWKSURL: config.JWKSURL,
IntrospectionURL: config.IntrospectionURL,
ClientID: config.ClientID,
ClientSecret: clientSecret,
ThvCABundlePath: config.ThvCABundlePath,
JWKSAuthTokenPath: config.JWKSAuthTokenPath,
ResourceURL: resourceURL,
JWKSAllowPrivateIP: config.JWKSAllowPrivateIP,
InsecureAllowHTTP: config.InsecureAllowHTTP,
Scopes: config.Scopes,
// MISSING: ProtectedResourceAllowPrivateIP: config.ProtectedResourceAllowPrivateIP,
}, nilBug 3: Wrong Field Mapped in Converter
Location: cmd/thv-operator/pkg/vmcpconfig/converter.go line 206
The converter maps the wrong source field:
config := &vmcpconfig.OIDCConfig{
Issuer: resolved.Issuer,
ClientID: resolved.ClientID,
Audience: resolved.Audience,
Resource: resolved.ResourceURL,
ProtectedResourceAllowPrivateIP: resolved.JWKSAllowPrivateIP, // BUG: Wrong source!
InsecureAllowHTTP: resolved.InsecureAllowHTTP,
Scopes: resolved.Scopes,
}This should use resolved.ProtectedResourceAllowPrivateIP (once that field exists).
Evidence from Tests
Evidence for thvCABundlePath Bug
The test files explicitly acknowledge this bug with comments:
File: cmd/thv-operator/controllers/mcpserver_runconfig_test.go lines 407-408, 810-811:
// NOTE: CACertPath and AuthTokenFile are not currently mapped in WithOIDCConfig function
// This is likely a bug that should be fixed separately
assert.Equal(t, "", runConfig.OIDCConfig.CACertPath)
assert.Equal(t, "", runConfig.OIDCConfig.AuthTokenFile)Evidence for protectedResourceAllowPrivateIP Bug
File: cmd/thv-operator/pkg/vmcpconfig/converter_test.go lines 83-94:
The test expects ProtectedResourceAllowPrivateIP to be set when JWKSAllowPrivateIP is true (testing the incorrect behavior):
mockReturn: &oidc.OIDCConfig{
Issuer: "https://issuer.example.com", Audience: "my-audience",
ResourceURL: "https://resource.example.com", JWKSAllowPrivateIP: true,
},
validate: func(t *testing.T, config *vmcpconfig.Config, err error) {
require.NoError(t, err)
assert.True(t, config.IncomingAuth.OIDC.ProtectedResourceAllowPrivateIP) // Wrong!
},Configuration Flow
The fields ARE correctly defined and partially propagated:
For thvCABundlePath:
- ✅ CRD Definition:
cmd/thv-operator/api/v1alpha1/mcpserver_types.go:495 - ✅ OIDC Resolver:
cmd/thv-operator/pkg/oidc/resolver.go:187,228 - ✅ Config Builder:
cmd/thv-operator/pkg/controllerutil/oidc.go:42 - ✅ RunConfig Storage:
pkg/runner/config_builder.go:345-346 - ❌ TokenValidatorConfig: NOT mapped (this is the bug)
For protectedResourceAllowPrivateIP:
- ✅ CRD Definition:
cmd/thv-operator/api/v1alpha1/mcpserver_types.go:508-512 - ❌ Resolver struct: Field missing (bug)
- ❌ Resolver copy: Field not copied (bug)
- ❌ Converter: Wrong field mapped (bug)
- ✅ Used correctly in:
pkg/vmcp/auth/factory/incoming.go:76-84(but never receives correct value)
Proposed Fixes
Fix 1: Map thvCABundlePath to TokenValidatorConfig
Update pkg/runner/config_builder.go lines 331-341:
b.config.OIDCConfig = &auth.TokenValidatorConfig{
Issuer: oidcIssuer,
Audience: oidcAudience,
JWKSURL: oidcJwksURL,
IntrospectionURL: oidcIntrospectionURL,
ClientID: oidcClientID,
ClientSecret: oidcClientSecret,
CACertPath: thvCABundle, // ADD THIS
AuthTokenFile: jwksAuthTokenFile, // ADD THIS
AllowPrivateIP: jwksAllowPrivateIP,
InsecureAllowHTTP: insecureAllowHTTP,
Scopes: scopes,
}Fix 2: Add protectedResourceAllowPrivateIP to Resolver
Update cmd/thv-operator/pkg/oidc/resolver.go line 40:
type OIDCConfig struct {
Issuer string
Audience string
JWKSURL string
IntrospectionURL string
ClientID string
ClientSecret string
ThvCABundlePath string
JWKSAuthTokenPath string
ResourceURL string
JWKSAllowPrivateIP bool
ProtectedResourceAllowPrivateIP bool // ADD THIS
InsecureAllowHTTP bool
Scopes []string
}Fix 3: Copy Field in resolveInlineConfig
Update cmd/thv-operator/pkg/oidc/resolver.go line 234:
return &OIDCConfig{
// ... existing fields ...
ProtectedResourceAllowPrivateIP: config.ProtectedResourceAllowPrivateIP, // ADD THIS
}, nilAlso update resolveConfigMapConfig() around line 187 similarly.
Fix 4: Fix Converter Mapping
Update cmd/thv-operator/pkg/vmcpconfig/converter.go line 206:
config := &vmcpconfig.OIDCConfig{
Issuer: resolved.Issuer,
ClientID: resolved.ClientID,
Audience: resolved.Audience,
Resource: resolved.ResourceURL,
ProtectedResourceAllowPrivateIP: resolved.ProtectedResourceAllowPrivateIP, // FIX THIS
InsecureAllowHTTP: resolved.InsecureAllowHTTP,
Scopes: resolved.Scopes,
}Test Updates Required
Update MCPServer/VirtualMCPServer RunConfig Tests
In cmd/thv-operator/controllers/mcpserver_runconfig_test.go:
// Remove the bug acknowledgment comments and assert proper values:
assert.Equal(t, "/etc/ssl/ca-bundle.pem", runConfig.OIDCConfig.CACertPath)
assert.Equal(t, "/path/to/token", runConfig.OIDCConfig.AuthTokenFile)Update Converter Test
In cmd/thv-operator/pkg/vmcpconfig/converter_test.go, fix the test to use the correct field:
mockReturn: &oidc.OIDCConfig{
Issuer: "https://issuer.example.com",
Audience: "my-audience",
ResourceURL: "https://resource.example.com",
ProtectedResourceAllowPrivateIP: true, // FIX: Use correct field
},Workarounds
For thvCABundlePath
Currently, there is no good workaround except:
- Using
insecureAllowHTTP: true(not recommended for production) - Using a publicly-trusted certificate for your OIDC provider
For protectedResourceAllowPrivateIP
Currently, setting jwksAllowPrivateIP: true will incorrectly also allow private IPs for the protected resource endpoint. There is no way to control these independently.
Environment
- Components: MCPServer, VirtualMCPServer, vmcp
- Affected versions: All versions with OIDC support
- Related files:
pkg/runner/config_builder.gopkg/auth/token.gocmd/thv-operator/api/v1alpha1/mcpserver_types.gocmd/thv-operator/pkg/oidc/resolver.gocmd/thv-operator/pkg/vmcpconfig/converter.go