Add configurable allowed hosts for MCP remote access (KB_MCP_ALLOWED_HOSTS)

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>
This commit is contained in:
2026-04-04 12:39:43 +01:00
parent e39e00a2c0
commit da5b8435bc
10 changed files with 203 additions and 0 deletions
@@ -0,0 +1,59 @@
## 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.