Skip to content
StudioWNK
all field notes

· Wes Klaassen · updated

One tool per container: cheap isolation beats clever isolation

  • architecture
  • mcp

Every MCP stack starts the same way: one server, a handful of tools, all in one process because that’s easiest. Then the tools accumulate — and with them, one process holding every credential, every dependency tree, and every attack surface you have.

Blast radius is a design choice

Put an X API key, a brokerage token, and a market data subscription in one container and you’ve decided — implicitly — that a compromise of any tool is a compromise of all three. A malicious update in one tool’s dependency tree runs in the same process that holds every other tool’s secrets.

Split them and the decision reverses. Each tool gets its own image, its own credentials, its own egress allowlist, its own subdomain. The search tool’s dependencies can be as sketchy as the ecosystem makes them; they physically cannot read the data tool’s keys. A bug in one tool is an incident the size of one tool.

Production converged on the same shape

This isn’t a homebrew hunch. ToolHive, Stacklok’s production MCP runtime, runs every MCP server as what it calls a workload: one server per container, and — on by default for local servers — a network cage built per workload. Concretely, starting one isolated server creates:

  • a Docker network marked Internal: true — a blackhole with no route out;
  • a dnsmasq sidecar that becomes the server’s only DNS resolver;
  • a Squid egress sidecar that is the only path off the box, enforcing an allowlist of hosts and ports and ending in http_access deny all;
  • for HTTP transports, a second Squid running as the reverse proxy in front.

That’s roughly four containers per isolated server, which sounds extravagant until you notice what it buys: the allowlist itself ships with the server. ToolHive’s curated registry attaches a permission profile — which hosts, which ports, which filesystem mounts — to each catalog entry, alongside sigstore provenance for the image. Least privilege arrives as metadata, not as an afternoon of your judgment.

The lesson I take from it: when a team builds this for production, they don’t reach for clever kernel-level tricks. They reach for containers, an internal network, and a proxy with a deny-all tail — the same cheap primitives available to anyone with a compose file.

The personal-scale version

My stack is the flattened form of that design. Same invariants, fewer moving parts:

  • One tool per container, each its own image, subdomain, and secrets — same as a ToolHive workload.
  • One shared egress sidecar instead of a Squid per workload: a single squid with a listener per tool, each enforcing that tool’s own default-deny domain allowlist, all denials landing in one audit log. At five to twenty tools, per-tool listeners on one proxy give you the per-workload policy without N proxy containers.
  • Compose profiles instead of a CLI and operator: every tool carries a profile named after itself, and COMPOSE_PROFILES decides what a deployment builds. The egress wall and approval sidecar carry no profile — substrate runs always.

The allowlists don’t arrive from a curated registry; I write them. That’s the honest cost of a personal stack — and also the point. Each list is a handful of lines saying exactly which tool may reach exactly which hosts, reviewable in a diff.

The hard rule: tools never call each other

Isolation dies the day tool A gets tool B’s URL. RPC between tools quietly rebuilds the monolith — now with network hops — because A’s compromise becomes B’s compromise via a channel no allowlist inspects.

ToolHive backs this up by omission: it has no server-to-server channel at all. Workloads are independent and client-facing; when servers need to be composed, a gateway in front fans out to each of them — the servers still never talk to each other. And its shared bridge network, where non-isolated containers can reach each other, is exactly the accident the per-workload cage exists to prevent.

But tools genuinely need to cooperate. A data tool ingests market history; a backtesting tool needs to read it. The answer that preserves isolation is an artifact plane: a shared volume where one tool writes files in a documented format and another reads them.

  • Exactly one writer. No negotiation, no locking, no merge conflicts.
  • The format is the contract. Ideally one that already exists — writing the backtester’s own on-disk format means the reader needs zero custom code and the contract is documented by someone else.
  • The dependency stays soft. Reader without writer sees an empty lake and says so. Writer without reader fills a lake nobody drains. Neither crashes, neither knows the other exists.

No credentials cross the boundary. No network path connects the two. The worst a compromised writer can do to the reader is produce bad files — which the reader must validate anyway, because that’s what files are.

What the boundary honestly buys

Two limits worth stating plainly, because production states them too.

An HTTP proxy allowlist only binds clients that honor it. ToolHive’s own source says as much: the egress proxy constrains cooperative traffic, while a process that ignores HTTP_PROXY is stopped by the internal network having no route out. The proxy is the policy; the blackhole network is the wall. Both layers, always — which is why the check that matters is the bypass test: from inside the tool’s container, try to reach the internet without the proxy, and confirm the packets die.

And a container reads its own environment. Secrets injected as env vars — ToolHive’s model and mine — are readable by the tool that holds them, so a compromised tool still loses its own credentials. Isolation doesn’t remove that failure; it bounds it. One tool’s worth of secrets, one tool’s worth of egress, one tool’s worth of incident.

Where this stops scaling (and why that’s fine)

ToolHive shows exactly what growing up looks like: a Kubernetes operator, a CRD taxonomy, OIDC in front of every server, Cedar authorization policies, OTLP telemetry, audit middleware. That’s the shape this pattern takes when it has to serve teams.

A personal stack needs none of it, because the part that does the protecting was never the enterprise tail — it’s the boring core both versions share: one tool per box, a default-deny wall, one writer per volume, zero tool-to-tool calls. Isolation you can explain in a sentence and verify with docker network inspect.

Clever isolation is a research project. Cheap isolation is a compose file. Take the compose file.