From 11b8595c24d05a4245b8948589b1b9eb771fd0e4 Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Mon, 26 Oct 2026 12:28:25 +0200 Subject: [PATCH] docs(security): say plainly that enable_socket: false closes the stdio-child token-minting path (#1704) A stdio server under none or sandbox isolation can mint agent tokens over the admin socket with no key. The configuration reference, the sandbox page, the quarantine limits and the isolation pages now name enable_socket: false as the setting that matters outside docker isolation. --- docs/configuration.md | 2 +- docs/configuration/config-file.md | 2 +- docs/docker-isolation.md | 2 +- docs/features/docker-isolation.md | 2 +- docs/features/sandbox-isolation.md | 14 ++++++++++++-- docs/features/security-quarantine.md | 1 + 6 files changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 6c6bc060c..0acf27e69 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -88,7 +88,7 @@ A running MCPProxy core watches `mcp_config.json` and hot-reloads external edits | Field | Type | Default | Description | |-------|------|---------|-------------| -| `enable_socket` | boolean | `true` | Enable Unix socket (macOS/Linux) or named pipe (Windows) for secure local IPC between tray and core | +| `enable_socket` | boolean | `true` | Enable the Unix socket (macOS/Linux) or named pipe (Windows) the tray uses to talk to the core. **Connections over it are trusted without the API key**, and that includes every stdio server the proxy itself spawns under `none` or `sandbox` isolation: such a server can call the admin API and mint agent tokens with no credential. The tray is the only socket client, so **set `false` on headless installs, and whenever a stdio server you do not fully trust runs outside `docker` isolation**. `mcpproxy doctor` warns about the combination. See [What the sandbox does not hide](features/sandbox-isolation.md#what-the-sandbox-does-not-hide). | | `tray_endpoint` | string | `""` | Override socket/pipe path (advanced, usually not needed) | ### Search & Tool Limits diff --git a/docs/configuration/config-file.md b/docs/configuration/config-file.md index 94eb1461d..3a2be0de2 100644 --- a/docs/configuration/config-file.md +++ b/docs/configuration/config-file.md @@ -63,7 +63,7 @@ MCPProxy uses a JSON configuration file located at `~/.mcpproxy/mcp_config.json` | `trusted_proxies` | string[] | `[]` (trust nobody) | CIDRs or IP addresses whose `X-Forwarded-For`, `X-Real-IP`, `X-Forwarded-Proto` and `X-Forwarded-Host` headers are believed. Headers from any other peer are ignored and the direct `RemoteAddr` is used. Env `MCPPROXY_TRUSTED_PROXIES` (comma list). Live (hot-reload, no restart). Validation: `trusted_proxies[N] "value" is not a valid CIDR or IP address` — refused identically at boot, `PATCH /api/v1/config` and `/config/apply`. See [Reverse Proxy Deployment](/operations/reverse-proxy#trusted_proxies-forwarded-headers) | | `require_mcp_auth` | boolean | `false` | Require an API key on the `/mcp` endpoint (off by default for client compatibility). Enable when exposing MCPProxy beyond localhost. **Server edition:** forced to `true` whenever `server_edition.enabled` is `true` — an explicit `false` is not an error, but boot logs `require_mcp_auth: false is overridden to true because server_edition.enabled is true` and `mcpproxy doctor` reports the same finding | | `mcp_reject_query_apikey` | boolean | `false` | Refuse a `?apikey=` query parameter on every MCP endpoint (`/mcp*` and the legacy `/v1/tool_code` aliases) with a 401, forcing clients onto `Authorization: Bearer ` or `X-API-Key` so tokens stop appearing in access logs, proxy logs and browser history (a URL already sent is still logged in transit; the option makes the client fail until it is migrated). MCP endpoints only: the REST API, the Web UI and `/events` keep accepting the query form. Clients connected by the Connect wizard with `?apikey=` in the URL (Codex, the generic fallback) must be switched to a header carrier first. Live, no restart | -| `enable_socket` | boolean | `true` | Enable Unix socket/named pipe for local communication | +| `enable_socket` | boolean | `true` | Enable the Unix socket/named pipe the tray uses. Socket connections are trusted **without the API key**, including stdio servers spawned under `none` or `sandbox` isolation, which can then mint agent tokens. Set `false` on headless installs and whenever untrusted stdio servers run outside `docker` isolation. | | `forward_client_headers` | boolean | `true` | Master switch for [client header forwarding](/configuration/upstream-servers#client-header-forwarding). Nothing is forwarded until a server lists names in `forward_headers`. Env `MCPPROXY_FORWARD_CLIENT_HEADERS=false` disables it for the process (never persisted). | ### `audit_log` (edition-neutral JSONL audit record) diff --git a/docs/docker-isolation.md b/docs/docker-isolation.md index 7a092df1f..82381a101 100644 --- a/docs/docker-isolation.md +++ b/docs/docker-isolation.md @@ -454,7 +454,7 @@ Your options on such a host: - **No uid/gid drop.** Dropping to an unprivileged uid/gid requires `CAP_SETUID`/`CAP_SETGID` (i.e. running as root). When mcpproxy runs unprivileged, the uid/gid drop is **best-effort and typically a no-op** — the sandboxed process keeps the launching user's identity. Landlock (filesystem) and `setrlimit` (resource caps) still apply. Docker mode does drop to a container user. This is an honest trade-off, not a bug. - **Linux-only.** Landlock is a Linux 5.13+ feature. On older kernels the launcher degrades best-effort (fewer access-right bits enforced on ABI 1). On macOS/Windows `sandbox` is a documented **no-op** and behaves like `none`. - **Filesystem + resources only.** Landlock confines the filesystem write-allowlist; it does not provide network namespacing. Pair with care for network-sensitive servers, or use `docker` mode with `network_mode: none`. -- **`none` and `sandbox` do not hide the data directory or the Unix socket.** A stdio server runs as the proxy's user, so it can read `mcp_config.json` (holds `api_key`) and `config.db` (holds the upstream OAuth tokens), and while `enable_socket` is on it can connect to `/mcpproxy.sock` without a key — a sandboxed child minted an agent token over it ([#1704](https://github.com/smart-mcp-proxy/mcpproxy-go/issues/1704)). Landlock does block `/proc//environ`, so the key is safe in the environment but not on disk or over the socket. Set `enable_socket: false` for headless installs and use `docker` isolation for servers you do not trust; `mcpproxy doctor` warns when an enabled stdio server resolves to `none` or `sandbox` with the socket on. Verification table: [What the sandbox does not hide](features/sandbox-isolation.md#what-the-sandbox-does-not-hide). +- **`none` and `sandbox` do not hide the data directory or the Unix socket.** A stdio server runs as the proxy's user, so it can read `mcp_config.json` (holds `api_key`) and `config.db` (holds the upstream OAuth tokens), and while `enable_socket` is on it can connect to `/mcpproxy.sock` without a key — a sandboxed child minted an agent token over it ([#1704](https://github.com/smart-mcp-proxy/mcpproxy-go/issues/1704)). Landlock does block `/proc//environ`, so the key is safe in the environment but not on disk or over the socket. **`enable_socket: false` is the setting that matters** when a stdio server you do not fully trust runs outside `docker` isolation (headless installs lose nothing: the tray is the only socket client); use `docker` isolation for such servers where you can; `mcpproxy doctor` warns when an enabled stdio server resolves to `none` or `sandbox` with the socket on. Verification table: [What the sandbox does not hide](features/sandbox-isolation.md#what-the-sandbox-does-not-hide). - **Docker-based scanners do not run under `sandbox`/`none`.** They are skipped and the skip is surfaced via the informational `deep_scan` descriptor — it never downgrades the baseline verdict (Spec 077 FR-008). A native scanner runtime is a future enhancement (D3 option a). ## Platform support matrix diff --git a/docs/features/docker-isolation.md b/docs/features/docker-isolation.md index 3e810ead8..5b2150238 100644 --- a/docs/features/docker-isolation.md +++ b/docs/features/docker-isolation.md @@ -424,7 +424,7 @@ See [Shutdown Behavior](/operations/shutdown-behavior) for detailed subprocess l - **No uid/gid drop.** Dropping to an unprivileged uid/gid requires `CAP_SETUID`/`CAP_SETGID` (i.e. running as root). When mcpproxy runs unprivileged, the uid/gid drop is **best-effort and typically a no-op** — the sandboxed process keeps the launching user's identity. Landlock (filesystem) and `setrlimit` (resource caps) still apply. Docker mode does drop to a container user. This is an honest trade-off, not a bug. - **Linux-only.** Landlock is a Linux 5.13+ feature. On older kernels the launcher degrades best-effort (fewer access-right bits enforced). On macOS/Windows `sandbox` is a documented **no-op** and behaves like `none`. - **Filesystem + resources only.** Landlock confines the filesystem write-allowlist; it does not provide network namespacing. For network-sensitive servers, use `docker` mode with `network_mode: none`. -- **`none` and `sandbox` do not hide the data directory or the Unix socket.** A stdio server runs as the proxy's user, so it can read `mcp_config.json` (holds `api_key`) and `config.db` (holds the upstream OAuth tokens), and while `enable_socket` is on it can connect to `/mcpproxy.sock` without a key — a sandboxed child minted an agent token over it ([#1704](https://github.com/smart-mcp-proxy/mcpproxy-go/issues/1704)). Landlock does block `/proc//environ`, so the key is safe in the environment but not on disk or over the socket. Set `enable_socket: false` for headless installs and use `docker` isolation for servers you do not trust; `mcpproxy doctor` warns when an enabled stdio server resolves to `none` or `sandbox` with the socket on. Verification table: [What the sandbox does not hide](/features/sandbox-isolation#what-the-sandbox-does-not-hide). +- **`none` and `sandbox` do not hide the data directory or the Unix socket.** A stdio server runs as the proxy's user, so it can read `mcp_config.json` (holds `api_key`) and `config.db` (holds the upstream OAuth tokens), and while `enable_socket` is on it can connect to `/mcpproxy.sock` without a key — a sandboxed child minted an agent token over it ([#1704](https://github.com/smart-mcp-proxy/mcpproxy-go/issues/1704)). Landlock does block `/proc//environ`, so the key is safe in the environment but not on disk or over the socket. **`enable_socket: false` is the setting that matters** when a stdio server you do not fully trust runs outside `docker` isolation (headless installs lose nothing: the tray is the only socket client); use `docker` isolation for such servers where you can; `mcpproxy doctor` warns when an enabled stdio server resolves to `none` or `sandbox` with the socket on. Verification table: [What the sandbox does not hide](/features/sandbox-isolation#what-the-sandbox-does-not-hide). - **Docker-based scanners do not run under `sandbox`/`none`.** They are skipped and the skip is surfaced via the informational `deep_scan` descriptor — it never downgrades the baseline verdict (Spec 077 FR-008). A native scanner runtime is a future enhancement. ## Platform support matrix diff --git a/docs/features/sandbox-isolation.md b/docs/features/sandbox-isolation.md index 904ceaa80..1ea26e40a 100644 --- a/docs/features/sandbox-isolation.md +++ b/docs/features/sandbox-isolation.md @@ -15,6 +15,15 @@ sandboxes, Landlock does **not** require unprivileged user namespaces, so it is not blocked by `kernel.apparmor_restrict_unprivileged_userns=1` (default on Ubuntu 23.10+). +:::caution The sandbox confines writes, not identity +A stdio server under `sandbox` (or `none`) still runs as the proxy's user. It can +read `mcp_config.json` and `config.db`, and while `enable_socket` is on it can +connect to the admin socket **without a key and mint itself an agent token**, in +both modes. If you run stdio servers you do not fully trust outside `docker` +isolation, **`enable_socket: false` is the setting that matters**. Details and +the verified matrix: [What the sandbox does not hide](#what-the-sandbox-does-not-hide). +::: + ## Enabling Set the isolation **mode** to `sandbox`, globally or per server: @@ -111,8 +120,9 @@ readable in `none` and `sandbox`. What to do: -- **Headless installs** (no tray): set `enable_socket: false`. The tray is the - only socket client, so a headless service loses nothing. +- **Set `enable_socket: false`** unless you use the tray. This is the one + setting that closes the token-minting path in both `none` and `sandbox`; the + tray is the only socket client, so a headless service loses nothing. - **Servers you do not trust**: use `docker` isolation (`docker_isolation.mode: "docker"`, or per-server `isolation.mode`). The container does not mount the data directory, so neither the files nor the diff --git a/docs/features/security-quarantine.md b/docs/features/security-quarantine.md index c5885500f..af042db43 100644 --- a/docs/features/security-quarantine.md +++ b/docs/features/security-quarantine.md @@ -219,6 +219,7 @@ What it does not do: - **It is not proof of safety.** Review is only as good as the reviewer, and the offline scanners can miss attacks. A clean scan means the checks found nothing in the captured definitions; it is not evidence that a server or tool is safe. Manual review, scanner-assisted review and agent-assisted inspection (`inspect_quarantined`) are inputs to your decision. None of them approves anything by itself. - **It does not see behavior outside the definition.** The hash covers name, description, schemas and the safety-relevant annotation hints. A server whose implementation or backend changes while its definitions stay identical is not detected, and neither is a malicious tool result returned at call time. - **Trust modes change the guarantee.** `scan` (the default since spec 119) admits a new server, a new tool and a changed tool contract automatically when the offline scan is clean, so a detector miss — including a paraphrased rug pull that scans clean — is admitted at that point; findings, uncovered tools and safer-direction hint flips are held. `manual` holds every new server and every tool contract change until it is approved: server release needs a person, while tool approval can also come from an admin-access MCP client. `auto` admits without scanning or holding at all and trusts tool changes (rug-pull risk). Server admission and tool-change review are separate controls: `"quarantined": false` on a server entry only releases the server-level quarantine, and under `manual` trust a new or changed tool on that server is still held after its baseline. Tool-change holds are removed by `trust_mode: auto`, the legacy `auto_approve_tool_changes: true` (which an explicit `trust_mode` overrides), or `quarantine_enabled: false` globally. See [Trust modes](#trust-modes-auto--scan--manual). +- **It gates definitions, not processes.** Quarantine decides which tool definitions agents may see and call. A stdio server the proxy spawns under `none` or `sandbox` isolation still runs as the proxy's user: it can read the data directory and, while `enable_socket` is on, mint agent tokens over the admin socket with no key, whatever its quarantine state. For such servers `enable_socket: false` or `docker` isolation is the control, not approval. See [What the sandbox does not hide](./sandbox-isolation.md#what-the-sandbox-does-not-hide). - **It is not data-loss prevention.** See [Sensitive Data Detection](./sensitive-data-detection.md) and [Output Sanitisation](./output-sanitisation.md) for what is flagged and what, if anything, is blocked. ## Managing Quarantine