Skip to content

Commit be6fdfd

Browse files
committed
Correct static auth and DCR guidance
1 parent 6436f43 commit be6fdfd

5 files changed

Lines changed: 103 additions & 144 deletions

File tree

docs/toolhive/guides-k8s/auth-k8s.mdx

Lines changed: 6 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -49,103 +49,16 @@ authenticated, your MCP server or `MCPRemoteProxy` may separately need its own
4949
way to authenticate to the backend API it calls. Which pattern fits depends on
5050
that backend's relationship to your identity provider:
5151

52-
| Scenario | Pattern | K8s guide |
53-
| ------------------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54-
| 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) |
55-
| Backend trusts the same IdP as your clients | Token exchange (RFC 8693) | [Configure token exchange](./token-exchange-k8s.mdx) |
56-
| Backend trusts a federated IdP (for example, AWS) | Federated token exchange | [AWS STS integration](../integrations/aws-sts.mdx) |
57-
| Backend is an external API with no federation (for example, GitHub) | Embedded authorization server | [Run an embedded OAuth server](#run-an-embedded-oauth-server) |
52+
| Scenario | Pattern | K8s guide |
53+
| ------------------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
54+
| 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` |
55+
| Backend trusts the same IdP as your clients | Token exchange (RFC 8693) | [Configure token exchange](./token-exchange-k8s.mdx) |
56+
| Backend trusts a federated IdP (for example, AWS) | Federated token exchange | [AWS STS integration](../integrations/aws-sts.mdx) |
57+
| Backend is an external API with no federation (for example, GitHub) | Embedded authorization server | [Run an embedded OAuth server](#run-an-embedded-oauth-server) |
5858

5959
For the full comparison and why each pattern fits its scenario, see
6060
[Choosing the right backend authentication pattern](../concepts/backend-auth.mdx#choosing-the-right-backend-authentication-pattern).
6161

62-
### Inject static backend credentials
63-
64-
Use an `MCPExternalAuthConfig` when the MCP server can't read a credential from
65-
an environment variable, or when you want multiple workloads to share one
66-
authentication configuration. ToolHive injects the credential into outbound HTTP
67-
requests at the proxy layer.
68-
69-
This differs from [`spec.secrets`](./run-mcp-k8s.mdx#run-a-server-with-secrets)
70-
and the [HashiCorp Vault integration](../integrations/vault.mdx), which pass
71-
credentials to the MCP server process as environment variables.
72-
73-
Two static credential types are available:
74-
75-
- `headerInjection` adds one custom header using `headerName` and a
76-
`valueSecretRef`.
77-
- `bearerToken` reads `tokenSecretRef` and adds it as
78-
`Authorization: Bearer <TOKEN_VALUE>`.
79-
80-
First, store the credential in a Secret:
81-
82-
```yaml title="backend-api-key-secret.yaml"
83-
apiVersion: v1
84-
kind: Secret
85-
metadata:
86-
name: backend-api-key
87-
namespace: toolhive-system
88-
type: Opaque
89-
stringData:
90-
api-key: '<YOUR_API_KEY>'
91-
```
92-
93-
Then create the `MCPExternalAuthConfig`:
94-
95-
```yaml title="backend-api-key-auth.yaml"
96-
apiVersion: toolhive.stacklok.dev/v1beta1
97-
kind: MCPExternalAuthConfig
98-
metadata:
99-
name: backend-api-key
100-
namespace: toolhive-system
101-
spec:
102-
type: headerInjection
103-
headerInjection:
104-
headerName: X-API-Key
105-
valueSecretRef:
106-
name: backend-api-key
107-
key: api-key
108-
```
109-
110-
For an `Authorization: Bearer` header, use this configuration instead. Store
111-
only the token value in the Secret, without the `Bearer` prefix:
112-
113-
```yaml title="backend-bearer-auth.yaml"
114-
apiVersion: toolhive.stacklok.dev/v1beta1
115-
kind: MCPExternalAuthConfig
116-
metadata:
117-
name: backend-bearer-token
118-
namespace: toolhive-system
119-
spec:
120-
type: bearerToken
121-
bearerToken:
122-
tokenSecretRef:
123-
name: backend-bearer-token
124-
key: token
125-
```
126-
127-
Reference the configuration from an `MCPServer` or `MCPRemoteProxy` in the same
128-
namespace:
129-
130-
```yaml title="MCPServer or MCPRemoteProxy: externalAuthConfigRef"
131-
spec:
132-
externalAuthConfigRef:
133-
name: backend-api-key
134-
```
135-
136-
The same `MCPExternalAuthConfig` can be referenced by multiple workloads. To
137-
configure headers specifically for one `MCPRemoteProxy`, including multiple
138-
plaintext or Secret-backed headers, use
139-
[`headerForward`](./remote-mcp-proxy.mdx#inject-custom-headers) instead.
140-
141-
:::note[Client authentication is separate]
142-
143-
Static credential injection authenticates ToolHive to the backend. It does not
144-
authenticate clients connecting to ToolHive. Configure client authentication
145-
separately using one of the approaches below.
146-
147-
:::
148-
14962
## Authenticate with OIDC
15063

15164
Both external IdP and Kubernetes service-to-service authentication use the same

docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx

Lines changed: 37 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -319,15 +319,15 @@ instead.
319319

320320
## Configure session storage
321321

322-
The embedded authorization server caches upstream tokens and DCR-issued client
323-
credentials in the same store as sessions. By default, that store is in memory,
324-
so these values are lost when pods restart and users must re-authenticate. When
325-
you use DCR, a restart also registers a new client with the upstream and leaves
326-
the previous registration orphaned. For production deployments, configure a
327-
Redis backend so ToolHive can reuse the DCR credentials and sessions after a
328-
restart. Add one of the following `storage` blocks at
329-
`spec.embeddedAuthServer.storage` in the `MCPExternalAuthConfig`. The `redis`
330-
block supports three connection modes; you must set exactly one:
322+
The embedded authorization server caches upstream tokens and client credentials
323+
obtained through DCR with an upstream provider in the same store as sessions. By
324+
default, that store is in memory, so these values are lost when pods restart and
325+
users must re-authenticate. When you use upstream DCR, a restart also registers
326+
a new client with the upstream and leaves the previous registration orphaned.
327+
For production deployments, configure a Redis backend so ToolHive can reuse the
328+
DCR credentials and sessions after a restart. Add one of the following `storage`
329+
blocks at `spec.embeddedAuthServer.storage` in the `MCPExternalAuthConfig`. The
330+
`redis` block supports three connection modes; you must set exactly one:
331331

332332
- **Sentinel** (`sentinelConfig`) - self-managed Redis with Sentinel-based high
333333
availability (HA)
@@ -396,21 +396,22 @@ For a complete walkthrough including deploying Redis Sentinel from scratch, see
396396

397397
## Configure MCP client registration
398398

399-
MCP clients must identify themselves to the embedded authorization server. DCR
400-
is enabled by default for backward compatibility. Enable CIMD for clients that
401-
support the MCP specification's preferred registration mechanism. You can also
402-
set baseline scopes for clients that need to request scopes beyond those in
403-
their registration metadata.
399+
MCP clients must identify themselves to the embedded authorization server. The
400+
server always accepts Dynamic Client Registration (DCR) requests from MCP
401+
clients; no configuration is required. Enable CIMD for clients that support the
402+
MCP specification's preferred registration mechanism. You can also set baseline
403+
scopes for clients that need to request scopes beyond those in their
404+
registration metadata.
404405

405406
### Enable CIMD for zero-registration clients
406407

407-
DCR requires every client to register before its first authorization request.
408-
Some MCP clients, including recent VS Code builds, can instead present an HTTPS
409-
URL that hosts a Client ID Metadata Document (CIMD), letting the authorization
410-
server resolve client metadata on demand with no prior registration step. CIMD
411-
is the MCP specification's preferred client registration mechanism; DCR is the
412-
backward-compatibility fallback. Add `cimd` under `spec.embeddedAuthServer` in
413-
the `MCPExternalAuthConfig`:
408+
Client-side DCR requires every client to register before its first authorization
409+
request. Some MCP clients, including recent VS Code builds, can instead present
410+
an HTTPS URL that hosts a Client ID Metadata Document (CIMD), letting the
411+
authorization server resolve client metadata on demand with no prior
412+
registration step. CIMD is the MCP specification's preferred client registration
413+
mechanism; client-side DCR is the backward-compatibility fallback. Add `cimd`
414+
under `spec.embeddedAuthServer` in the `MCPExternalAuthConfig`:
414415

415416
```yaml title="MCPExternalAuthConfig: CIMD configuration"
416417
spec:
@@ -580,6 +581,11 @@ to create an application in a provider dashboard. Add `dcrConfig` to an
580581
`oauth2Config` upstream to have the embedded authorization server register
581582
itself at runtime using RFC 7591.
582583

584+
In this flow, ToolHive registers with the upstream provider as an OAuth client.
585+
This is separate from MCP clients registering with ToolHive's embedded
586+
authorization server in
587+
[Configure MCP client registration](#configure-mcp-client-registration).
588+
583589
This example uses an RFC 8414 discovery document. ToolHive reads the
584590
`registration_endpoint` and other provider metadata from `discoveryUrl`:
585591

@@ -615,6 +621,9 @@ spec:
615621
Set `authorizationEndpoint` and `tokenEndpoint` even when the discovery document
616622
advertises them. The CRD requires both fields on `oauth2Config`.
617623

624+
For a complete deployment that uses `discoveryUrl` with Notion, see the
625+
[Notion MCP server guide](../guides-mcp/notion-remote.mdx).
626+
618627
If the provider gives you a registration URL directly, use
619628
`registrationEndpoint` instead of `discoveryUrl` in the `MCPExternalAuthConfig`
620629
provider's `oauth2Config`:
@@ -641,17 +650,10 @@ endpoints and the scopes that ToolHive should request.
641650
Don't set `clientId` or `clientSecretRef` when you set `dcrConfig`. ToolHive
642651
obtains the client ID and client secret from the DCR response.
643652

644-
#### Optional DCR fields
645-
646-
Use these fields only when the upstream provider requires them:
653+
#### Authorize registration with an initial access token
647654

648-
| Field | Purpose |
649-
| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
650-
| `initialAccessTokenRef` | References a Secret containing the bearer token that authorizes the registration request. |
651-
| `softwareId` | Sends an RFC 7591 `software_id` value that identifies the client software independently of a particular registration. |
652-
| `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. |
653-
654-
For an initial access token, create a Secret in the same namespace:
655+
If the upstream provider requires an initial access token, create a Secret in
656+
the same namespace:
655657

656658
```yaml title="dcr-initial-access-token.yaml"
657659
apiVersion: v1
@@ -664,22 +666,18 @@ stringData:
664666
token: '<YOUR_DCR_INITIAL_ACCESS_TOKEN>'
665667
```
666668
667-
Then add the reference to the upstream provider's `oauth2Config.dcrConfig` in
668-
the `MCPExternalAuthConfig`:
669+
Then add `initialAccessTokenRef` to the upstream provider's
670+
`oauth2Config.dcrConfig` in the `MCPExternalAuthConfig`. ToolHive sends the
671+
Secret value as a bearer token when it calls the registration endpoint:
669672

670-
```yaml title="MCPExternalAuthConfig: optional DCR fields"
673+
```yaml title="MCPExternalAuthConfig: DCR initial access token"
671674
dcrConfig:
672675
registrationEndpoint: 'https://mcp.example.com/register'
673676
initialAccessTokenRef:
674677
name: dcr-initial-access-token
675678
key: token
676-
softwareId: 'toolhive-embedded-auth'
677-
softwareStatement: '<YOUR_SIGNED_SOFTWARE_STATEMENT_JWT>'
678679
```
679680
680-
The `softwareStatement` is a signed attestation, not a secret. Anyone who can
681-
read the `MCPExternalAuthConfig` can see its contents.
682-
683681
### Extract identity from the token response
684682
685683
Some providers don't expose a userinfo endpoint but return user identity in the

docs/toolhive/guides-k8s/mcp-server-entry.mdx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,9 @@ spec:
229229
```
230230

231231
For sensitive values like API keys, use `addHeadersFromSecret` instead. See the
232-
[Inject custom headers](./remote-mcp-proxy.mdx#inject-custom-headers) section of
233-
the MCPRemoteProxy guide for the full syntax, which MCPServerEntry shares.
232+
[Inject headers into upstream requests](./remote-mcp-proxy.mdx#inject-headers-into-upstream-requests)
233+
section of the MCPRemoteProxy guide for the full syntax, which MCPServerEntry
234+
shares.
234235

235236
## Complete example
236237

docs/toolhive/guides-k8s/remote-mcp-proxy.mdx

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -511,18 +511,12 @@ condition to `False`, moves the resource to phase `Failed`, and stops updating
511511
the Deployment. See [Check remote proxy status](#check-remote-proxy-status) to
512512
inspect the failure message.
513513

514-
### Inject custom headers
514+
### Inject headers into upstream requests
515515

516516
Some remote MCP servers require custom headers for tenant identification, API
517517
keys, or other purposes. Use the `headerForward` field to inject headers into
518-
every request forwarded to the remote server.
519-
520-
To put a single API key or bearer token behind a reusable authentication
521-
resource, use an
522-
[`MCPExternalAuthConfig`](./auth-k8s.mdx#inject-static-backend-credentials)
523-
instead. That approach also works with `MCPServer`. Use `headerForward` when you
524-
want to configure headers specifically for this proxy, including multiple
525-
plaintext or Secret-backed values.
518+
every request forwarded to the remote server. This configuration belongs to one
519+
`MCPRemoteProxy` and supports multiple plaintext or Secret-backed values.
526520

527521
For non-sensitive values like tenant IDs or correlation headers, use
528522
`addPlaintextHeaders`:
@@ -609,6 +603,58 @@ spec:
609603

610604
:::
611605

606+
#### Reuse a bearer token across remote proxies
607+
608+
To inject the same `Authorization: Bearer <TOKEN>` header from multiple
609+
`MCPRemoteProxy` resources, store the token behind a shared
610+
`MCPExternalAuthConfig` of type `bearerToken`.
611+
612+
First, create a Secret containing only the token value, without the `Bearer`
613+
prefix:
614+
615+
```yaml title="backend-bearer-token-secret.yaml"
616+
apiVersion: v1
617+
kind: Secret
618+
metadata:
619+
name: backend-bearer-token
620+
namespace: toolhive-system
621+
type: Opaque
622+
stringData:
623+
token: '<YOUR_BEARER_TOKEN>'
624+
```
625+
626+
Create the authentication configuration in the same namespace:
627+
628+
```yaml title="backend-bearer-auth.yaml"
629+
apiVersion: toolhive.stacklok.dev/v1beta1
630+
kind: MCPExternalAuthConfig
631+
metadata:
632+
name: backend-bearer-auth
633+
namespace: toolhive-system
634+
spec:
635+
type: bearerToken
636+
bearerToken:
637+
tokenSecretRef:
638+
name: backend-bearer-token
639+
key: token
640+
```
641+
642+
Reference it from each remote proxy that uses the token:
643+
644+
```yaml title="MCPRemoteProxy: externalAuthConfigRef"
645+
spec:
646+
externalAuthConfigRef:
647+
name: backend-bearer-auth
648+
```
649+
650+
Both `headerForward` and `bearerToken` authenticate the proxy to the remote MCP
651+
server. They do not authenticate clients connecting to the proxy. Configure
652+
client authentication separately with `oidcConfigRef` or `authServerRef`.
653+
654+
For static credentials sent from a Virtual MCP Server (vMCP) directly to its
655+
backends, see
656+
[Inject a static credential](../guides-vmcp/authentication.mdx#inject-a-static-credential-header-injection).
657+
612658
## Quick start example
613659

614660
For testing and development, you can use the public MCP specification server:

docs/toolhive/guides-vmcp/authentication.mdx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,8 @@ spec:
463463

464464
Alternatively, attach the `MCPExternalAuthConfig` to a backend `MCPServer` via
465465
its `externalAuthConfigRef` and use `outgoingAuth.source: discovered` to pick it
466-
up automatically.
466+
up automatically. In that case, the reference is metadata that vMCP reads; the
467+
`MCPServer` does not inject the header itself.
467468

468469
For an `Authorization: Bearer <token>` header, set `headerName: Authorization`
469470
and store the full `Bearer <token>` string (including the `Bearer` prefix) in

0 commit comments

Comments
 (0)