diff --git a/README.md b/README.md index 0532ad5..846582d 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,8 @@ The chart exposes three ports: | `9091` | gRPC | Same API over gRPC | Yes — opt-out via `service.grpc.enabled: false` | | `8081` | HTTP | Prometheus `/metrics` endpoint | No — opt-in via `metrics.service.enabled: true` | +The MCP surface is served on the **main HTTP port**, not a port of its own: it must be publicly reachable on the same origin as the OAuth metadata clients discover it through, so it inherits the existing Ingress, CORS, security headers and rate limiting. Enable it with `authorizer.mcp_enabled: true` (requires `authorizer.authorizer_url`). + The metrics port is never added to the main `Service` used for Ingress. Use `metrics.service.enabled: true` to create a dedicated internal `ClusterIP` service for in-cluster scraping, or `metrics.serviceMonitor.enabled: true` for Prometheus Operator integration. ## Values Reference @@ -113,6 +115,7 @@ The metrics port is never added to the main `Service` used for Ingress. Use `met | `authorizer.metrics_port` | Dedicated Prometheus `/metrics` listen port (`--metrics-port`) | false | `8081` | | `authorizer.metrics_host` | Bind address for `/metrics` (`--metrics-host`). Use `0.0.0.0` for in-cluster scraping | false | `0.0.0.0` | | `authorizer.authorizer_url` | Public URL of this Authorizer deployment | false | — | +| `authorizer.mcp_enabled` | Serve the MCP tool surface at `POST /mcp` as an OAuth 2.1 resource server. **Requires `authorizer.authorizer_url`** — the chart fails at render time without it, because the server exits at boot | false | `false` | | `authorizer.reset_password_url` | Custom URL for password reset emails | false | — | | `authorizer.backchannel_logout_uri` | Back-channel logout URI | false | — | | `authorizer.custom_access_token_script` | JavaScript snippet (URL-encoded) injected at token issuance | false | — | diff --git a/templates/deployment.yaml b/templates/deployment.yaml index 7772a17..9cafd9f 100644 --- a/templates/deployment.yaml +++ b/templates/deployment.yaml @@ -13,6 +13,24 @@ port > 0, sender email set — so this never rejects a config the server accepts {{- fail "authorizer.enable_email_verification is true but SMTP is incomplete. Authorizer 2.4.0 exits at boot in this configuration (users would be created unverified with no way to ever verify), so the pod would CrashLoopBackOff. Set authorizer.smtp_host, authorizer.smtp_port and authorizer.smtp_sender_email, or set enable_email_verification: false." }} {{- end }} {{- end }} +{{/* +--mcp-enabled requires --url, and the server EXITS at boot without it rather +than starting in a degraded mode. + +The reason is not cosmetic: every token presented at /mcp is checked against +this deployment's canonical resource identifier, /mcp. With no --url that +identifier would be derived from request headers (X-Authorizer-URL, +X-Forwarded-Host, Host), which lets a caller name the audience their own token +must match — no check at all. So the server refuses the combination. + +Caught at render time for the same reason as the SMTP check above: in-cluster +the failure is a CrashLoopBackOff whose cause is one line in the container log. +*/}} +{{- if .Values.authorizer.mcp_enabled }} + {{- if not .Values.authorizer.authorizer_url }} + {{- fail "authorizer.mcp_enabled is true but authorizer.authorizer_url is not set. The MCP surface binds access tokens to /mcp, and deriving that identifier from request headers instead would let a caller choose their own token audience — so the server exits at boot in this configuration and the pod would CrashLoopBackOff. Set authorizer.authorizer_url (e.g. https://auth.example.com), or set mcp_enabled: false." }} + {{- end }} +{{- end }} apiVersion: apps/v1 kind: Deployment metadata: @@ -112,6 +130,7 @@ spec: --grpc-insecure="${GRPC_INSECURE:-false}" \ --grpc-tls-cert="${GRPC_TLS_CERT}" \ --grpc-tls-key="${GRPC_TLS_KEY}" \ + --mcp-enabled="${MCP_ENABLED:-false}" \ --rate-limit-rps="${RATE_LIMIT_RPS}" \ --rate-limit-burst="${RATE_LIMIT_BURST}" \ --rate-limit-fail-closed="${RATE_LIMIT_FAIL_CLOSED}" \ @@ -372,6 +391,12 @@ spec: value: {{ .Values.authorizer.grpc_port | default 9091 | toString | quote }} - name: "ENABLE_GRPC_REFLECTION" value: {{ include "authorizer.bool" (list .Values.authorizer.enable_grpc_reflection true) | quote }} + # MCP tool surface, served at POST /mcp on the main HTTP listener + # (not its own port — it must be publicly reachable on the same origin + # as the OAuth metadata clients discover it through). Requires + # authorizer_url; guarded at render time above. + - name: "MCP_ENABLED" + value: {{ include "authorizer.bool" (list .Values.authorizer.mcp_enabled false) | quote }} - name: "GRPC_INSECURE" value: {{ .Values.authorizer.grpc_insecure | default false | toString | quote }} {{- if .Values.authorizer.grpc_tls_cert }} diff --git a/values.yaml b/values.yaml index adffaf6..f90b92d 100644 --- a/values.yaml +++ b/values.yaml @@ -181,6 +181,16 @@ authorizer: # URL for authorizer deployment authorizer_url: null + # Serve the MCP tool surface over HTTP at POST /mcp, as an + # OAuth 2.1 resource server (RFC 9728 discovery, RFC 8707 audience-bound + # tokens). Off by default: it is a new internet-facing authenticated surface. + # + # REQUIRES authorizer_url. The chart fails at render time without it, because + # the server exits at boot — the audience every MCP token is checked against + # is derived from that URL, and deriving it from request headers instead would + # let a caller choose their own audience. + mcp_enabled: false + # Couchbase bucket. In case of couchbase database_type couchbase_bucket: null