Sandboxing MCP Servers in a Regulated Multi-Tenant SaaS: Two Tiers That Actually Hold Up

Sandboxing MCP Servers in a Regulated Multi-Tenant SaaS: Two Tiers That Actually Hold Up

Sandboxing MCP Servers in a Regulated Multi-Tenant SaaS: Two Tiers That Actually Hold Up

MCP server sandboxing is not a single setting. It is a chain of boundaries, and the question “which sandbox should I use” has a different answer depending on whose data the server handles.

If you are still setting up the agent itself, start with our private AI agent setup guide. Before adding MCP servers to the agent’s tool surface, see which AI agent tools to enable first. This post is for the next step: once MCP servers are in the platform, what runtime isolates them, and how do you prove it to a compliance reviewer.

In Agent Setup reviews, the first thing we map for a regulated multi-tenant deployment is the data sensitivity of each MCP server. The sandbox choice follows from that map. Two tiers cover almost every regulated SaaS deployment. A third tier is optional.

Why “just pick a sandbox” is the wrong frame

A SOC 2 / ISO 27001 / PCI / HIPAA audit does not accept “we used a sandbox.” It asks for the sandbox technology, the version, the policy files, the audit log, and the per-tenant isolation evidence.1 The OWASP MCP Security Cheat Sheet is explicit on the contract: sandbox every local MCP server, restrict filesystem, disable network unless needed, prefer stdio transport for local servers, and separate sensitive servers from general-purpose ones.1

The mistake is to pick a sandbox technology first (“we will use Firecracker”) and only later discover that the deployment did not actually produce a per-server permission profile, did not run a per-tenant egress allowlist, did not record an audit log keyed on a tenant identifier, and did not produce the evidence package the buyer’s compliance team will ask for.

The order has to be:

Then the sandbox choice falls out.

The two-tier decision

The smallest set of sandbox tiers that survives a SOC 2 / ISO 27001 review for a regulated multi-tenant SaaS is two, plus an optional third:

The rest of this post is the per-tier checklist and the boundary controls the tier alone does not give you.

Tier 1 — Firecracker microVM with NSJail inner sandbox

Firecracker is a microVM VMM that runs each guest as a hardware-virtualized VM with its own kernel.2 The threat-containment model is nested trust zones: the guest vCPU is least-trusted, the host kernel is most-trusted, and the separation is enforced by KVM plus the jailer.2 Throughput is roughly five microVMs per host-core per second, with a 128 MiB minimum memory per guest.2

The jailer is a short-lived root-privileged process that builds a mount namespace, optionally a network namespace and PID namespace, chroots the Firecracker binary in place, drops uid/gid on exec, and then execs into the jailed Firecracker VMM.2 The jailed Firecracker process is the one that sets PR_SET_NO_NEW_PRIVS=1 and loads the per-thread BPF seccomp filter before any guest code runs.2

For Pattern A buyers (regulated multi-tenant SaaS), the inner layer is NSJail: a per-call process wrapped in a Linux namespace + seccomp-bpf filter, with empty root, only the bind-mounted paths the policy allows, and no NET_ADMIN. A single MCP-server exploit gets a one-shot NSJail container with a default-deny syscall filter; the caller’s MCP server cannot escalate to the host kernel.

Two jailer-era CVEs are worth recording in the buyer evidence package. CVE-2026-1386 (symlink in the jailer init, CVSS 6.0) is patched in v1.13.2 and v1.14.1.3 CVE-2026-5747 (virtio-pci out-of-bounds write, CVSS 8.7) is PCI-only and patched in v1.14.4 and v1.15.1.3 Production must pin to v1.15.1 or later.

One gap the upstream jailer does not close for itself: it does not call capset to bound its own capability set, does not install a seccomp filter on itself, and does not set PR_SET_NO_NEW_PRIVS on its own process.2 The buyer evidence package has to acknowledge this. A small wrapper shim that calls prctl(PR_SET_NO_NEW_PRIVS,1) then bounds the capability set to only what the jailer needs, then loads a minimal defense-in-depth seccomp filter before exec’ing the upstream jailer, closes the gap. The wrapper can verify itself by reading /proc/<pid>/status and asserting NoNewPrivs=1, CapBnd=0x0, and Seccomp=2.

