You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 27eb24a
Browse filesBrowse the repository at this point in the historyBrowse files
Closes#1704
Delivers what was promised on the issue: honest docs, a `doctor`
warning, and a check of the `/dev/null` side observation (which turned
out to be a real bug, fixed here).
## Docs
`none` and `sandbox` isolation do not hide the data directory or the
Unix socket from a stdio child: the child runs as the proxy's user, can
read `mcp_config.json` (`api_key`) and `config.db` (OAuth tokens), and
while `enable_socket` is on it can mint agent tokens over
`<data-dir>/mcpproxy.sock` without a key. Landlock confines writes only
(it does block `/proc/<pid>/environ`).
-
[`features/sandbox-isolation`](https://docs.mcpproxy.app/features/sandbox-isolation/):
new **What the sandbox does not hide** section with @james-crowley's
verification table and wording, plus two new honest-limits bullets (data
dir / socket; snap-packaged runtimes).
-
[`features/docker-isolation`](https://docs.mcpproxy.app/features/docker-isolation/)
and the unpublished root `docs/docker-isolation.md`: `sandbox` and
`none` rows of the mode table say so; a new **Honest limitations**
bullet links the table.
-
[`socket-communication`](https://docs.mcpproxy.app/socket-communication/):
trust-boundary bullet under API Key Authentication.
-
[`getting-started/installation`](https://docs.mcpproxy.app/getting-started/installation/):
headless recommendation (`enable_socket: false` + `docker` isolation) in
the LAN security checklist; the headless example config now sets
`enable_socket: false`.
Website build is green and the new anchor resolves from all three
linking pages (checked in `website/build`).
## Doctor
`config.StdioSocketExposureFinding` joins the existing `DoctorFindings`
→ `runtime_warnings` path. It names every enabled stdio server whose
resolved isolation mode is `none` or `sandbox` while `enable_socket` is
true, and states the fix:
```
1 enabled stdio server(s) run under isolation none or sandbox while enable_socket is true ("inherits-none"): a stdio server runs as the proxy's user, can read the data directory (mcp_config.json holds api_key, config.db holds OAuth tokens) and can mint agent tokens over the admin socket in that directory without a key; the sandbox mode confines writes only. Set enable_socket: false on headless installs and use docker isolation (docker_isolation.mode: "docker", or per-server isolation.mode) for servers you do not trust. See https://docs.mcpproxy.app/features/sandbox-isolation/
```
Skipped: servers resolved to `docker`, servers whose command is `docker`
itself, HTTP/SSE servers, disabled servers. A per-server `enabled:
false` opt-out under a global `docker` mode is listed. Tests in
`internal/config/doctor_findings_test.go`. `doctor` still exits 0; the
finding counts toward `total_issues` like every other runtime warning.
## `/dev/null` under `sandbox` — confirmed and fixed
Reproduced on a real kernel (kubic, Ubuntu 24.04, kernel 6.8, Landlock
ABI 4) with the production spec: `sh -c '… 2>/dev/null'` fails with
`cannot create /dev/null: Permission denied`. The read-only `/` rule
covers `READ_FILE` but not `WRITE_FILE` on the device.
Two changes:
1. `buildSandboxSpec` grants `/dev/null` read-write (the single device,
not `/dev`).
2. `addPathRule` masks directory-only rights (`READ_DIR`, `MAKE_*`,
`REMOVE_*`, `REFER`) when the target is not a directory. Without this
Landlock rejects the rule with `EINVAL`, and because the wrapper is
fail-closed the whole confinement aborted (exit 3) — so a naive
`/dev/null` grant would have broken every sandboxed server.
What the probe showed with the real `mcpproxy __sandbox_exec` wrapper on
kubic (RO `/`, RW tmp + caches):
| | without the grant | with the grant |
|---|---|---|
| `sh -c 'echo x 2>/dev/null'` | Permission denied | ok |
| Node `spawnSync(…, {stdio: "ignore"})` | `EACCES` | ok |
| Python `subprocess.run(…, stdout=DEVNULL)` | `PermissionError` | ok |
| `npx --version`; `npx -y @modelcontextprotocol/server-everything
stdio` → `initialize` | ok | ok |
| `uvx --version`; `uvx mcp-server-time` → `initialize` (non-snap uv) |
ok | ok |
| snap-packaged `uvx` | `snap-confine is packaged without necessary
permissions` | same |
| `echo x >/dev/stdout` | ok (pipe, not a filesystem object) | ok |
So `npx`/`uvx` servers *launched* fine before; what broke was any server
that routes a child's stdio to `/dev/null`, and shell launch wrappers
using `2>/dev/null`. The snap finding is independent of `/dev/null`:
Landlock requires `no_new_privs`, which stops snap-confine from gaining
the privileges it needs. Documented as a limitation; the fix is a
non-snap runtime install.
Tests: `TestBuildSandboxSpec_DevNullWritable` (any OS),
`TestSandboxWrapper_DefaultSpecAllowsDevNull` (`internal/upstream/core`,
linux), `TestAddPathRule_FileTarget` (`internal/sandbox`, linux). The
two linux-tagged tests were watched red then green on kubic via
cross-compiled test binaries; macOS cannot run them (Landlock is
Linux-only).
## Verification
- `golangci-lint` with `.github/.golangci.yml`: bare, `--build-tags
server`, and `GOOS=linux` for the `_linux.go` files — no findings in
changed files (the local v2 reports two pre-existing `reflect.Ptr` govet
items in untouched test files, identical on `main`).
- `go test -race ./internal/config/... ./internal/sandbox/...
./internal/management/...`, `-tags server` config tests, `cmd/mcpproxy`
doctor tests, `internal/server` + `internal/httpapi` doctor/bundle tests
under the CI skip regex: green.
- `./scripts/test-api-e2e.sh` on a private port (`LISTEN_PORT=18790`,
with `mcpproxy-server` built): 70 run, 0 failed.
- `website && npm run build`: green.
No new dependencies.
| `docker` | Wraps the server in a Docker container (process/FS/network isolation, resource limits). The default and most capable mode. | Any host with a working Docker daemon. | Yes (container user) |
38
-
| `sandbox` | Runs the server **natively** under a Linux [Landlock](https://docs.kernel.org/userspace-api/landlock.html) filesystem allowlist + `setrlimit` resource caps — **no Docker required**. For hosts where Docker isolation is unavailable or broken (e.g. snap-docker + AppArmor). | Linux 5.13+ only (Landlock). Best-effort downgrade across ABI 1–5. macOS/Windows: documented no-op ⇒ behaves like `none`. | **No** — see [Honest limitations](#honest-limitations) |
39
-
| `none` | No confinement; the server runs directly on the host. | Everywhere. | n/a |
38
+
| `sandbox` | Runs the server **natively** under a Linux [Landlock](https://docs.kernel.org/userspace-api/landlock.html) filesystem allowlist + `setrlimit` resource caps — **no Docker required**. For hosts where Docker isolation is unavailable or broken (e.g. snap-docker + AppArmor). Confines **writes** only: the server still runs as the proxy's user and can read the data directory and use the admin socket (see [Honest limitations](#honest-limitations)). | Linux 5.13+ only (Landlock). Best-effort downgrade across ABI 1–5. macOS/Windows: documented no-op ⇒ behaves like `none`. | **No** — see [Honest limitations](#honest-limitations) |
39
+
| `none` | No confinement; the server runs directly on the host as the proxy's user, with the data directory and the admin socket reachable. | Everywhere. | n/a |
40
40
41
41
```json
42
42
{
@@ -454,6 +454,7 @@ Your options on such a host:
454
454
-**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.
455
455
-**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`.
456
456
-**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`.
457
+
-**`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 `<data-dir>/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/<pid>/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).
457
458
-**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).
|`docker`| Wraps the server in a Docker container (process/FS/network isolation, resource limits). The default and most capable mode. | Any host with a working Docker daemon. | Yes (container user) |
35
-
|`sandbox`| Runs the server **natively** under a Linux [Landlock](https://docs.kernel.org/userspace-api/landlock.html) filesystem allowlist + `setrlimit` resource caps — **no Docker required**. For hosts where Docker isolation is unavailable or broken (e.g. snap-docker + AppArmor). | Linux 5.13+ only. macOS/Windows: documented no-op ⇒ behaves like `none`. |**No** — see [Honest limitations](#honest-limitations)|
36
-
|`none`| No confinement; the server runs directly on the host. | Everywhere. | n/a |
35
+
|`sandbox`| Runs the server **natively** under a Linux [Landlock](https://docs.kernel.org/userspace-api/landlock.html) filesystem allowlist + `setrlimit` resource caps — **no Docker required**. For hosts where Docker isolation is unavailable or broken (e.g. snap-docker + AppArmor). Confines **writes** only: the server still runs as the proxy's user and can read the data directory and use the admin socket (see [Honest limitations](#honest-limitations)). | Linux 5.13+ only. macOS/Windows: documented no-op ⇒ behaves like `none`. |**No** — see [Honest limitations](#honest-limitations)|
36
+
|`none`| No confinement; the server runs directly on the host as the proxy's user, with the data directory and the admin socket reachable. | Everywhere. | n/a |
37
37
38
38
```json
39
39
{
@@ -424,6 +424,7 @@ See [Shutdown Behavior](/operations/shutdown-behavior) for detailed subprocess l
424
424
-**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.
425
425
-**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`.
426
426
-**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`.
427
+
-**`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 `<data-dir>/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/<pid>/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).
427
428
-**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.
-**Always set a non-empty `api_key`.** The REST API enforces it, but a network-reachable instance with an obvious or empty key is one `curl` away from full tool execution.
301
301
-**Put a firewall in front.** If only one workstation needs access, prefer binding to a single LAN IP (e.g. `"listen": "192.168.1.10:8080"`) or restrict port 8080 in `ufw`/`firewalld` to known source addresses.
302
302
-**Consider `require_mcp_auth: true`** if you also want the `/mcp` endpoint to require the API key. It's `false` by default for AI-client compatibility.
303
+
-**Headless? Set `enable_socket: false` and use `docker` isolation for stdio servers.** The Unix socket in the data directory trusts every same-user connection without a key, and a stdio server under isolation `none` or `sandbox` runs as the service user: it can read `mcp_config.json` and `config.db` and mint agent tokens over the socket. The tray is the only socket client, so a headless service loses nothing by turning it off; `docker` isolation keeps the data directory out of the server's view. `mcpproxy doctor` warns about the combination. See [What the sandbox does not hide](/features/sandbox-isolation#what-the-sandbox-does-not-hide).
303
304
-**Don't expose to the public internet without TLS in front.** Run nginx/Caddy/Traefik as a reverse proxy with HTTPS, or tunnel via Tailscale/WireGuard/SSH. The built-in HTTP server is not designed to be a public ingress.
Copy file name to clipboardExpand all lines: docs/socket-communication.md
+1Lines changed: 1 addition & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -33,6 +33,7 @@ MCPProxy uses platform-specific local IPC for secure, low-latency communication
33
33
-**Socket/Pipe connections**: Trusted by default (skip API key validation)
34
34
-**TCP connections**: Require API key authentication
35
35
-**Middleware**: `internal/httpapi/server.go` checks connection source via context
36
+
-**Trust boundary**: "trusted by default" means every process running as the same user — including the stdio MCP servers mcpproxy itself spawns under isolation `none` or `sandbox` (Landlock confines writes only). Such a server can mint agent tokens over the socket without a key. On headless installs set `enable_socket: false` (the tray is the only socket client); run stdio servers you do not trust under `docker` isolation, which does not mount the data directory. `mcpproxy doctor` reports the combination. See [What the sandbox does not hide](/features/sandbox-isolation#what-the-sandbox-does-not-hide).
returnfmt.Sprintf("%d enabled stdio server(s) run under isolation none or sandbox while enable_socket is true (%s): "+
61
+
"a stdio server runs as the proxy's user, can read the data directory (mcp_config.json holds api_key, config.db holds OAuth tokens) "+
62
+
"and can mint agent tokens over the admin socket in that directory without a key; the sandbox mode confines writes only. "+
63
+
"Set enable_socket: false on headless installs and use docker isolation (docker_isolation.mode: \"docker\", or per-server isolation.mode) for servers you do not trust. "+
64
+
"See https://docs.mcpproxy.app/features/sandbox-isolation/",
0 commit comments