From 3b02e1b4833a3e4e4a6f34a14c9395311fc5d5f2 Mon Sep 17 00:00:00 2001 From: Pranay Sanghvi Date: Wed, 30 Sep 2026 12:48:45 +0530 Subject: [PATCH] docs: document MCP per-user authentication --- docs/getting-started/install-mcp.md | 104 ++++++++++++++++++++++++++-- docs/reference/mcp.md | 54 +++++++++++++-- 2 files changed, 145 insertions(+), 13 deletions(-) diff --git a/docs/getting-started/install-mcp.md b/docs/getting-started/install-mcp.md index 4518ca1a79..6c67ea41a2 100644 --- a/docs/getting-started/install-mcp.md +++ b/docs/getting-started/install-mcp.md @@ -34,6 +34,8 @@ capabilities through CLI commands, with nothing to deploy. will not work against earlier releases. - `kubectl` and the [`epinio` CLI](./install-cli.md) pointed at your cluster. - `make` and a Go toolchain, plus a clone of [epinio/mcp](https://github.com/epinio/mcp). +- A route for the MCP server. OAuth clients outside the cluster require a + publicly reachable HTTPS URL. ## Choose an install path @@ -57,24 +59,99 @@ This targets the `mcp` namespace (creating it if needed), pushes the server, and smoke-tests `/healthz` and `/readyz`. Override the namespace with `make setup NAMESPACE=`, and run `make help` to see every target. -`epinio.yml` carries the connection details. Fill in the `environment` section -with your cluster's API URL and credentials (default `admin` / `password`): +`epinio.yml` carries the connection and OAuth discovery details. Fill in the +`environment` section: ```yaml environment: EPINIO_API_URL: "https://epinio.your-cluster.example.com" - EPINIO_USERNAME: "admin" - EPINIO_PASSWORD: "your-password" + EPINIO_MCP_RESOURCE_URL: "https://epinio-mcp.your-cluster.example.com" + EPINIO_MCP_OIDC_ISSUER: "https://auth.your-cluster.example.com" ``` -For OIDC clusters, leave the username and password empty and set `EPINIO_TOKEN`, -`EPINIO_REFRESH_TOKEN`, and `EPINIO_TOKEN_ENDPOINT` instead. +`EPINIO_MCP_RESOURCE_URL` must exactly match the URL entered in the MCP client, +including any path. `EPINIO_MCP_OIDC_ISSUER` is the issuer reported by Dex's +OpenID Connect discovery document. Both values are required. + +The server does not store an Epinio username, password, access token, or refresh +token. Every MCP request must carry credentials that Epinio accepts. Tools +therefore run with the calling user's permissions instead of a shared server +identity. The push runs the full build cycle (upload source, stage, deploy, wait for ready) and assigns a route, for example `https://epinio-mcp.192.168.X.X.sslip.io`. The MCP endpoint is that route's root — point your agent at the URL as-is (no `/mcp` suffix). +## Configure Dex for OAuth clients + +OAuth-capable MCP clients discover Dex through the MCP server, open the Dex +login page, and return an access token to the server. Dex requires each client +and its exact callback URI to be registered in the `dex-config` Secret. + +For example, a public client used by Claude can be added to `staticClients`: + +```yaml +- id: claude-mcp + name: Claude MCP + public: true + redirectURIs: + # Claude on the web + - https://claude.ai/api/mcp/auth_callback + # Claude Code with --callback-port 3118 + - http://localhost:3118/callback + - http://127.0.0.1:3118/callback +``` + +Also add the client ID to the `trustedPeers` of `epinio-api`: + +```yaml +- id: epinio-api + # ... + trustedPeers: + - epinio-cli + - epinio-ui + - claude-mcp +``` + +Back up the Secret before changing it: + +```bash +kubectl get secret dex-config -n epinio -o yaml > dex-config-backup.yaml +kubectl get secret dex-config -n epinio \ + -o jsonpath='{.data.config\.yaml}' | base64 -d > dex-config.yaml +``` + +After editing `dex-config.yaml`, update only its key in the existing Secret so +the other Dex settings are preserved, then restart Dex: + +```bash +CONFIG=$(base64 < dex-config.yaml | tr -d '\n') +kubectl patch secret dex-config -n epinio --type merge \ + -p "{\"data\":{\"config.yaml\":\"${CONFIG}\"}}" +kubectl rollout restart deployment/dex -n epinio +kubectl rollout status deployment/dex -n epinio +``` + +:::note +The Epinio Helm chart manages `dex-config`. Reapply this customization after an +upgrade that replaces the Secret. +::: + +Dex does not support dynamic client registration or Client ID Metadata +Documents (CIMD). Configure the client ID explicitly in clients that support +it. For Claude Code: + +```bash +claude mcp add --transport http \ + --client-id claude-mcp --callback-port 3118 \ + epinio https://epinio-mcp.your-cluster.example.com +``` + +Other MCP clients can use the same OAuth flow, but need their own registered +client ID and exact callback URI. A local Epinio user can instead supply HTTP +Basic credentials when the MCP client supports manual authorization headers. + ### Elevated tier (optional) The core install wires only to the Epinio API. To turn on the opt-in @@ -97,7 +174,8 @@ RBAC. The install manifest is self-contained: it creates the namespace, the server's ServiceAccount and RBAC, the Deployment and Service, and an Epinio `App` record so `epinio app list/show/logs` keep working. -Deploy the server (edit the image tag, credentials, and Ingress host first): +Deploy the server (edit the image tag, authentication URLs, and Ingress host +first): ```bash kubectl apply -f install/epinio-mcp.yaml @@ -131,6 +209,18 @@ A healthy `/readyz` response reports the Epinio version it reached: {"epinio":{"kube_version":"...","platform":"...","version":"..."},"status":"ok","version":"..."} ``` +Verify OAuth discovery: + +```bash +curl https://epinio-mcp./.well-known/oauth-protected-resource +curl -i https://epinio-mcp./ +``` + +The metadata request returns the configured resource and Dex issuer. The +unauthenticated MCP request returns `401 Unauthorized` with a +`WWW-Authenticate` header pointing to that metadata. A client can then begin +the OAuth flow. + ## See also - [MCP server reference](../reference/mcp.md) diff --git a/docs/reference/mcp.md b/docs/reference/mcp.md index 59456a50b1..ee31717d4a 100644 --- a/docs/reference/mcp.md +++ b/docs/reference/mcp.md @@ -41,23 +41,65 @@ agent and the Epinio REST API to the cluster: ```text AI Agent (Claude, etc.) - | MCP protocol (Streamable HTTP, served at the server root) + | MCP protocol (Streamable HTTP) + caller credentials Epinio MCP Server - | REST API (Basic Auth or OIDC, TLS) + | REST API using the same caller credentials Epinio API Server | Kubernetes API Kubernetes Cluster ``` -Authentication is per request: the agent passes an `Authorization` header -(`Bearer ` or `Basic `) that the server forwards to -Epinio. When no header is present, the server falls back to the credentials it was -configured with (default `admin` / `password`). +The MCP endpoint is served at the server root. Authentication is mandatory and +per caller: the agent passes either an OIDC bearer token or HTTP Basic +credentials, and the MCP server asks Epinio's authenticated `/me` endpoint to +validate it. Epinio remains the authority for users and permissions. + +There is no anonymous mode or shared server-credential fallback. A request +without credentials receives `401 Unauthorized` and cannot access tools. Every +tool call uses an Epinio API client created from the caller's credential, so +the result is limited by that user's Epinio permissions. By default every tool wires **only** to the Epinio REST API, as the calling user. An optional [elevated tier](#elevated-tier) that reaches directly into Kubernetes is off unless explicitly enabled. +### OAuth discovery + +The server is an OAuth 2.0 protected resource. It exposes +`/.well-known/oauth-protected-resource`, which contains: + +- The MCP resource URL. +- The Dex authorization-server issuer. +- The supported bearer-token method and scopes. + +An unauthenticated MCP request returns a `WWW-Authenticate` challenge pointing +to this document. An OAuth-capable MCP client can then discover Dex, perform +Authorization Code with PKCE, and send the resulting access token as +`Authorization: Bearer `. + +Epinio accepts tokens intended for the `epinio-api` client. The MCP OAuth client +must therefore be registered as a trusted peer of `epinio-api`, and request the +`audience:server:client_id:epinio-api` scope. Dex requires static client +registration and an exact callback URI; it does not support dynamic client +registration or Client ID Metadata Documents (CIMD). + +See [Install the MCP server](../getting-started/install-mcp#configure-dex-for-oauth-clients) +for Dex configuration and a Claude Code example. Compatibility with another +MCP client depends on its support for Streamable HTTP, protected-resource +discovery, and an explicitly configured OAuth client ID. + +### Authentication configuration + +| Variable | Required | Purpose | +| --- | --- | --- | +| `EPINIO_API_URL` | Yes | Epinio API base URL. | +| `EPINIO_MCP_RESOURCE_URL` | Yes | Exact MCP URL entered by users and advertised in protected-resource metadata. | +| `EPINIO_MCP_OIDC_ISSUER` | Yes | Dex issuer URL advertised to OAuth clients. | + +The resource URL is compared exactly by OAuth clients, so scheme, hostname, +port, and path must match. The issuer must match the `issuer` field in Dex's +OpenID Connect discovery document. + ## Core tools These are always available and act purely through the Epinio API.