Tier 2 — Wasmtime WASI Preview 2 capability sandbox

Wasmtime is a WebAssembly runtime that, on the WASI Preview 2 ABI, implements a capability-based security model with no ambient authority.4 The default state of a WASI component has no filesystem, no network, no environment variables, and no stdio. Every capability is explicitly granted by the host embedding through WasiCtxBuilder.4

The capability grant shape is what makes Tier 2 mathematically reasoning-able:

For the HTTP layer the canonical hook is WasiHttpHooks::send_request. The hook receives the already-built hyper::Request plus an OutgoingRequestConfig, so it can inspect the literal authority string, compare it against an allowlist stored in the host state, and either reject or delegate to default_send_request.4

The reference production MCP server on Wasmtime is wasmcp. It runs as a middleware chain of WebAssembly components with HTTP and stdio transports layered on top. No production Wasmtime MCP embedding today exposes a public FQDN allowlist at the wasi:http boundary, so a buyer integrating a first-party MCP server on Wasmtime in production must build a custom embedding and override WasiHttpHooks::send_request themselves.

Tier 2 is the right answer for first-party MCP servers because:

Tier 2 is the wrong answer for third-party MCP servers distributed as Docker / PyPI / npm packages. Those do not ship as WASI components. Rewriting them is a multi-quarter migration.

Optional Tier 3 — gVisor runsc for third-party MCP servers

For third-party MCP servers you cannot compile to WASI, gVisor runsc is the strongest container-runtime sandbox that does not require a microVM per workload. It runs the application in a user-space kernel (“Sentry”) that intercepts syscalls; the host kernel never sees raw syscalls from the sandboxed process.5 Memory overhead is 30–50 MiB per sandbox on top of application memory.5

The cold-start story is more honest than the marketing copy. Blog posts and AI overviews cite 50–100ms. The actual production number, confirmed by a gVisor collaborator in the upstream issue tracker, is closer to 300ms on current code:

“Startup time on the order of 300ms seems like a reasonable approximation of the current state of gVisor startup performance at the moment. I am not sure how the author in the blog post quoted in the OP gets the 50-100ms figure from, I’m not aware of gVisor ever hitting that.”

— EtiennePerot, gVisor collaborator, github.com/google/gvisor#131825

Two optimizations bring the warm-pool number down to roughly 24ms p50 with 1–3ms standard deviation: THP tuning (transparent_hugepage/defrag=defer) and a warm pool of pre-booted sandboxes dispatched via runsc exec.5 Plan for ~50 MiB RSS per warm slot and per-tenant pool partitioning.

There is one gVisor-specific CVE in the last 24 months: CVE-2025-2713 (HostLeak LPE, CVSS 6.8, EPSS 0.072%).5 Beyond that, gVisor structurally defends against ~96% of Linux kernel CVEs because the Sentry reimplements the syscall surface in Go.5

The honest Tier 3 fit: third-party MCP servers that cannot be compiled to WASI today, in deployments where the audit story does not require the hardware-backed boundary of Firecracker. Non-PII workloads in a regulated SaaS, with a per-server permission profile and an egress proxy in front.

Boundary controls you cannot skip

The sandbox alone does not satisfy OWASP MCP §3 and §5. Three boundary controls are mandatory on every tier.

Per-server permission profile

A per-server permission profile is the source of truth for filesystem, network, transport, and privilege. In ToolHive’s permission-profile schema it has five top-level fields:6

The profile is enforced via a Squid egress proxy + custom DNS container + HTTP_PROXY/HTTPS_PROXY env injection on the docker runtime.6 On the Wasmtime runtime, the same fields map onto WasiCtxBuilder::preopened_dir, socket_addr_check, and allow_tcp/allow_udp.

Transport must default to stdio for local servers, with host=127.0.0.1 to prevent the DNS-rebinding vector the MCP spec warns about for streamable-HTTP.7

Egress proxy with cloud-metadata deny-list

Every tier needs an egress proxy that explicitly denies cloud-metadata endpoints. The OWASP §5 SSRF defense is incomplete without it.

