Skip to content

Commit e7fec58

Browse files
aron-muonclaude
andcommitted
Make oidcConfig optional on MCPRemoteProxy for anonymous access
MCPRemoteProxy previously required oidcConfig, making it impossible to proxy public MCP servers without configuring authentication. Make the oidcConfig field optional — when omitted, the proxy allows anonymous access and forwards requests to the upstream without any authentication on either side. This enables "case 1" where both the upstream MCP and the proxy are fully public. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4ba2dcf commit e7fec58

13 files changed

Lines changed: 126 additions & 59 deletions

cmd/thv-operator/api/v1alpha1/mcpremoteproxy_types.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,11 @@ type MCPRemoteProxySpec struct {
5858
// +kubebuilder:default=streamable-http
5959
Transport string `json:"transport,omitempty"`
6060

61-
// OIDCConfig defines OIDC authentication configuration for the proxy
62-
// This validates incoming tokens from clients. Required for proxy mode.
63-
// +kubebuilder:validation:Required
64-
OIDCConfig OIDCConfigRef `json:"oidcConfig"`
61+
// OIDCConfig defines OIDC authentication configuration for the proxy.
62+
// When set, incoming requests are validated against the configured OIDC provider.
63+
// When omitted, the proxy allows anonymous access (no authentication required).
64+
// +optional
65+
OIDCConfig *OIDCConfigRef `json:"oidcConfig,omitempty"`
6566

6667
// ExternalAuthConfigRef references a MCPExternalAuthConfig resource for token exchange.
6768
// When specified, the proxy will exchange validated incoming tokens for remote service tokens.
@@ -312,7 +313,7 @@ func (m *MCPRemoteProxy) GetNamespace() string {
312313

313314
// GetOIDCConfig returns the OIDC configuration reference
314315
func (m *MCPRemoteProxy) GetOIDCConfig() *OIDCConfigRef {
315-
return &m.Spec.OIDCConfig
316+
return m.Spec.OIDCConfig
316317
}
317318

318319
// GetProxyPort returns the proxy port of the MCPRemoteProxy

cmd/thv-operator/api/v1alpha1/zz_generated.deepcopy.go

Lines changed: 5 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/thv-operator/controllers/mcpremoteproxy_controller.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,9 @@ func setConfigurationInvalidCondition(proxy *mcpv1alpha1.MCPRemoteProxy, reason,
394394
// validateOIDCIssuerURL validates the OIDC issuer URL scheme.
395395
func (*MCPRemoteProxyReconciler) validateOIDCIssuerURL(proxy *mcpv1alpha1.MCPRemoteProxy) error {
396396
oidcConfig := proxy.Spec.OIDCConfig
397+
if oidcConfig == nil {
398+
return nil
399+
}
397400

398401
switch oidcConfig.Type {
399402
case mcpv1alpha1.OIDCConfigTypeInline:

cmd/thv-operator/controllers/mcpremoteproxy_controller_test.go

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func TestMCPRemoteProxyValidateSpec(t *testing.T) {
5757
Spec: mcpv1alpha1.MCPRemoteProxySpec{
5858
RemoteURL: "https://mcp.salesforce.com",
5959
ProxyPort: 8080,
60-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
60+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
6161
Type: mcpv1alpha1.OIDCConfigTypeInline,
6262
Inline: &mcpv1alpha1.InlineOIDCConfig{
6363
Issuer: "https://login.salesforce.com",
@@ -77,7 +77,7 @@ func TestMCPRemoteProxyValidateSpec(t *testing.T) {
7777
},
7878
Spec: mcpv1alpha1.MCPRemoteProxySpec{
7979
ProxyPort: 8080,
80-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
80+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
8181
Type: mcpv1alpha1.OIDCConfigTypeInline,
8282
Inline: &mcpv1alpha1.InlineOIDCConfig{
8383
Issuer: "https://auth.example.com",
@@ -89,8 +89,20 @@ func TestMCPRemoteProxyValidateSpec(t *testing.T) {
8989
expectError: true,
9090
errContains: "remoteURL is required",
9191
},
92-
// Note: "missing OIDC config" test removed - OIDCConfig is now a required value type
93-
// with kubebuilder:validation:Required, so the API server prevents resources without it
92+
{
93+
name: "valid spec without OIDC config (anonymous access)",
94+
proxy: &mcpv1alpha1.MCPRemoteProxy{
95+
ObjectMeta: metav1.ObjectMeta{
96+
Name: "anon-proxy",
97+
Namespace: "default",
98+
},
99+
Spec: mcpv1alpha1.MCPRemoteProxySpec{
100+
RemoteURL: "https://docs.example.com/mcp",
101+
ProxyPort: 8080,
102+
},
103+
},
104+
expectError: false,
105+
},
94106
{
95107
name: "with valid external auth config",
96108
proxy: &mcpv1alpha1.MCPRemoteProxy{
@@ -101,7 +113,7 @@ func TestMCPRemoteProxyValidateSpec(t *testing.T) {
101113
Spec: mcpv1alpha1.MCPRemoteProxySpec{
102114
RemoteURL: "https://mcp.example.com",
103115
ProxyPort: 8080,
104-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
116+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
105117
Type: mcpv1alpha1.OIDCConfigTypeInline,
106118
Inline: &mcpv1alpha1.InlineOIDCConfig{
107119
Issuer: "https://auth.company.com",
@@ -159,7 +171,7 @@ func TestMCPRemoteProxyReconcile_CreateResources(t *testing.T) {
159171
Spec: mcpv1alpha1.MCPRemoteProxySpec{
160172
RemoteURL: "https://mcp.salesforce.com",
161173
ProxyPort: 8080,
162-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
174+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
163175
Type: mcpv1alpha1.OIDCConfigTypeInline,
164176
Inline: &mcpv1alpha1.InlineOIDCConfig{
165177
Issuer: "https://login.salesforce.com",

cmd/thv-operator/controllers/mcpremoteproxy_deployment.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ func (r *MCPRemoteProxyReconciler) buildEnvVarsForProxy(
189189
}
190190

191191
// Add OIDC client secret environment variable if using inline config with secretRef
192-
if proxy.Spec.OIDCConfig.Type == "inline" && proxy.Spec.OIDCConfig.Inline != nil {
192+
if proxy.Spec.OIDCConfig != nil && proxy.Spec.OIDCConfig.Type == "inline" && proxy.Spec.OIDCConfig.Inline != nil {
193193
oidcClientSecretEnvVar, err := ctrlutil.GenerateOIDCClientSecretEnvVar(
194194
ctx, r.Client, proxy.Namespace, proxy.Spec.OIDCConfig.Inline.ClientSecretRef,
195195
)

cmd/thv-operator/controllers/mcpremoteproxy_deployment_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -911,7 +911,7 @@ func TestBuildEnvVarsForProxy(t *testing.T) {
911911
},
912912
Spec: mcpv1alpha1.MCPRemoteProxySpec{
913913
RemoteURL: "https://mcp.example.com",
914-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
914+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
915915
Type: mcpv1alpha1.OIDCConfigTypeInline,
916916
Inline: &mcpv1alpha1.InlineOIDCConfig{
917917
Issuer: "https://auth.example.com",

cmd/thv-operator/controllers/mcpremoteproxy_reconciler_test.go

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,50 @@ func TestMCPRemoteProxyFullReconciliation(t *testing.T) {
5050
secret *corev1.Secret
5151
validateResult func(*testing.T, *mcpv1alpha1.MCPRemoteProxy, client.Client)
5252
}{
53+
{
54+
name: "anonymous proxy without OIDC config",
55+
proxy: &mcpv1alpha1.MCPRemoteProxy{
56+
ObjectMeta: metav1.ObjectMeta{
57+
Name: "anon-proxy",
58+
Namespace: "default",
59+
},
60+
Spec: mcpv1alpha1.MCPRemoteProxySpec{
61+
RemoteURL: "https://docs.example.com/mcp",
62+
Port: 8080,
63+
Transport: "streamable-http",
64+
},
65+
},
66+
validateResult: func(t *testing.T, proxy *mcpv1alpha1.MCPRemoteProxy, c client.Client) {
67+
t.Helper()
68+
69+
// Verify RunConfig ConfigMap created with no OIDC config
70+
cm := &corev1.ConfigMap{}
71+
err := c.Get(context.TODO(), types.NamespacedName{
72+
Name: fmt.Sprintf("%s-runconfig", proxy.Name),
73+
Namespace: proxy.Namespace,
74+
}, cm)
75+
assert.NoError(t, err, "RunConfig ConfigMap should be created")
76+
assert.Contains(t, cm.Data, "runconfig.json")
77+
// OIDC config should not be present in the RunConfig
78+
assert.NotContains(t, cm.Data["runconfig.json"], "oidc_issuer")
79+
80+
// Verify Deployment created
81+
dep := &appsv1.Deployment{}
82+
err = c.Get(context.TODO(), types.NamespacedName{
83+
Name: proxy.Name,
84+
Namespace: proxy.Namespace,
85+
}, dep)
86+
assert.NoError(t, err, "Deployment should be created")
87+
88+
// Verify Service created
89+
svc := &corev1.Service{}
90+
err = c.Get(context.TODO(), types.NamespacedName{
91+
Name: createProxyServiceName(proxy.Name),
92+
Namespace: proxy.Namespace,
93+
}, svc)
94+
assert.NoError(t, err, "Service should be created")
95+
},
96+
},
5397
{
5498
name: "basic proxy with inline OIDC",
5599
proxy: &mcpv1alpha1.MCPRemoteProxy{
@@ -61,7 +105,7 @@ func TestMCPRemoteProxyFullReconciliation(t *testing.T) {
61105
RemoteURL: "https://mcp.salesforce.com",
62106
Port: 8080,
63107
Transport: "streamable-http",
64-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
108+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
65109
Type: mcpv1alpha1.OIDCConfigTypeInline,
66110
Inline: &mcpv1alpha1.InlineOIDCConfig{
67111
Issuer: "https://login.salesforce.com",
@@ -134,7 +178,7 @@ func TestMCPRemoteProxyFullReconciliation(t *testing.T) {
134178
RemoteURL: "https://mcp.example.com",
135179
Port: 9090,
136180
Transport: "sse",
137-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
181+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
138182
Type: mcpv1alpha1.OIDCConfigTypeInline,
139183
Inline: &mcpv1alpha1.InlineOIDCConfig{
140184
Issuer: "https://auth.company.com",
@@ -248,7 +292,7 @@ func TestMCPRemoteProxyFullReconciliation(t *testing.T) {
248292
Spec: mcpv1alpha1.MCPRemoteProxySpec{
249293
RemoteURL: "https://mcp.example.com",
250294
Port: 8080,
251-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
295+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
252296
Type: mcpv1alpha1.OIDCConfigTypeInline,
253297
Inline: &mcpv1alpha1.InlineOIDCConfig{
254298
Issuer: "https://auth.example.com",
@@ -363,7 +407,7 @@ func TestMCPRemoteProxyConfigChangePropagation(t *testing.T) {
363407
Spec: mcpv1alpha1.MCPRemoteProxySpec{
364408
RemoteURL: "https://mcp.example.com",
365409
Port: 8080,
366-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
410+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
367411
Type: mcpv1alpha1.OIDCConfigTypeInline,
368412
Inline: &mcpv1alpha1.InlineOIDCConfig{
369413
Issuer: "https://auth.example.com",
@@ -444,7 +488,7 @@ func TestMCPRemoteProxyStatusProgression(t *testing.T) {
444488
Spec: mcpv1alpha1.MCPRemoteProxySpec{
445489
RemoteURL: "https://mcp.example.com",
446490
Port: 8080,
447-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
491+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
448492
Type: mcpv1alpha1.OIDCConfigTypeInline,
449493
Inline: &mcpv1alpha1.InlineOIDCConfig{
450494
Issuer: "https://auth.example.com",
@@ -617,7 +661,7 @@ func TestEnsureAuthzConfigMapShared(t *testing.T) {
617661
},
618662
Spec: mcpv1alpha1.MCPRemoteProxySpec{
619663
RemoteURL: "https://mcp.example.com",
620-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
664+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
621665
Type: mcpv1alpha1.OIDCConfigTypeInline,
622666
Inline: &mcpv1alpha1.InlineOIDCConfig{
623667
Issuer: "https://auth.example.com",
@@ -681,7 +725,7 @@ func TestRBACClientIntegration(t *testing.T) {
681725
},
682726
Spec: mcpv1alpha1.MCPRemoteProxySpec{
683727
RemoteURL: "https://mcp.example.com",
684-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
728+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
685729
Type: mcpv1alpha1.OIDCConfigTypeInline,
686730
Inline: &mcpv1alpha1.InlineOIDCConfig{
687731
Issuer: "https://auth.example.com",
@@ -813,7 +857,7 @@ func TestValidateSpecConfigurationConditions(t *testing.T) {
813857
ObjectMeta: metav1.ObjectMeta{Name: "http-oidc-proxy", Namespace: "default"},
814858
Spec: mcpv1alpha1.MCPRemoteProxySpec{
815859
RemoteURL: "https://mcp.example.com",
816-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
860+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
817861
Type: mcpv1alpha1.OIDCConfigTypeInline,
818862
Inline: &mcpv1alpha1.InlineOIDCConfig{
819863
Issuer: "http://insecure-idp.example.com",
@@ -833,7 +877,7 @@ func TestValidateSpecConfigurationConditions(t *testing.T) {
833877
ObjectMeta: metav1.ObjectMeta{Name: "http-insecure-proxy", Namespace: "default"},
834878
Spec: mcpv1alpha1.MCPRemoteProxySpec{
835879
RemoteURL: "https://mcp.example.com",
836-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
880+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
837881
Type: mcpv1alpha1.OIDCConfigTypeInline,
838882
Inline: &mcpv1alpha1.InlineOIDCConfig{
839883
Issuer: "http://dev-idp.example.com",
@@ -853,7 +897,7 @@ func TestValidateSpecConfigurationConditions(t *testing.T) {
853897
ObjectMeta: metav1.ObjectMeta{Name: "https-oidc-proxy", Namespace: "default"},
854898
Spec: mcpv1alpha1.MCPRemoteProxySpec{
855899
RemoteURL: "https://mcp.example.com",
856-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
900+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
857901
Type: mcpv1alpha1.OIDCConfigTypeInline,
858902
Inline: &mcpv1alpha1.InlineOIDCConfig{
859903
Issuer: "https://auth.example.com",
@@ -940,7 +984,7 @@ func TestValidateAndHandleConfigs(t *testing.T) {
940984
Spec: mcpv1alpha1.MCPRemoteProxySpec{
941985
RemoteURL: "https://mcp.example.com",
942986
Port: 8080,
943-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
987+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
944988
Type: mcpv1alpha1.OIDCConfigTypeInline,
945989
Inline: &mcpv1alpha1.InlineOIDCConfig{
946990
Issuer: "https://auth.example.com",
@@ -976,7 +1020,7 @@ func TestValidateAndHandleConfigs(t *testing.T) {
9761020
Spec: mcpv1alpha1.MCPRemoteProxySpec{
9771021
RemoteURL: "https://mcp.example.com",
9781022
Port: 8080,
979-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
1023+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
9801024
Type: mcpv1alpha1.OIDCConfigTypeInline,
9811025
Inline: &mcpv1alpha1.InlineOIDCConfig{
9821026
Issuer: "https://auth.example.com",

cmd/thv-operator/controllers/mcpremoteproxy_runconfig.go

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -140,23 +140,26 @@ func (r *MCPRemoteProxyReconciler) createRunConfigFromMCPRemoteProxy(
140140
return nil, fmt.Errorf("failed to process AuthzConfig: %w", err)
141141
}
142142

143-
// Add OIDC configuration (required for proxy mode)
144-
if err := ctrlutil.AddOIDCConfigOptions(ctx, r.Client, proxy, &options); err != nil {
145-
return nil, fmt.Errorf("failed to process OIDCConfig: %w", err)
143+
// Add OIDC configuration if specified (nil means anonymous access)
144+
if proxy.Spec.OIDCConfig != nil {
145+
if err := ctrlutil.AddOIDCConfigOptions(ctx, r.Client, proxy, &options); err != nil {
146+
return nil, fmt.Errorf("failed to process OIDCConfig: %w", err)
147+
}
146148
}
147149

148150
// Resolve OIDC config for embedded auth server configuration
149151
// ResourceURL provides AllowedAudiences, Scopes provides ScopesSupported
150-
// Note: Validation (OIDC config required) happens in AddExternalAuthConfigOptions
151152
var resolvedOIDCConfig *oidc.OIDCConfig
152-
resolver := oidc.NewResolver(r.Client)
153-
resolvedOIDCConfig, err := resolver.Resolve(ctx, proxy)
154-
if err != nil {
155-
return nil, fmt.Errorf("failed to resolve OIDC config: %w", err)
153+
if proxy.Spec.OIDCConfig != nil {
154+
resolver := oidc.NewResolver(r.Client)
155+
var err error
156+
resolvedOIDCConfig, err = resolver.Resolve(ctx, proxy)
157+
if err != nil {
158+
return nil, fmt.Errorf("failed to resolve OIDC config: %w", err)
159+
}
156160
}
157161

158-
// Add external auth configuration if specified (updated call)
159-
// Will fail if embedded auth server is used without OIDC config or resourceUrl
162+
// Add external auth configuration if specified
160163
if err := ctrlutil.AddExternalAuthConfigOptions(
161164
ctx, r.Client, proxy.Namespace, proxy.Name, proxy.Spec.ExternalAuthConfigRef,
162165
resolvedOIDCConfig, &options,

0 commit comments

Comments
 (0)