Skip to content

Commit bc36676

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 1e72e79 commit bc36676

17 files changed

+976
-766
lines changed

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.
@@ -327,7 +328,7 @@ func (m *MCPRemoteProxy) GetNamespace() string {
327328

328329
// GetOIDCConfig returns the OIDC configuration reference
329330
func (m *MCPRemoteProxy) GetOIDCConfig() *OIDCConfigRef {
330-
return &m.Spec.OIDCConfig
331+
return m.Spec.OIDCConfig
331332
}
332333

333334
// 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
@@ -423,6 +423,9 @@ func setConfigurationInvalidCondition(proxy *mcpv1alpha1.MCPRemoteProxy, reason,
423423
// validateOIDCIssuerURL validates the OIDC issuer URL scheme.
424424
func (*MCPRemoteProxyReconciler) validateOIDCIssuerURL(proxy *mcpv1alpha1.MCPRemoteProxy) error {
425425
oidcConfig := proxy.Spec.OIDCConfig
426+
if oidcConfig == nil {
427+
return nil
428+
}
426429

427430
switch oidcConfig.Type {
428431
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: "remote URL must not be empty",
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: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -154,42 +154,11 @@ func (r *MCPRemoteProxyReconciler) buildEnvVarsForProxy(
154154
env = append(env, otelEnvVars...)
155155
}
156156

157-
// Add token exchange environment variables
158-
// Note: Embedded auth server env vars are added separately in deploymentForMCPRemoteProxy
159-
// to avoid duplicate API calls.
160-
if proxy.Spec.ExternalAuthConfigRef != nil {
161-
tokenExchangeEnvVars, err := ctrlutil.GenerateTokenExchangeEnvVars(
162-
ctx,
163-
r.Client,
164-
proxy.Namespace,
165-
proxy.Spec.ExternalAuthConfigRef,
166-
ctrlutil.GetExternalAuthConfigByName,
167-
)
168-
if err != nil {
169-
ctxLogger := log.FromContext(ctx)
170-
ctxLogger.Error(err, "Failed to generate token exchange environment variables")
171-
} else {
172-
env = append(env, tokenExchangeEnvVars...)
173-
}
174-
175-
// Add bearer token environment variables
176-
bearerTokenEnvVars, err := ctrlutil.GenerateBearerTokenEnvVar(
177-
ctx,
178-
r.Client,
179-
proxy.Namespace,
180-
proxy.Spec.ExternalAuthConfigRef,
181-
ctrlutil.GetExternalAuthConfigByName,
182-
)
183-
if err != nil {
184-
ctxLogger := log.FromContext(ctx)
185-
ctxLogger.Error(err, "Failed to generate bearer token environment variables")
186-
} else {
187-
env = append(env, bearerTokenEnvVars...)
188-
}
189-
}
157+
// Add token exchange and bearer token environment variables
158+
env = append(env, r.buildExternalAuthEnvVars(ctx, proxy)...)
190159

191160
// Add OIDC client secret environment variable if using inline config with secretRef
192-
if proxy.Spec.OIDCConfig.Type == "inline" && proxy.Spec.OIDCConfig.Inline != nil {
161+
if proxy.Spec.OIDCConfig != nil && proxy.Spec.OIDCConfig.Type == "inline" && proxy.Spec.OIDCConfig.Inline != nil {
193162
oidcClientSecretEnvVar, err := ctrlutil.GenerateOIDCClientSecretEnvVar(
194163
ctx, r.Client, proxy.Namespace, proxy.Spec.OIDCConfig.Inline.ClientSecretRef,
195164
)
@@ -228,6 +197,40 @@ func (r *MCPRemoteProxyReconciler) buildEnvVarsForProxy(
228197
return ctrlutil.EnsureRequiredEnvVars(ctx, env)
229198
}
230199

200+
// buildExternalAuthEnvVars builds environment variables for external auth (token exchange and bearer token).
201+
// Note: Embedded auth server env vars are added separately in deploymentForMCPRemoteProxy
202+
// to avoid duplicate API calls.
203+
func (r *MCPRemoteProxyReconciler) buildExternalAuthEnvVars(
204+
ctx context.Context, proxy *mcpv1alpha1.MCPRemoteProxy,
205+
) []corev1.EnvVar {
206+
if proxy.Spec.ExternalAuthConfigRef == nil {
207+
return nil
208+
}
209+
210+
ctxLogger := log.FromContext(ctx)
211+
var env []corev1.EnvVar
212+
213+
tokenExchangeEnvVars, err := ctrlutil.GenerateTokenExchangeEnvVars(
214+
ctx, r.Client, proxy.Namespace, proxy.Spec.ExternalAuthConfigRef, ctrlutil.GetExternalAuthConfigByName,
215+
)
216+
if err != nil {
217+
ctxLogger.Error(err, "Failed to generate token exchange environment variables")
218+
} else {
219+
env = append(env, tokenExchangeEnvVars...)
220+
}
221+
222+
bearerTokenEnvVars, err := ctrlutil.GenerateBearerTokenEnvVar(
223+
ctx, r.Client, proxy.Namespace, proxy.Spec.ExternalAuthConfigRef, ctrlutil.GetExternalAuthConfigByName,
224+
)
225+
if err != nil {
226+
ctxLogger.Error(err, "Failed to generate bearer token environment variables")
227+
} else {
228+
env = append(env, bearerTokenEnvVars...)
229+
}
230+
231+
return env
232+
}
233+
231234
// buildHeaderForwardSecretEnvVars builds environment variables for header forward secrets.
232235
// Each secret is mounted as an env var using Kubernetes SecretKeyRef, with a name following
233236
// the TOOLHIVE_SECRET_<identifier> pattern expected by the secrets.EnvironmentProvider.

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",
@@ -1064,7 +1108,7 @@ func TestValidateAndHandleConfigs(t *testing.T) {
10641108
Spec: mcpv1alpha1.MCPRemoteProxySpec{
10651109
RemoteURL: "https://mcp.example.com",
10661110
Port: 8080,
1067-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
1111+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
10681112
Type: mcpv1alpha1.OIDCConfigTypeInline,
10691113
Inline: &mcpv1alpha1.InlineOIDCConfig{
10701114
Issuer: "https://auth.example.com",
@@ -1100,7 +1144,7 @@ func TestValidateAndHandleConfigs(t *testing.T) {
11001144
Spec: mcpv1alpha1.MCPRemoteProxySpec{
11011145
RemoteURL: "https://mcp.example.com",
11021146
Port: 8080,
1103-
OIDCConfig: mcpv1alpha1.OIDCConfigRef{
1147+
OIDCConfig: &mcpv1alpha1.OIDCConfigRef{
11041148
Type: mcpv1alpha1.OIDCConfigTypeInline,
11051149
Inline: &mcpv1alpha1.InlineOIDCConfig{
11061150
Issuer: "https://auth.example.com",

0 commit comments

Comments
 (0)