The canonical primary-docs-grounded control is a Squid proxy with a built-in to_linklocal deny rule. Squid’s default Recommended minimum Access Permission config includes http_access deny to_linklocal, which blocks 169.254.0.0/16 (AWS / Azure / GCP / Oracle / Alibaba IMDSv4) and fd00:ec2::/64 (AWS IMDSv6 on Nitro instances).8 The deny must be the first rule so a per-server allow rule cannot shadow it. The per-server allowlist (e.g. .api.github.com, .slack.com) comes next, then http_access deny all as the default-deny terminator.

Two gotchas the deployment has to respect: Cilium FQDN policies and Cilium Egress Gateway do not substitute for the IP-based deny (Cilium Egress Gateway does not affect local link-local, and FQDN matches DNS names not IMDS IPs), and AWS IMDSv2 token enforcement is a host-level iptables job, not a Squid job.8

OAuth-metadata SSRF defense

OAuth metadata discovery is the second SSRF surface. The MCP spec allows the client to fetch the OAuth protected-resource metadata from the URL the resource server returns in WWW-Authenticate: ... resource_metadata=..., or from the .well-known/oauth-protected-resource path.7 A compromised MCP server can return a metadata URL pointing at the cloud-metadata endpoint. The egress deny-list is the only thing that stops it.

The Anthropic MCP Security Best Practices also forbid token passthrough — an MCP server must not forward the user’s access token to a downstream resource server with the original audience and scope.7 Use RFC 8693 token exchange (act-on-behalf) to issue a downstream-scoped token at the gateway.

The Kubernetes gap you have to close yourself

If you deploy on Kubernetes, the ToolHive operator does not emit NetworkPolicy resources from MCPServer.spec.permissionProfile today. The reconciler manages ConfigMap, Secret, ServiceAccount, Role, RoleBinding, StatefulSet/Deployment only. There is no Owns(&NetworkPolicy{}) registration, no networking/v1 import, and no kubebuilder RBAC marker for networkpolicies.9

The maintainer’s stated reason is issue #1129, which has been open since July 2025 and was gated on #1127 (now Closed): a NetworkPolicy today would break the unauthenticated headless-service path some MCP transports still depend on.9

Two related issues affect the design:

Until the upstream gap closes, a regulated Kubernetes deployment must author NetworkPolicy external to the operator. The minimum viable set per MCPServer:

Each NetworkPolicy carries a toolhive.stacklok.dev/permission-profile-hash annotation so drift is detectable. A Validating Admission Webhook denies out-of-band edits that would override the operator’s baseline.

The evidence package a compliance team will actually ask for

For a regulated multi-tenant SaaS deployment, the buyer’s compliance team will ask for, at minimum, the following items:

  1. Sandbox boundary attestation. Sandbox technology per tier, exact version pin, primary-doc source for the hardening claims. For Firecracker, the evidence is the source-of-truth hardening attestation from docs/jailer.md plus the buyer-side shim’s /proc/<pid>/status snapshot showing NoNewPrivs=1, CapBnd=0x0, and Seccomp=2.2
  2. Per-MCP-server permission profiles. The full permissionProfile JSON for each registered server, signed at build time, with the build provenance recorded.
  3. Network-egress policy. The Squid ACL file (or eBPF program binary) with hash pinning. The file must include the cloud-metadata deny rule as the first line and the per-server dstdomain allowlist as the second line.
  4. Tool-schema pinning policy. Hash generation script, pin storage location, mismatch alert wiring. This is the OWASP §2 + §7 hook.
  5. OWASP MCP Top 10 coverage matrix. Explicit list of which OWASP MCP controls are realized by which layer (sandbox, gateway, egress, audit).
  6. Reconciliation drift runbook. For Kubernetes deployments, the runbook for NetworkPolicy drift, including SLI / SLO targets (p50 reconcile ≤ 500ms, p99 ≤ 5s, profile-hash-mismatch count = 0 in steady state).
  7. External pen-test report. At least one pen-test covering prompt injection → tool poisoning, SSRF, confused deputy, sandbox-escape attempts, and cross-server shadowing.
  8. OTel / SIEM integration. Tool call traces keyed on a tenant identifier, with audit-log events correlated to the same identifier.

