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
Enforce a trusted host-path mount policy before launching container MCP servers (#10928)
Container-backed MCP servers were launched with whatever mount arguments
upstream configuration supplied, validated only syntactically
(`source:dest:mode` shape). Since the gateway is the component that
actually starts the backend process, it should independently decide
which host paths may be exposed — configurations can originate from
other producers or older compiler versions.
## Changes
**`internal/launcher/mount_policy.go` (new)**
- **Typed policy boundary**: `MountPolicy` / `MountRoot` (root path +
explicit `Writable` flag) is owned by the `Launcher` and never derived
from MCP server configuration.
- **Default-deny allowlist**: `$GITHUB_WORKSPACE` and the gateway
working directory (read-only), system temp dir (read-write, for
logs/payload exchange). Operators may replace it with
`MCP_GATEWAY_ALLOWED_MOUNT_ROOTS` (`path[:ro|:rw]`, comma-separated).
Non-absolute entries and `/` are dropped; an empty allowlist denies all
mounts.
- **Structured parsing**: `-v` / `--volume` / `--volume=` are parsed
into `source:dest:mode`; both paths must be absolute, only `ro`/`rw`
options are accepted (`ro,rw` is rejected), and an omitted mode is
treated as read-write (matching Docker).
- **Canonicalization**: host sources are symlink- and `..`-resolved
before the containment check, resolving the longest existing ancestor so
not-yet-created leaf directories still validate. Roots are ordered
most-specific-first, so a read-only root nested in a writable one
narrows access rather than inheriting it.
- **Bypass rejection**: `--mount`, `--volumes-from`, `--privileged`,
`--device`.
**`internal/launcher/launcher.go`**
- `launchStdioConnection` validates `serverCfg.Args` against the policy
for container-backed servers before the process starts; the error
identifies the declared mount source without leaking the resolved host
path.
**Docs**
- New "Host Mount Policy" section in `docs/CONFIGURATION.md`;
`MCP_GATEWAY_ALLOWED_MOUNT_ROOTS` added to the `README.md` / `AGENTS.md`
env lists; `config.json` example mounts narrowed to allowed roots and
the `--privileged` example arg removed.
## Behavior
```
$ awmg --config config.json # server mounting /etc
[LAUNCHER] server "custom-app": mount "/etc" rejected: host source is outside the allowed mount roots
```
Symlink escapes are caught after canonicalization, e.g.
`$GITHUB_WORKSPACE/link -> /etc` resolves outside the workspace root and
is rejected. Writable mounts under a read-only root
(`$GITHUB_WORKSPACE:/workspace:rw`) are rejected as well.
Tests cover allowed workspace/temp mounts, disallowed host paths,
symlink and traversal escapes, malformed declarations, env-override
precedence, runtime-argument bypasses, and end-to-end rejection before
process launch.
<!-- START COPILOT CODING AGENT SUFFIX -->
- Fixes#10927
-`MCP_GATEWAY_AGENT_ID` - Used by environment validation (`--validate-env`) and containerized startup checks; to enable auth set `gateway.agentId` (commonly `"${MCP_GATEWAY_AGENT_ID}"` in JSON stdin config)
411
411
-`MCP_GATEWAY_API_KEY` - *Deprecated alias for `MCP_GATEWAY_AGENT_ID`*; still accepted with a deprecation warning (lower precedence when both are set). Use `MCP_GATEWAY_AGENT_ID` instead.
-`MCP_GATEWAY_ALLOWED_MOUNT_ROOTS` - Comma-separated allowlist of host roots (`path[:ro|:rw]`, default `ro`) that container-backed MCP servers may bind-mount. Overrides the default roots (`$GITHUB_WORKSPACE` and working directory read-only, system temp dir read-write). Enforced by the launcher immediately before container launch.
-`MCP_GATEWAY_ALLOWED_MOUNT_ROOTS` — comma-separated allowlist of host roots (`path[:ro|:rw]`) that container-backed MCP servers may bind-mount (default: `$GITHUB_WORKSPACE` and working directory read-only, system temp dir read-write)
86
87
-`MCP_GATEWAY_FORCE_PUBLIC_REPOS` — when `true` (default), auto-forces `repos="public"` allow-only policy when workflow repo is public
-**Enforced at launch time**: before a container-backed MCP server is started, the launcher independently validates every mount against a trusted host-path allowlist (see [Host mount policy](#host-mount-policy)). Mounts outside the allowed roots, symlink or `..` escapes, and read-write mounts under read-only roots are rejected with a configuration error.
173
+
174
+
### Host Mount Policy
175
+
176
+
The gateway applies a default-deny host-path policy immediately before launching a container-backed (stdio) MCP server. The allowlist is owned by the launcher and is never derived from MCP server configuration.
177
+
178
+
Default allowed roots:
179
+
180
+
-`$GITHUB_WORKSPACE` (read-only)
181
+
- The gateway working directory (read-only)
182
+
- The system temporary directory, e.g. `/tmp` (read-write; used for gateway logs and large payload exchange)
183
+
184
+
Enforcement rules:
185
+
186
+
- Host sources are canonicalized (symlinks and `..` components resolved) before the allowlist check.
187
+
- Mounts whose canonicalized source is outside every allowed root are rejected.
188
+
- Read-write mounts (`:rw`, or a declaration with no mode) are only permitted under roots explicitly marked writable.
189
+
- Container runtime options that bypass structured mount declarations are rejected: `--mount`, `--volumes-from`, `--privileged`, and `--device`.
190
+
191
+
Operators can replace the default allowlist with `MCP_GATEWAY_ALLOWED_MOUNT_ROOTS`, a comma-separated list of `path[:ro|:rw]` entries (default `ro`), for example:
0 commit comments