da5b8435bc
The MCP SDK's DNS rebinding protection rejects remote clients with 421 when the Host header isn't in the allowlist. Add KB_MCP_ALLOWED_HOSTS env var (comma-separated IPs/FQDNs) to configure additional allowed hosts while keeping localhost always permitted. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
60 lines
3.9 KiB
Markdown
60 lines
3.9 KiB
Markdown
## Context
|
|
|
|
The MCP Python SDK includes DNS rebinding protection via `TransportSecuritySettings` in `mcp.server.transport_security`. When enabled, it validates the `Host` header against an allowlist and returns 421 for unrecognised hosts.
|
|
|
|
`FastMCP` auto-enables this protection when `host` is `127.0.0.1`, `localhost`, or `::1`, with a default allowlist of those three values (wildcard port). The kb MCP server does not pass a `host` to `FastMCP()` and does not pass `transport_security`, so the behaviour depends on the SDK's defaults — which have changed between versions and will likely keep changing.
|
|
|
|
Currently the server calls `mcp.streamable_http_app()` to get a Starlette sub-app and wraps it in its own Starlette app with `BearerAuthMiddleware`. The `transport_security` settings flow through `FastMCP()` → `Settings` → `StreamableHTTPSessionManager`, so they must be set at `FastMCP` construction time.
|
|
|
|
When a remote client connects (e.g. `Host: 192.168.1.50:3000`), the SDK rejects the request with 421 before our auth middleware even runs.
|
|
|
|
## Goals / Non-Goals
|
|
|
|
**Goals:**
|
|
|
|
- Allow operators to configure additional allowed hosts via environment variable so remote clients can connect.
|
|
- Support both IP addresses and FQDNs, with or without port.
|
|
- Preserve DNS rebinding protection (keep it enabled, just widen the allowlist).
|
|
- Maintain backward compatibility — unset variable means localhost-only, same as today.
|
|
|
|
**Non-Goals:**
|
|
|
|
- Disabling DNS rebinding protection entirely.
|
|
- Configuring allowed origins separately from allowed hosts (derive origins automatically from hosts).
|
|
- TLS termination or HTTPS — that belongs to a reverse proxy in front of the MCP container.
|
|
|
|
## Decisions
|
|
|
|
### 1. Environment variable format
|
|
|
|
`KB_MCP_ALLOWED_HOSTS` is a comma-separated list of hosts. Each entry is an IP address or FQDN without port and without scheme.
|
|
|
|
Examples: `192.168.1.50`, `kb.example.com`, `192.168.1.50,kb.example.com,10.0.0.1`
|
|
|
|
**Rationale:** Comma-separated is the simplest format that doesn't require quoting in Docker Compose YAML or shell environments. Ports are omitted because the wildcard-port pattern (`host:*`) covers all ports — operators shouldn't need to know the internal port.
|
|
|
|
**Alternative considered:** JSON array — rejected, awkward in env vars and Compose files.
|
|
|
|
### 2. Merge with localhost defaults
|
|
|
|
The parsed hosts are merged with the hardcoded localhost set (`127.0.0.1`, `localhost`, `[::1]`). Localhost is always allowed regardless of the env var value.
|
|
|
|
**Rationale:** Removing localhost would break local development and health checks. There's no reason to ever disallow it.
|
|
|
|
### 3. Auto-derive allowed origins from allowed hosts
|
|
|
|
For each allowed host, generate `http://<host>:*` as an allowed origin. No separate env var for origins.
|
|
|
|
**Rationale:** The MCP server doesn't serve HTTPS (TLS is terminated by a reverse proxy), so `http://` is always correct at the container level. If HTTPS origins are needed in future, a separate env var can be added then.
|
|
|
|
### 4. Pass TransportSecuritySettings explicitly to FastMCP
|
|
|
|
Always construct a `TransportSecuritySettings` with `enable_dns_rebinding_protection=True` and the merged allowlist, and pass it as `transport_security=` to `FastMCP()`. This makes the behaviour explicit rather than depending on SDK defaults.
|
|
|
|
**Rationale:** The SDK's auto-detection logic depends on the `host` parameter which we don't set, and the defaults may change between SDK versions. Being explicit removes the ambiguity.
|
|
|
|
## Risks / Trade-offs
|
|
|
|
- **Operator must know their Host header value** — If a reverse proxy rewrites the Host header, the operator needs to allowlist the rewritten value, not the original. → Mitigation: document this in Compose file comments.
|
|
- **No HTTPS origin support** — If a client sends `Origin: https://...`, it will be rejected. → Mitigation: acceptable for now; the MCP server sits behind a proxy that terminates TLS. Can add `KB_MCP_ALLOWED_ORIGINS` later if needed.
|