Items 1–4 are needed to put MCP servers into production. Items 5–8 are needed to keep them in production.

Closing

Sandboxing MCP servers in a regulated multi-tenant SaaS is a chain of boundaries, not a single choice. The chain is:

Agent Setup can implement this for teams that want MCP servers in production without blurring the boundary between PII / regulated and non-PII workloads. The chain is not bureaucracy. It is how you make MCP servers useful without making their authority invisible.

If you are still working on the agent side, see the AI agent security checklist and which AI agent tools to enable first for the upstream steps.

Sources

Footnotes

  1. OWASP, “MCP (Model Context Protocol) Security Cheat Sheet,” §3 Sandbox and Isolate MCP Servers, §5 Input and Output Validation. https://cheatsheetseries.owasp.org/cheatsheets/MCP_Security_Cheat_Sheet.html 2

  2. Amazon Web Services, “Firecracker — Design Document” (Sandboxing / Jailer process) and “Firecracker — Jailer Documentation.” https://github.com/firecracker-microvm/firecracker/blob/main/docs/design.md https://github.com/firecracker-microvm/firecracker/blob/main/docs/jailer.md 2 3 4 5 6 7

  3. Amazon Web Services Firecracker security advisories — GHSA-36j2-f825-qvgc (CVE-2026-1386) GHSA-776c-mpj7-jm3r (CVE-2026-5747). https://github.com/firecracker-microvm/firecracker/security/advisories/GHSA-36j2-f825-qvgc https://github.com/firecracker-microvm/firecracker/security/advisories/GHSA-776c-mpj7-jm3r 2

  4. Bytecode Alliance, “Wasmtime — WASI Preview 2 Plugins” “Wasmtime — Security” WasiCtxBuilder API reference WasiHttpHooks trait reference. https://docs.wasmtime.dev/wasip2-plugins.html https://docs.wasmtime.dev/security.html https://docs.rs/wasmtime-wasi/latest/wasmtime_wasi/struct.WasiCtxBuilder.html https://docs.wasmtime.dev/api/wasmtime_wasi_http/trait.WasiHttpHooks.html 2 3

  5. Google, “gVisor — Performance Guide” “gVisor — Compatibility Guide” “gVisor — Security Track Record” EtiennePerot GitHub issue google/gvisor#13182 GitHub Advisory GHSA-4fj4-9m67-3mj3 CVE-2025-2713. https://gvisor.dev/docs/architecture_guide/performance/ https://gvisor.dev/docs/user_guide/compatibility/ https://gvisor.dev/security-track-record https://github.com/google/gvisor/issues/13182 https://github.com/advisories/GHSA-4fj4-9m67-3mj3 2 3 4 5 6

  6. Stacklok, “ToolHive — RunConfig and Permission Profiles.” https://raw.githubusercontent.com/stacklok/toolhive/main/docs/arch/05-runconfig-and-permissions.md 2

  7. Anthropic / Model Context Protocol, “Security Best Practices.” https://modelcontextprotocol.io/docs/tutorials/security/security_best_practices 2 3

  8. Squid Cache Project “http_access configuration directive v6” “SquidFaq/SquidAcl” Amazon Web Services “Instance metadata and user data — Limit access to the IMDS” Microsoft Azure “Azure Instance Metadata Service” Google Cloud “Compute Engine metadata server overview”. https://wiki.squid-cache.org/SquidFaq/SquidAcl https://github.com/squid-cache/squid/blob/master/src/cf.data.pre https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-metadata-limiting-access.html https://learn.microsoft.com/en-us/azure/virtual-machines/instance-metadata-service https://docs.cloud.google.com/compute/docs/metadata/overview 2

  9. Stacklok ToolHive GitHub issue #1129 (NetworkPolicy for Headless Services for MCP Servers) GitHub issue #4538 (Remove unused PermissionProfile from operator CRD) cmd/thv-operator/controllers/mcpserver_controller.go. https://github.com/stacklok/toolhive/issues/1129 https://github.com/stacklok/toolhive/issues/4538 https://raw.githubusercontent.com/stacklok/toolhive/main/cmd/thv-operator/controllers/mcpserver_controller.go 2