Skip to content

OIDC configuration fields not properly applied (thvCABundlePath, protectedResourceAllowPrivateIP) #3142

@danbarr

Description

@danbarr

Problem

Multiple OIDC configuration fields in MCPServer and VirtualMCPServer CRDs are defined but never actually used, preventing secure OIDC authentication in several scenarios:

  1. thvCABundlePath: Not applied to token validator, preventing connections to OIDC providers with self-signed or internal CA certificates
  2. protectedResourceAllowPrivateIP: 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 thvCABundlePath is 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:

  1. Setting protectedResourceAllowPrivateIP: true in the CRD has NO EFFECT (field is never read from CRD)
  2. Setting jwksAllowPrivateIP: true INCORRECTLY enables private IPs for the protected resource endpoint (wrong field mapping)
  3. 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 InlineOIDCConfig structure 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,
}, nil

Bug 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:

  1. ✅ CRD Definition: cmd/thv-operator/api/v1alpha1/mcpserver_types.go:495
  2. ✅ OIDC Resolver: cmd/thv-operator/pkg/oidc/resolver.go:187,228
  3. ✅ Config Builder: cmd/thv-operator/pkg/controllerutil/oidc.go:42
  4. ✅ RunConfig Storage: pkg/runner/config_builder.go:345-346
  5. TokenValidatorConfig: NOT mapped (this is the bug)

For protectedResourceAllowPrivateIP:

  1. ✅ CRD Definition: cmd/thv-operator/api/v1alpha1/mcpserver_types.go:508-512
  2. Resolver struct: Field missing (bug)
  3. Resolver copy: Field not copied (bug)
  4. Converter: Wrong field mapped (bug)
  5. ✅ 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
}, nil

Also 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.go
    • pkg/auth/token.go
    • cmd/thv-operator/api/v1alpha1/mcpserver_types.go
    • cmd/thv-operator/pkg/oidc/resolver.go
    • cmd/thv-operator/pkg/vmcpconfig/converter.go

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions