You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Reduce the MCP configuration to the four values only a deployer knows
What remains after this is resourceUrl, the OIDC configuration URL, the
client ID, and a Secret holding the client secret. Everything else is
derived, inherited, or defaulted.
**Secret paths follow the mount.** clientSecretFile and redis.passwordFile
had to equal the existing Secret's mount path plus a fixed filename -- the
chart failed to render otherwise, and the volume already hardcodes both
filenames. They are derived from the mount now, and the mount itself
defaults, so an existing-Secret deployment states neither. Three fail
blocks and their cross-checks go with them. A deployment supplying the
secret another way, such as Vault injection, still states
clientSecretFile, the one case where it is genuinely input.
**The Redis password is opt-in.** Deriving the password file from the
mount left it keyed on redisPasswordKey, defaulting to redis-password --
so every existing-Secret deployment carried that key whether or not its
Redis wanted a password, and one whose Redis needs none failed to mount a
key it had no reason to create. Naming the key is what opts in now.
**The access-token issuer is optional.** It was required even though only
a provider issuing v1-format access tokens needs it: every other issues
from the issuer its discovery document advertises, which is what FastMCP
defaults to. The verifier falls back to the discovery issuer.
**Authentication is not a mode.** An earlier commit in this stack removed
oidcProxy.enabled from values.yaml but left the template reading it, so it
fell through to its default of false. Every OIDC variable sits behind that
gate, which means a values file written to the documented contract
rendered with the whole auth block missing and the container failed at
start on absent configuration; the fixture only passed because it still
carried the flag. The gate, the environment variable and the flag are
gone, and the required messages name MCP rather than a proxy that could be
switched off.
**The MCP audience joins the provider that already exists.** Enabling MCP
required a gateway.envoy.jwt.providers entry carrying the resource URL as
its audience, and in every deployment that entry was a copy of one already
present -- same issuer, JWKS URI, claim and cluster -- differing only in
audience, because the relayed upstream token comes from the identity
provider already configured for this deployment's own clients. Envoy
accepts several audiences per provider, so the resource URL is appended to
the entry whose issuer matches. The issuer is derivable, since OpenID
Connect Discovery defines the configuration URL as the issuer plus
/.well-known/openid-configuration; accessTokenIssuer still overrides it.
The comparison ignores a trailing slash, and an entry already carrying the
audience does not get it twice. Failing to find a provider now names the
issuer rather than saying an entry is required, which was the error a
deployer hit after supplying one.
**The signing key is resolved.** The derived signing and storage keys are
only as strong as the client secret, and nothing checked it; the service
now refuses to start below 32 characters, which rejects a hand-written
placeholder. The HKDF derivation is pinned locally rather than calling
fastmcp.server.auth.jwt_issuer.derive_jwt_key, because an upstream change
to that derivation would silently make every stored registration and token
undecryptable; the bytes are identical today and test_auth asserts that
equivalence against FastMCP so a divergence fails the build. The
production-readiness warning telling operators to assess the derived key
is replaced by that entropy requirement -- it does not survive reading
FastMCP 3.4.7, where high-entropy material goes through HKDF and the
PBKDF2 path with a length warning is reserved for low-entropy strings.
**mcp:Access is retired.** The action mapped to exactly one path, /mcp,
where ext_authz is disabled -- so it was never evaluated, and a role
granting it implied an access control that did not run. The wholesale gate
is the identity-provider scope: JWTVerifier rejects a token without it, so
a caller cannot reach the protocol endpoint. Per-operation authorization
is unchanged, because every tool call reaches OSMO through /api where
gateway JWT validation and semantic RBAC apply as they do for the CLI and
UI. resourceTypeMCP and ResourceTypeMCP go too. Existing deployments need
no migration: IsValidAction has no callers, so a role row still granting
mcp:Access keeps an action matching no path, which is already true today.
Rendering the proxy fixture is byte-identical apart from the removed
enable switch. Each change is covered by a render assertion and
negative-tested by reintroducing the defect it guards.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
{{- $mcpResourceUrl= include "osmo.mcp-resource-url". }}
52
-
{{- ifnot$envoy.jwt.providers }}
53
-
{{- fail "services.mcp.enabled requires at least one gateway.envoy.jwt.providers entry" }}
53
+
{{- /*
54
+
The relayed upstream token carries the MCP resource URL as its audience, so the
55
+
Gateway has to accept that audience from the identity provider MCP authenticates
56
+
against. That provider is already configured for this deployment's own clients,
57
+
and differs only in audience -- so the audience is appended to it rather than
58
+
asking for a second, near-identical entry.
59
+
60
+
OpenID Connect Discovery defines the configuration URL as the issuer followed by
61
+
/.well-known/openid-configuration, so the issuer is derivable. accessTokenIssuer
62
+
overrides it for providers whose access tokens are issued elsewhere, as an
63
+
application configured for v1-format tokens does.
64
+
*/ -}}
65
+
{{- $mcpTokenIssuer=$mcpOidcProxy.oidc.accessTokenIssuer| default (trimSuffix "/.well-known/openid-configuration" (required "services.mcp.oidcProxy.oidc.configUrl is required when MCP is enabled"$mcpOidcProxy.oidc.configUrl)) }}
{{- fail (printf"services.mcp.enabled requires a gateway.envoy.jwt.providers entry with issuer %s, which is where MCP's relayed tokens come from"$mcpTokenIssuer) }}
54
75
{{- end }}
55
76
{{- $mcpServiceName:= required "services.mcp.serviceName is required when MCP is enabled"$mcp.serviceName }}
56
77
{{- $mcpImageName:= required "services.mcp.imageName is required when MCP is enabled"$mcp.imageName }}
{{- $oidcConfigUrl = required "services.mcp.oidcProxy.oidc.configUrl is required when the OIDC proxy is enabled" $oidcProxy.oidc.configUrl }}
37
-
{{- $oidcClientId = required "services.mcp.oidcProxy.oidc.clientId is required when the OIDC proxy is enabled" $oidcProxy.oidc.clientId }}
38
-
{{- $oidcClientSecretFile = required "services.mcp.oidcProxy.oidc.clientSecretFile is required when the OIDC proxy is enabled" $oidcProxy.oidc.clientSecretFile }}
39
-
{{- $oidcAccessTokenIssuer = required "services.mcp.oidcProxy.oidc.accessTokenIssuer is required when the OIDC proxy is enabled" $oidcProxy.oidc.accessTokenIssuer }}
40
-
{{- $oidcAccessTokenRequiredScope = required "services.mcp.oidcProxy.oidc.accessTokenRequiredScope is required when the OIDC proxy is enabled" $oidcProxy.oidc.accessTokenRequiredScope }}
36
+
{{- $oidcConfigUrl = required "services.mcp.oidcProxy.oidc.configUrl is required when MCP is enabled" $oidcProxy.oidc.configUrl }}
37
+
{{- $oidcClientId = required "services.mcp.oidcProxy.oidc.clientId is required when MCP is enabled" $oidcProxy.oidc.clientId }}
{{- $oidcAccessTokenRequiredScope = required "services.mcp.oidcProxy.oidc.accessTokenRequiredScope is required when MCP is enabled" $oidcProxy.oidc.accessTokenRequiredScope }}
41
40
{{- if not (regexMatch "^[A-Za-z0-9:._~-]{1,128}$" $oidcAccessTokenRequiredScope) }}
42
41
{{- fail "services.mcp.oidcProxy.oidc.accessTokenRequiredScope must be one non-empty scope" }}
{{- if not (hasPrefix "/" $oidcClientSecretFile) }}
61
-
{{- fail "services.mcp.oidcProxy.oidc.clientSecretFile must be an absolute path" }}
62
-
{{- end }}
63
63
{{- $redisHost := .Values.services.redis.serviceName | required "services.redis.serviceName is required when the MCP OIDC proxy is enabled" }}
64
64
{{- $redisPort := .Values.services.redis.port }}
65
65
{{- if or (lt (int $oidcProxy.redis.dbNumber) 0) (gt (int $oidcProxy.redis.dbNumber) 15) }}
@@ -88,25 +88,23 @@
88
88
{{- fail "services.mcp.oidcProxy.upstreamTimeoutSeconds must be between 1 and 60" }}
89
89
{{- end }}
90
90
{{- if $oidcProxy.existingSecret.name }}
91
-
{{- $mountPath := required "services.mcp.oidcProxy.existingSecret.mountPath is required when an existing Secret is configured" $oidcProxy.existingSecret.mountPath }}
{{- if and $oidcProxy.redis.passwordFile (not $oidcProxy.existingSecret.redisPasswordKey) }}
104
-
{{- fail "services.mcp.oidcProxy.existingSecret.redisPasswordKey is required when redis.passwordFile is configured" }}
99
+
{{- else }}
100
+
{{- $oidcClientSecretFile = required "services.mcp.oidcProxy.oidc.clientSecretFile is required when no existingSecret is configured" $oidcProxy.oidc.clientSecretFile }}
0 commit comments