From 1b92715313b3fbdc2e9f4ecd78f59da0803330cb Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Tue, 21 Jul 2026 16:44:44 -0400 Subject: [PATCH 1/3] Document Kubernetes static auth and DCR --- docs/toolhive/guides-k8s/auth-k8s.mdx | 98 ++++++++- .../guides-k8s/embedded-auth-server-k8s.mdx | 201 +++++++++++++++--- docs/toolhive/guides-k8s/remote-mcp-proxy.mdx | 6 + 3 files changed, 270 insertions(+), 35 deletions(-) diff --git a/docs/toolhive/guides-k8s/auth-k8s.mdx b/docs/toolhive/guides-k8s/auth-k8s.mdx index 655ab095..8df9ca50 100644 --- a/docs/toolhive/guides-k8s/auth-k8s.mdx +++ b/docs/toolhive/guides-k8s/auth-k8s.mdx @@ -49,16 +49,102 @@ authenticated, your MCP server or `MCPRemoteProxy` may separately need its own way to authenticate to the backend API it calls. Which pattern fits depends on that backend's relationship to your identity provider: -| Scenario | Pattern | K8s guide | -| ------------------------------------------------------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Backend only accepts API keys or static credentials | Static credentials | [Run a server with secrets](./run-mcp-k8s.mdx#run-a-server-with-secrets) (MCPServer), [HashiCorp Vault integration](../integrations/vault.mdx), or [inject custom headers](./remote-mcp-proxy.mdx#inject-custom-headers) (MCPRemoteProxy) | -| Backend trusts the same IdP as your clients | Token exchange (RFC 8693) | [Configure token exchange](./token-exchange-k8s.mdx) | -| Backend trusts a federated IdP (for example, AWS) | Federated token exchange | [AWS STS integration](../integrations/aws-sts.mdx) | -| Backend is an external API with no federation (for example, GitHub) | Embedded authorization server | [Run an embedded OAuth server](#run-an-embedded-oauth-server) | +| Scenario | Pattern | K8s guide | +| ------------------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Backend only accepts API keys or static credentials | Static credentials | [Inject static backend credentials](#inject-static-backend-credentials), [run a server with secrets](./run-mcp-k8s.mdx#run-a-server-with-secrets), or use the [HashiCorp Vault integration](../integrations/vault.mdx) | +| Backend trusts the same IdP as your clients | Token exchange (RFC 8693) | [Configure token exchange](./token-exchange-k8s.mdx) | +| Backend trusts a federated IdP (for example, AWS) | Federated token exchange | [AWS STS integration](../integrations/aws-sts.mdx) | +| Backend is an external API with no federation (for example, GitHub) | Embedded authorization server | [Run an embedded OAuth server](#run-an-embedded-oauth-server) | For the full comparison and why each pattern fits its scenario, see [Choosing the right backend authentication pattern](../concepts/backend-auth.mdx#choosing-the-right-backend-authentication-pattern). +### Inject static backend credentials + +Use an `MCPExternalAuthConfig` when the MCP server can't read a credential from +an environment variable, or when you want multiple workloads to share one +authentication configuration. ToolHive injects the credential into outbound HTTP +requests at the proxy layer. + +This differs from [`spec.secrets`](./run-mcp-k8s.mdx#run-a-server-with-secrets) +and the [HashiCorp Vault integration](../integrations/vault.mdx), which pass +credentials to the MCP server process as environment variables. + +Two static credential types are available: + +- `headerInjection` adds one custom header using `headerName` and a + `valueSecretRef`. +- `bearerToken` reads `tokenSecretRef` and adds it as + `Authorization: Bearer `. + +First, store the credential in a Secret: + +```yaml title="backend-api-key-secret.yaml" +apiVersion: v1 +kind: Secret +metadata: + name: backend-api-key + namespace: toolhive-system +type: Opaque +stringData: + api-key: '' +``` + +Then create the `MCPExternalAuthConfig`: + +```yaml title="backend-api-key-auth.yaml" +apiVersion: toolhive.stacklok.dev/v1beta1 +kind: MCPExternalAuthConfig +metadata: + name: backend-api-key + namespace: toolhive-system +spec: + type: headerInjection + headerInjection: + headerName: X-API-Key + valueSecretRef: + name: backend-api-key + key: api-key +``` + +For an `Authorization: Bearer` header, use this configuration instead. Store +only the token value in the Secret, without the `Bearer` prefix: + +```yaml title="backend-bearer-auth.yaml" +apiVersion: toolhive.stacklok.dev/v1beta1 +kind: MCPExternalAuthConfig +metadata: + name: backend-bearer-token + namespace: toolhive-system +spec: + type: bearerToken + bearerToken: + tokenSecretRef: + name: backend-bearer-token + key: token +``` + +Reference the configuration from an `MCPServer` or `MCPRemoteProxy` in the same +namespace: + +```yaml title="MCPServer or MCPRemoteProxy: externalAuthConfigRef" +spec: + externalAuthConfigRef: + name: backend-api-key +``` + +The same `MCPExternalAuthConfig` can be referenced by multiple workloads. For an +`MCPRemoteProxy` that needs multiple headers or non-sensitive plaintext headers, +use [`headerForward`](./remote-mcp-proxy.mdx#inject-custom-headers) instead. + +:::note[Client authentication is separate] + +Static credential injection authenticates ToolHive to the backend. It does not +authenticate clients connecting to ToolHive. Configure client authentication +separately using one of the approaches below. + +::: + ## Authenticate with OIDC Both external IdP and Kubernetes service-to-service authentication use the same diff --git a/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx b/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx index 071293fe..b0c40173 100644 --- a/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx +++ b/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx @@ -25,8 +25,9 @@ You'll need: - An upstream identity provider that supports the OAuth 2.0 authorization code flow (such as Okta, Microsoft Entra ID, Auth0, or any OIDC-compliant provider) -- A registered OAuth application/client with your upstream provider -- Client ID and client secret from your upstream provider +- Either a registered OAuth application/client or an upstream provider that + supports RFC 7591 Dynamic Client Registration (DCR) +- For a registered client, its client ID and client secret This setup uses the `MCPExternalAuthConfig` custom resource, following the same pattern as [token exchange configuration](./token-exchange-k8s.mdx). @@ -47,7 +48,8 @@ flowchart LR ## Step 1: Create a Secret for the upstream provider client credentials -Store the OAuth client secret for your upstream identity provider: +If you registered a client with the upstream provider, store its OAuth client +secret: ```yaml title="upstream-idp-secret.yaml" apiVersion: v1 @@ -64,6 +66,10 @@ stringData: kubectl apply -f upstream-idp-secret.yaml ``` +Skip this step if you use DCR without an initial access token. If the upstream +requires an initial access token for DCR, create a Secret for that token in +[Use dynamic client registration with an upstream provider](#use-dynamic-client-registration-with-an-upstream-provider). + ## Step 2: Create a Secret for JWT signing keys The embedded authorization server signs JWTs with a private key you provide. @@ -281,7 +287,7 @@ authorization endpoints automatically. ::: -### Combine embedded auth with outgoing token exchange +## Combine embedded auth with outgoing token exchange A single MCP server can use the embedded authorization server for incoming client authentication and token exchange for outgoing calls to backend services. @@ -291,7 +297,7 @@ This works the same way regardless of which outgoing strategy you use: `authServerRef` and `externalAuthConfigRef` on the same `MCPServer` or `MCPRemoteProxy` resource: -```yaml +```yaml title="MCPServer or MCPRemoteProxy: authentication references" spec: # Embedded auth server for incoming client authentication authServerRef: @@ -311,13 +317,17 @@ For plain token exchange, use the same pattern, but point [`type: tokenExchange`](./token-exchange-k8s.mdx#step-2-create-the-mcpexternalauthconfig-resource) instead. -### Configure session storage +## Configure session storage -By default, the embedded authorization server stores sessions in memory. -Upstream tokens are lost when pods restart, requiring users to re-authenticate. -For production deployments, configure a Redis backend by adding a `storage` -block to your `MCPExternalAuthConfig`. The `redis` block supports three -connection modes; you must set exactly one: +The embedded authorization server caches upstream tokens and DCR-issued client +credentials in the same store as sessions. By default, that store is in memory, +so these values are lost when pods restart and users must re-authenticate. When +you use DCR, a restart also registers a new client with the upstream and leaves +the previous registration orphaned. For production deployments, configure a +Redis backend so ToolHive can reuse the DCR credentials and sessions after a +restart. Add one of the following `storage` blocks at +`spec.embeddedAuthServer.storage` in the `MCPExternalAuthConfig`. The `redis` +block supports three connection modes; you must set exactly one: - **Sentinel** (`sentinelConfig`) - self-managed Redis with Sentinel-based high availability (HA) @@ -327,7 +337,7 @@ connection modes; you must set exactly one: services, such as GCP Memorystore Cluster or AWS ElastiCache with cluster mode enabled -```yaml title="storage block for MCPExternalAuthConfig - Sentinel" +```yaml title="MCPExternalAuthConfig: Sentinel storage" storage: type: redis redis: @@ -345,7 +355,7 @@ storage: key: password ``` -```yaml title="storage block for MCPExternalAuthConfig - Standalone" +```yaml title="MCPExternalAuthConfig: standalone storage" storage: type: redis redis: @@ -358,7 +368,7 @@ storage: key: password ``` -```yaml title="storage block for MCPExternalAuthConfig - Cluster" +```yaml title="MCPExternalAuthConfig: cluster storage" storage: type: redis redis: @@ -384,6 +394,14 @@ kubectl create secret generic redis-acl-secret \ For a complete walkthrough including deploying Redis Sentinel from scratch, see [Redis session storage](./redis-session-storage.mdx). +## Configure MCP client registration + +MCP clients must identify themselves to the embedded authorization server. DCR +is enabled by default for backward compatibility. Enable CIMD for clients that +support the MCP specification's preferred registration mechanism. You can also +set baseline scopes for clients that need to request scopes beyond those in +their registration metadata. + ### Enable CIMD for zero-registration clients DCR requires every client to register before its first authorization request. @@ -391,9 +409,10 @@ Some MCP clients, including recent VS Code builds, can instead present an HTTPS URL that hosts a Client ID Metadata Document (CIMD), letting the authorization server resolve client metadata on demand with no prior registration step. CIMD is the MCP specification's preferred client registration mechanism; DCR is the -backward-compatibility fallback. Enable it by adding a `cimd` block: +backward-compatibility fallback. Add `cimd` under `spec.embeddedAuthServer` in +the `MCPExternalAuthConfig`: -```yaml +```yaml title="MCPExternalAuthConfig: CIMD configuration" spec: embeddedAuthServer: cimd: @@ -440,9 +459,10 @@ Some MCP clients (for example, Claude Code) register via DCR with a narrowed `scope` value, then request a wider set of scopes at `/oauth/authorize`. By default, the embedded authorization server rejects those requests with `invalid_scope` because the registered client's scope set doesn't include the -scopes being requested. To support this pattern, set `baselineClientScopes`: +scopes being requested. Set `baselineClientScopes` under +`spec.embeddedAuthServer` in the `MCPExternalAuthConfig`: -```yaml +```yaml title="MCPExternalAuthConfig: baseline client scopes" spec: embeddedAuthServer: baselineClientScopes: @@ -463,12 +483,26 @@ public clients like Claude Code, Cursor, and VS Code, so privileged scopes don't belong in the baseline. For the conceptual reason this exists, see [Baseline scopes for DCR clients](../concepts/embedded-auth-server.mdx#baseline-scopes-for-dcr-clients). +## Configure upstream providers + +The base setup in Step 4 uses a pre-registered OIDC client. Use the options in +this section to adapt that upstream connection: + +- Use `oauth2Config` instead of `oidcConfig` when the upstream provider does not + support OIDC discovery. +- Within `oauth2Config`, set either pre-provisioned client credentials or + `dcrConfig`, not both. +- Add identity mapping, authorization parameters, and an explicit callback URL + as the upstream provider requires. + ### Using an OAuth 2.0 upstream provider If your upstream identity provider does not support OIDC discovery, you can configure it as an OAuth 2.0 provider with explicit endpoints. This is useful for providers like GitHub that use OAuth 2.0 but don't implement the full OIDC -specification. +specification. If the provider supports RFC 7591 instead of pre-provisioned +client credentials, see +[Use dynamic client registration with an upstream provider](#use-dynamic-client-registration-with-an-upstream-provider). ```yaml title="embedded-auth-oauth2-config.yaml" apiVersion: toolhive.stacklok.dev/v1beta1 @@ -539,12 +573,120 @@ For OAuth 2.0 servers that return identity in the token response itself, see ::: +### Use dynamic client registration with an upstream provider + +Some OAuth 2.0 providers register clients dynamically instead of requiring you +to create an application in a provider dashboard. Add `dcrConfig` to an +`oauth2Config` upstream to have the embedded authorization server register +itself at runtime using RFC 7591. + +This example uses an RFC 8414 discovery document. ToolHive reads the +`registration_endpoint` and other provider metadata from `discoveryUrl`: + +```yaml title="embedded-auth-config-dcr.yaml" +apiVersion: toolhive.stacklok.dev/v1beta1 +kind: MCPExternalAuthConfig +metadata: + name: embedded-auth-server-dcr + namespace: toolhive-system +spec: + type: embeddedAuthServer + embeddedAuthServer: + issuer: 'https://toolhive.example.com' + signingKeySecretRefs: + - name: auth-server-signing-key + key: signing-key + hmacSecretRefs: + - name: auth-server-hmac-secret + key: hmac-key + upstreamProviders: + - name: remote-mcp + type: oauth2 + oauth2Config: + authorizationEndpoint: 'https://mcp.example.com/authorize' + tokenEndpoint: 'https://mcp.example.com/token' + redirectUri: 'https://toolhive.example.com/oauth/callback' + # highlight-start + dcrConfig: + discoveryUrl: 'https://mcp.example.com/.well-known/oauth-authorization-server' + # highlight-end +``` + +Set `authorizationEndpoint` and `tokenEndpoint` even when the discovery document +advertises them. The CRD requires both fields on `oauth2Config`. + +If the provider gives you a registration URL directly, use +`registrationEndpoint` instead of `discoveryUrl` in the `MCPExternalAuthConfig` +provider's `oauth2Config`: + +```yaml title="MCPExternalAuthConfig: direct DCR endpoint" +upstreamProviders: + - name: remote-mcp + type: oauth2 + oauth2Config: + authorizationEndpoint: 'https://mcp.example.com/authorize' + tokenEndpoint: 'https://mcp.example.com/token' + scopes: + - mcp:read + # highlight-start + dcrConfig: + registrationEndpoint: 'https://mcp.example.com/register' + # highlight-end +``` + +Set exactly one of `discoveryUrl` or `registrationEndpoint`. A direct +registration endpoint bypasses discovery, so specify the authorization and token +endpoints and the scopes that ToolHive should request. + +Don't set `clientId` or `clientSecretRef` when you set `dcrConfig`. ToolHive +obtains the client ID and client secret from the DCR response. + +#### Optional DCR fields + +Use these fields only when the upstream provider requires them: + +| Field | Purpose | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `initialAccessTokenRef` | References a Secret containing the bearer token that authorizes the registration request. | +| `softwareId` | Sends an RFC 7591 `software_id` value that identifies the client software independently of a particular registration. | +| `softwareStatement` | Sends a signed RFC 7591 `software_statement` JWT. The value is visible in the resource and etcd backups, so don't put secrets inside it. | + +For an initial access token, create a Secret in the same namespace: + +```yaml title="dcr-initial-access-token.yaml" +apiVersion: v1 +kind: Secret +metadata: + name: dcr-initial-access-token + namespace: toolhive-system +type: Opaque +stringData: + token: '' +``` + +Then add the reference to the upstream provider's `oauth2Config.dcrConfig` in +the `MCPExternalAuthConfig`: + +```yaml title="MCPExternalAuthConfig: optional DCR fields" +dcrConfig: + registrationEndpoint: 'https://mcp.example.com/register' + initialAccessTokenRef: + name: dcr-initial-access-token + key: token + softwareId: 'toolhive-embedded-auth' + softwareStatement: '' +``` + +The `softwareStatement` is a signed attestation, not a secret. Anyone who can +read the `MCPExternalAuthConfig` can see its contents. + ### Extract identity from the token response Some providers don't expose a userinfo endpoint but return user identity in the OAuth 2.0 token response itself. For these providers, set `identityFromToken` on -`oauth2Config` instead of `userInfo`. The embedded auth server then skips the -userinfo HTTP call and extracts identity from the token response body using +the upstream provider's `oauth2Config` in the `MCPExternalAuthConfig` instead of +`userInfo`. The embedded auth server then skips the userinfo HTTP call and +extracts identity from the token response body using [gjson dot-notation paths](https://github.com/tidwall/gjson#path-syntax): `username` extracts a top-level field, `authed_user.id` extracts a nested field, and the pipe operator chains modifiers like `@upstreamjwt`. @@ -552,7 +694,7 @@ and the pipe operator chains modifiers like `@upstreamjwt`. For example, Slack's `oauth.v2.access` response includes the authenticated user ID at `authed_user.id`: -```yaml title="oauth2Config snippet for Slack" +```yaml title="MCPExternalAuthConfig: Slack identity mapping" oauth2Config: # highlight-start identityFromToken: @@ -564,7 +706,7 @@ Snowflake returns the authenticated login name as a top-level `username` field in every authorization-code grant response, and does not expose a userinfo endpoint: -```yaml title="oauth2Config snippet for Snowflake" +```yaml title="MCPExternalAuthConfig: Snowflake identity mapping" oauth2Config: # highlight-start identityFromToken: @@ -577,7 +719,7 @@ For providers whose token response embeds identity inside a JWT-shaped access token, the `@upstreamjwt` modifier decodes the JWT payload so subsequent path segments can drill into it: -```yaml title="oauth2Config snippet for JWT-embedded identity" +```yaml title="MCPExternalAuthConfig: JWT-embedded identity mapping" oauth2Config: # highlight-start identityFromToken: @@ -611,9 +753,10 @@ includes `access_type=offline` - Google's non-standard alternative to the `offline_access` scope. To pass these parameters, set `additionalAuthorizationParams` on the -`oidcConfig` or `oauth2Config` of an upstream provider: +`oidcConfig` or `oauth2Config` under `spec.embeddedAuthServer.upstreamProviders` +in the `MCPExternalAuthConfig`: -```yaml title="upstreamProviders configuration block" +```yaml title="MCPExternalAuthConfig: Google OIDC provider" upstreamProviders: - name: google type: oidc @@ -658,9 +801,9 @@ provider before the flow can complete. Use the same URL on both sides: the value computed from `resourceUrl` here, and the authorized redirect URI in your provider's application settings. -For example, given this `oidcConfigRef` on an MCPServer: +For example, given this `oidcConfigRef` on an `MCPServer`: -```yaml +```yaml title="MCPServer: oidcConfigRef" spec: oidcConfigRef: name: embedded-auth-oidc @@ -671,7 +814,7 @@ spec: Omitting `redirectUri` on the upstream provider resolves the callback to `https://mcp.example.com/mcp/oauth/callback`: -```yaml +```yaml title="MCPExternalAuthConfig: Google OIDC provider" upstreamProviders: - name: google type: oidc diff --git a/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx b/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx index 04b7cec4..8c62387a 100644 --- a/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx +++ b/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx @@ -517,6 +517,12 @@ Some remote MCP servers require custom headers for tenant identification, API keys, or other purposes. Use the `headerForward` field to inject headers into every request forwarded to the remote server. +To put a single API key or bearer token behind a reusable authentication +resource, use an +[`MCPExternalAuthConfig`](./auth-k8s.mdx#inject-static-backend-credentials) +instead. That approach also works with `MCPServer`. Use `headerForward` when +this proxy needs multiple headers or non-sensitive plaintext values. + For non-sensitive values like tenant IDs or correlation headers, use `addPlaintextHeaders`: From 8cf0e80c3b061016ae4c03d8d159b7892689cfb2 Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:10:41 -0400 Subject: [PATCH 2/3] Clarify static header configuration --- docs/toolhive/guides-k8s/auth-k8s.mdx | 7 ++++--- docs/toolhive/guides-k8s/remote-mcp-proxy.mdx | 5 +++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/toolhive/guides-k8s/auth-k8s.mdx b/docs/toolhive/guides-k8s/auth-k8s.mdx index 8df9ca50..fe7b6f8e 100644 --- a/docs/toolhive/guides-k8s/auth-k8s.mdx +++ b/docs/toolhive/guides-k8s/auth-k8s.mdx @@ -133,9 +133,10 @@ spec: name: backend-api-key ``` -The same `MCPExternalAuthConfig` can be referenced by multiple workloads. For an -`MCPRemoteProxy` that needs multiple headers or non-sensitive plaintext headers, -use [`headerForward`](./remote-mcp-proxy.mdx#inject-custom-headers) instead. +The same `MCPExternalAuthConfig` can be referenced by multiple workloads. To +configure headers specifically for one `MCPRemoteProxy`, including multiple +plaintext or Secret-backed headers, use +[`headerForward`](./remote-mcp-proxy.mdx#inject-custom-headers) instead. :::note[Client authentication is separate] diff --git a/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx b/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx index 8c62387a..7df5fb01 100644 --- a/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx +++ b/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx @@ -520,8 +520,9 @@ every request forwarded to the remote server. To put a single API key or bearer token behind a reusable authentication resource, use an [`MCPExternalAuthConfig`](./auth-k8s.mdx#inject-static-backend-credentials) -instead. That approach also works with `MCPServer`. Use `headerForward` when -this proxy needs multiple headers or non-sensitive plaintext values. +instead. That approach also works with `MCPServer`. Use `headerForward` when you +want to configure headers specifically for this proxy, including multiple +plaintext or Secret-backed values. For non-sensitive values like tenant IDs or correlation headers, use `addPlaintextHeaders`: From e40592227e8d04f7a5b596a45d49b692e1ac62f9 Mon Sep 17 00:00:00 2001 From: Dan Barr <6922515+danbarr@users.noreply.github.com> Date: Tue, 21 Jul 2026 17:52:37 -0400 Subject: [PATCH 3/3] Correct static auth and DCR guidance --- docs/toolhive/guides-k8s/auth-k8s.mdx | 99 ++----------------- .../guides-k8s/embedded-auth-server-k8s.mdx | 76 +++++++------- docs/toolhive/guides-k8s/mcp-server-entry.mdx | 5 +- docs/toolhive/guides-k8s/remote-mcp-proxy.mdx | 64 ++++++++++-- docs/toolhive/guides-vmcp/authentication.mdx | 3 +- 5 files changed, 103 insertions(+), 144 deletions(-) diff --git a/docs/toolhive/guides-k8s/auth-k8s.mdx b/docs/toolhive/guides-k8s/auth-k8s.mdx index fe7b6f8e..5d1f1619 100644 --- a/docs/toolhive/guides-k8s/auth-k8s.mdx +++ b/docs/toolhive/guides-k8s/auth-k8s.mdx @@ -49,103 +49,16 @@ authenticated, your MCP server or `MCPRemoteProxy` may separately need its own way to authenticate to the backend API it calls. Which pattern fits depends on that backend's relationship to your identity provider: -| Scenario | Pattern | K8s guide | -| ------------------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Backend only accepts API keys or static credentials | Static credentials | [Inject static backend credentials](#inject-static-backend-credentials), [run a server with secrets](./run-mcp-k8s.mdx#run-a-server-with-secrets), or use the [HashiCorp Vault integration](../integrations/vault.mdx) | -| Backend trusts the same IdP as your clients | Token exchange (RFC 8693) | [Configure token exchange](./token-exchange-k8s.mdx) | -| Backend trusts a federated IdP (for example, AWS) | Federated token exchange | [AWS STS integration](../integrations/aws-sts.mdx) | -| Backend is an external API with no federation (for example, GitHub) | Embedded authorization server | [Run an embedded OAuth server](#run-an-embedded-oauth-server) | +| Scenario | Pattern | K8s guide | +| ------------------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Backend only accepts API keys or static credentials | Static credentials | [Run a server with secrets](./run-mcp-k8s.mdx#run-a-server-with-secrets) or use the [HashiCorp Vault integration](../integrations/vault.mdx) for `MCPServer`; [inject headers into upstream requests](./remote-mcp-proxy.mdx#inject-headers-into-upstream-requests) for `MCPRemoteProxy` | +| Backend trusts the same IdP as your clients | Token exchange (RFC 8693) | [Configure token exchange](./token-exchange-k8s.mdx) | +| Backend trusts a federated IdP (for example, AWS) | Federated token exchange | [AWS STS integration](../integrations/aws-sts.mdx) | +| Backend is an external API with no federation (for example, GitHub) | Embedded authorization server | [Run an embedded OAuth server](#run-an-embedded-oauth-server) | For the full comparison and why each pattern fits its scenario, see [Choosing the right backend authentication pattern](../concepts/backend-auth.mdx#choosing-the-right-backend-authentication-pattern). -### Inject static backend credentials - -Use an `MCPExternalAuthConfig` when the MCP server can't read a credential from -an environment variable, or when you want multiple workloads to share one -authentication configuration. ToolHive injects the credential into outbound HTTP -requests at the proxy layer. - -This differs from [`spec.secrets`](./run-mcp-k8s.mdx#run-a-server-with-secrets) -and the [HashiCorp Vault integration](../integrations/vault.mdx), which pass -credentials to the MCP server process as environment variables. - -Two static credential types are available: - -- `headerInjection` adds one custom header using `headerName` and a - `valueSecretRef`. -- `bearerToken` reads `tokenSecretRef` and adds it as - `Authorization: Bearer `. - -First, store the credential in a Secret: - -```yaml title="backend-api-key-secret.yaml" -apiVersion: v1 -kind: Secret -metadata: - name: backend-api-key - namespace: toolhive-system -type: Opaque -stringData: - api-key: '' -``` - -Then create the `MCPExternalAuthConfig`: - -```yaml title="backend-api-key-auth.yaml" -apiVersion: toolhive.stacklok.dev/v1beta1 -kind: MCPExternalAuthConfig -metadata: - name: backend-api-key - namespace: toolhive-system -spec: - type: headerInjection - headerInjection: - headerName: X-API-Key - valueSecretRef: - name: backend-api-key - key: api-key -``` - -For an `Authorization: Bearer` header, use this configuration instead. Store -only the token value in the Secret, without the `Bearer` prefix: - -```yaml title="backend-bearer-auth.yaml" -apiVersion: toolhive.stacklok.dev/v1beta1 -kind: MCPExternalAuthConfig -metadata: - name: backend-bearer-token - namespace: toolhive-system -spec: - type: bearerToken - bearerToken: - tokenSecretRef: - name: backend-bearer-token - key: token -``` - -Reference the configuration from an `MCPServer` or `MCPRemoteProxy` in the same -namespace: - -```yaml title="MCPServer or MCPRemoteProxy: externalAuthConfigRef" -spec: - externalAuthConfigRef: - name: backend-api-key -``` - -The same `MCPExternalAuthConfig` can be referenced by multiple workloads. To -configure headers specifically for one `MCPRemoteProxy`, including multiple -plaintext or Secret-backed headers, use -[`headerForward`](./remote-mcp-proxy.mdx#inject-custom-headers) instead. - -:::note[Client authentication is separate] - -Static credential injection authenticates ToolHive to the backend. It does not -authenticate clients connecting to ToolHive. Configure client authentication -separately using one of the approaches below. - -::: - ## Authenticate with OIDC Both external IdP and Kubernetes service-to-service authentication use the same diff --git a/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx b/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx index b0c40173..f1144409 100644 --- a/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx +++ b/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx @@ -319,15 +319,15 @@ instead. ## Configure session storage -The embedded authorization server caches upstream tokens and DCR-issued client -credentials in the same store as sessions. By default, that store is in memory, -so these values are lost when pods restart and users must re-authenticate. When -you use DCR, a restart also registers a new client with the upstream and leaves -the previous registration orphaned. For production deployments, configure a -Redis backend so ToolHive can reuse the DCR credentials and sessions after a -restart. Add one of the following `storage` blocks at -`spec.embeddedAuthServer.storage` in the `MCPExternalAuthConfig`. The `redis` -block supports three connection modes; you must set exactly one: +The embedded authorization server caches upstream tokens and client credentials +obtained through DCR with an upstream provider in the same store as sessions. By +default, that store is in memory, so these values are lost when pods restart and +users must re-authenticate. When you use upstream DCR, a restart also registers +a new client with the upstream and leaves the previous registration orphaned. +For production deployments, configure a Redis backend so ToolHive can reuse the +DCR credentials and sessions after a restart. Add one of the following `storage` +blocks at `spec.embeddedAuthServer.storage` in the `MCPExternalAuthConfig`. The +`redis` block supports three connection modes; you must set exactly one: - **Sentinel** (`sentinelConfig`) - self-managed Redis with Sentinel-based high availability (HA) @@ -396,21 +396,22 @@ For a complete walkthrough including deploying Redis Sentinel from scratch, see ## Configure MCP client registration -MCP clients must identify themselves to the embedded authorization server. DCR -is enabled by default for backward compatibility. Enable CIMD for clients that -support the MCP specification's preferred registration mechanism. You can also -set baseline scopes for clients that need to request scopes beyond those in -their registration metadata. +MCP clients must identify themselves to the embedded authorization server. The +server always accepts Dynamic Client Registration (DCR) requests from MCP +clients; no configuration is required. Enable CIMD for clients that support the +MCP specification's preferred registration mechanism. You can also set baseline +scopes for clients that need to request scopes beyond those in their +registration metadata. ### Enable CIMD for zero-registration clients -DCR requires every client to register before its first authorization request. -Some MCP clients, including recent VS Code builds, can instead present an HTTPS -URL that hosts a Client ID Metadata Document (CIMD), letting the authorization -server resolve client metadata on demand with no prior registration step. CIMD -is the MCP specification's preferred client registration mechanism; DCR is the -backward-compatibility fallback. Add `cimd` under `spec.embeddedAuthServer` in -the `MCPExternalAuthConfig`: +Client-side DCR requires every client to register before its first authorization +request. Some MCP clients, including recent VS Code builds, can instead present +an HTTPS URL that hosts a Client ID Metadata Document (CIMD), letting the +authorization server resolve client metadata on demand with no prior +registration step. CIMD is the MCP specification's preferred client registration +mechanism; client-side DCR is the backward-compatibility fallback. Add `cimd` +under `spec.embeddedAuthServer` in the `MCPExternalAuthConfig`: ```yaml title="MCPExternalAuthConfig: CIMD configuration" spec: @@ -580,6 +581,11 @@ to create an application in a provider dashboard. Add `dcrConfig` to an `oauth2Config` upstream to have the embedded authorization server register itself at runtime using RFC 7591. +In this flow, ToolHive registers with the upstream provider as an OAuth client. +This is separate from MCP clients registering with ToolHive's embedded +authorization server in +[Configure MCP client registration](#configure-mcp-client-registration). + This example uses an RFC 8414 discovery document. ToolHive reads the `registration_endpoint` and other provider metadata from `discoveryUrl`: @@ -615,6 +621,9 @@ spec: Set `authorizationEndpoint` and `tokenEndpoint` even when the discovery document advertises them. The CRD requires both fields on `oauth2Config`. +For a complete deployment that uses `discoveryUrl` with Notion, see the +[Notion MCP server guide](../guides-mcp/notion-remote.mdx). + If the provider gives you a registration URL directly, use `registrationEndpoint` instead of `discoveryUrl` in the `MCPExternalAuthConfig` provider's `oauth2Config`: @@ -641,17 +650,10 @@ endpoints and the scopes that ToolHive should request. Don't set `clientId` or `clientSecretRef` when you set `dcrConfig`. ToolHive obtains the client ID and client secret from the DCR response. -#### Optional DCR fields - -Use these fields only when the upstream provider requires them: +#### Authorize registration with an initial access token -| Field | Purpose | -| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | -| `initialAccessTokenRef` | References a Secret containing the bearer token that authorizes the registration request. | -| `softwareId` | Sends an RFC 7591 `software_id` value that identifies the client software independently of a particular registration. | -| `softwareStatement` | Sends a signed RFC 7591 `software_statement` JWT. The value is visible in the resource and etcd backups, so don't put secrets inside it. | - -For an initial access token, create a Secret in the same namespace: +If the upstream provider requires an initial access token, create a Secret in +the same namespace: ```yaml title="dcr-initial-access-token.yaml" apiVersion: v1 @@ -664,22 +666,18 @@ stringData: token: '' ``` -Then add the reference to the upstream provider's `oauth2Config.dcrConfig` in -the `MCPExternalAuthConfig`: +Then add `initialAccessTokenRef` to the upstream provider's +`oauth2Config.dcrConfig` in the `MCPExternalAuthConfig`. ToolHive sends the +Secret value as a bearer token when it calls the registration endpoint: -```yaml title="MCPExternalAuthConfig: optional DCR fields" +```yaml title="MCPExternalAuthConfig: DCR initial access token" dcrConfig: registrationEndpoint: 'https://mcp.example.com/register' initialAccessTokenRef: name: dcr-initial-access-token key: token - softwareId: 'toolhive-embedded-auth' - softwareStatement: '' ``` -The `softwareStatement` is a signed attestation, not a secret. Anyone who can -read the `MCPExternalAuthConfig` can see its contents. - ### Extract identity from the token response Some providers don't expose a userinfo endpoint but return user identity in the diff --git a/docs/toolhive/guides-k8s/mcp-server-entry.mdx b/docs/toolhive/guides-k8s/mcp-server-entry.mdx index 3f7feead..4ecfcfef 100644 --- a/docs/toolhive/guides-k8s/mcp-server-entry.mdx +++ b/docs/toolhive/guides-k8s/mcp-server-entry.mdx @@ -229,8 +229,9 @@ spec: ``` For sensitive values like API keys, use `addHeadersFromSecret` instead. See the -[Inject custom headers](./remote-mcp-proxy.mdx#inject-custom-headers) section of -the MCPRemoteProxy guide for the full syntax, which MCPServerEntry shares. +[Inject headers into upstream requests](./remote-mcp-proxy.mdx#inject-headers-into-upstream-requests) +section of the MCPRemoteProxy guide for the full syntax, which MCPServerEntry +shares. ## Complete example diff --git a/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx b/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx index 7df5fb01..cebd54c5 100644 --- a/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx +++ b/docs/toolhive/guides-k8s/remote-mcp-proxy.mdx @@ -511,18 +511,12 @@ condition to `False`, moves the resource to phase `Failed`, and stops updating the Deployment. See [Check remote proxy status](#check-remote-proxy-status) to inspect the failure message. -### Inject custom headers +### Inject headers into upstream requests Some remote MCP servers require custom headers for tenant identification, API keys, or other purposes. Use the `headerForward` field to inject headers into -every request forwarded to the remote server. - -To put a single API key or bearer token behind a reusable authentication -resource, use an -[`MCPExternalAuthConfig`](./auth-k8s.mdx#inject-static-backend-credentials) -instead. That approach also works with `MCPServer`. Use `headerForward` when you -want to configure headers specifically for this proxy, including multiple -plaintext or Secret-backed values. +every request forwarded to the remote server. This configuration belongs to one +`MCPRemoteProxy` and supports multiple plaintext or Secret-backed values. For non-sensitive values like tenant IDs or correlation headers, use `addPlaintextHeaders`: @@ -609,6 +603,58 @@ spec: ::: +#### Reuse a bearer token across remote proxies + +To inject the same `Authorization: Bearer ` header from multiple +`MCPRemoteProxy` resources, store the token behind a shared +`MCPExternalAuthConfig` of type `bearerToken`. + +First, create a Secret containing only the token value, without the `Bearer` +prefix: + +```yaml title="backend-bearer-token-secret.yaml" +apiVersion: v1 +kind: Secret +metadata: + name: backend-bearer-token + namespace: toolhive-system +type: Opaque +stringData: + token: '' +``` + +Create the authentication configuration in the same namespace: + +```yaml title="backend-bearer-auth.yaml" +apiVersion: toolhive.stacklok.dev/v1beta1 +kind: MCPExternalAuthConfig +metadata: + name: backend-bearer-auth + namespace: toolhive-system +spec: + type: bearerToken + bearerToken: + tokenSecretRef: + name: backend-bearer-token + key: token +``` + +Reference it from each remote proxy that uses the token: + +```yaml title="MCPRemoteProxy: externalAuthConfigRef" +spec: + externalAuthConfigRef: + name: backend-bearer-auth +``` + +Both `headerForward` and `bearerToken` authenticate the proxy to the remote MCP +server. They do not authenticate clients connecting to the proxy. Configure +client authentication separately with `oidcConfigRef` or `authServerRef`. + +For static credentials sent from a Virtual MCP Server (vMCP) directly to its +backends, see +[Inject a static credential](../guides-vmcp/authentication.mdx#inject-a-static-credential-header-injection). + ## Quick start example For testing and development, you can use the public MCP specification server: diff --git a/docs/toolhive/guides-vmcp/authentication.mdx b/docs/toolhive/guides-vmcp/authentication.mdx index 359ab92e..9bacc6c4 100644 --- a/docs/toolhive/guides-vmcp/authentication.mdx +++ b/docs/toolhive/guides-vmcp/authentication.mdx @@ -463,7 +463,8 @@ spec: Alternatively, attach the `MCPExternalAuthConfig` to a backend `MCPServer` via its `externalAuthConfigRef` and use `outgoingAuth.source: discovered` to pick it -up automatically. +up automatically. In that case, the reference is metadata that vMCP reads; the +`MCPServer` does not inject the header itself. For an `Authorization: Bearer ` header, set `headerName: Authorization` and store the full `Bearer ` string (including the `Bearer` prefix) in