Skip to main content

Docker Is the Security Layer Your AI Agent Doesn't Know It Needs

· 9 min read
Andres

Last September, a developer opened their email client and sent a message to a client. Normal enough.

But somewhere in that email — invisibly, silently — a copy went somewhere else. Not to a spam filter. Not to a backup server. To an attacker.

They weren't hacked in the traditional sense. No phishing link. No password breach. The tool they were using to send the email — an AI tool, one they'd installed in about thirty seconds — had been quietly updated. And that update had one extra line of code.

This wasn't an obscure vulnerability. Thousands of developers were using the same tool. Almost none of them knew.

That's the story I want to unpack today.

The MCP Gold Rush

Right now, there is a protocol spreading through the AI ecosystem faster than almost anything I've seen. It's called MCP — Model Context Protocol.

The idea is beautiful in its simplicity: give your AI model real tools. Let it read your files. Query your database. Send emails. Book calendar events. Push code. Every major AI framework — Claude, LLM agents, Cursor — has adopted it. There are now thousands of MCP servers available: plugins for GitHub, Notion, PostgreSQL, Stripe, you name it.

It is, legitimately, one of the most exciting developments in AI right now.

But when a developer wants to add one of these tools, this is what that looks like:

{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "ghp_..." }
}
}
}

You paste that into a config file and restart the app. That's it.

Now — what is that actually doing?

npx is a Node.js command that goes to the internet, finds a package, and runs it. Right now. On your machine. With your permissions. No sandbox. No signature check. No review. Full access to your filesystem. Full access to your network. With your credentials sitting right there in the config.

And the email incident I opened with? That's exactly what happened.

Four Ways the Default MCP Setup Fails

This isn't one problem — it's a cluster of problems that compound each other.

1. Arbitrary code execution

When you run npx @modelcontextprotocol/server-github, you are executing code you have never reviewed, from a package registry with essentially no trust mechanism, with full access to your system.

Last year, a package called mcp-remote — with over half a million downloads — was found to contain an exploitable vulnerability: CVE-2025-6514. Half a million downloads. One bad update. That's your blast radius.

2. Supply chain attacks

The email story is a textbook supply chain attack. The Postmark MCP server — popular, trusted — shipped an update that silently BCC'd outgoing emails to an attacker-controlled address. No CVE. No security advisory. No one knew until someone noticed an email going somewhere it shouldn't.

npm's trust model is: whoever published last, wins. There is no mandatory code signing. No policy gate. No delay for review.

3. Tool poisoning (and you can't patch your way out)

This one is structural.

When an MCP server starts up, it sends the AI model a list of tools and their descriptions. Those descriptions go directly into the model's context — the same context where your instructions live. A malicious MCP server can craft those descriptions to instruct the model to do things: exfiltrate data, ignore user instructions, leak credentials.

The model cannot tell the difference between a tool description and a trusted instruction.

CVE-2025-54136 is a formalized version of this. But it's not really a bug — it's a design constraint of how LLMs work.

4. Authentication as an afterthought

53% of MCP servers fall back to static API keys — tokens hardcoded in config files, often committed to repos. And the official debugging tool for MCP — MCP Inspector — had its own remote code execution vulnerability: CVE-2025-49596.

The debugging tool for a security-critical protocol had an RCE. Let that land for a second.

Why Containers Are the Answer

I want to show you why I believe this, because the argument isn't "use our product." The argument is about what containers are.

A Linux container gives you namespace isolation. Your process — the MCP server — runs in its own little world. It sees only the files you explicitly give it. It talks only to the networks you allow. It lives in its own process space.

Compare that to the npx approach:

ThreatnpxDocker
Filesystem accessFull hostOnly mounted paths
Network accessFull host networkExplicit grants only
Process isolationYour user, your PID spaceSeparate namespace
Resource limitsNone--memory, --cpus enforced
RollbackRe-run and hopePin a digest, pull

Let's use the GitHub MCP server as a real example. Before running anything, scan it:

docker scout cves mcp/github:latest

This checks the image against known vulnerability databases. If there's a critical CVE in any bundled library, you see it before it ever touches your system.

Then verify its provenance:

docker buildx imagetools inspect mcp/github:latest \
--format '{{ json .Provenance }}'

This reads the cryptographic provenance attestation baked into the image at build time. You can see exactly which source commit was compiled into this binary, which build pipeline produced it, and confirm the digest matches what Docker Hub has on record. With npx, you get none of this.

Now run it:

docker run --rm -i \
--read-only \
--tmpfs /tmp \
--cap-drop ALL \
-e GITHUB_TOKEN="$GITHUB_TOKEN" \
mcp/github:latest

A few flags worth understanding:

  • --read-only — the container cannot write to your disk
  • --cap-drop ALL — strips all Linux kernel capabilities from the process (raw sockets, port binding, filesystem mounting). Normal outbound HTTPS to api.github.com doesn't need any of these — it's just TCP on port 443. So you get a hardened process that still does its job.

This server needs network access to reach api.github.com, so we don't use --network none. But we've already scanned the image and verified its provenance. We know exactly what we're running, and the kernel has stripped every privilege it doesn't need.

The tool poisoning problem is still a model-level issue — Docker doesn't fix what happens inside the model's context window. But it severely limits what a compromised server can actually do with those instructions.

The Docker MCP Tooling Available Today

Docker has been quietly building a native MCP ecosystem, and it's further along than most people realize.

There's a catalog of 270+ MCP servers available as signed OCI images on Docker Hub. Pre-built, scanned for vulnerabilities, with provenance attestations — a cryptographic record of exactly which code was compiled into that image.

And instead of manually writing the Docker command above, there's a CLI:

docker mcp install github

That one command finds the image, verifies it, and injects a hardened docker run config — with --read-only, --cap-drop ALL, and scoped mounts — into your Claude Desktop setup. The flags from the previous section are exactly what it produces. You can do it manually to understand what's happening, or let the CLI handle it. Either way, the security posture is the same.

The MCP Gateway

For teams, the piece that matters most is the MCP Gateway.

It's open source — a reverse proxy for your MCP servers. Instead of each MCP server implementing its own auth (and remember, 53% don't), the Gateway handles auth once, for all of them.

There's also a --block-secrets flag. Every payload passing through the Gateway gets scanned for credential patterns — tokens, API keys, passwords. If something looks like a secret, it blocks the request.

That's your audit trail. That's what lets you walk into a security review and say: here is every tool call the agent made, every argument it passed, every response it got. No more "the AI did something, we're not sure what."

What This Unlocks for Teams

The reason most teams can't get MCP approved by their security teams isn't technical — it's because the answer to "what can this thing access?" is currently "everything on the developer's machine." And the answer to "what did it do last Tuesday?" is "we don't know."

Containers change those answers:

  • "What can it access?" → Only the paths you mounted.
  • "What network calls did it make?" → Only the endpoints it was permitted to reach — or none at all for servers that don't need external access.
  • "Can we roll back?" → Yes. Pin the digest, restart.

The Gateway changes the audit answers:

  • "What did it do last Tuesday?" → Here are the logs.
  • "Were any secrets exposed?" → Blocked before they left the process.
  • "Can we enforce which tools the agent can call?" → Yes. Policy at the Gateway level.

The multi-tenant case is where this gets especially interesting. The MCP spec has no concept of tenant isolation. If you're building a product where multiple users share an MCP backend — which is where the ecosystem is heading — you have to build that boundary yourself. A container gives you that boundary. Each session gets its own container, its own mounts, its own network policy. When the session ends, the container dies. No state bleeds across users.

What to Do Next

MCP is genuinely exciting. The speed at which developers are building with it — I find it exciting. I use these tools. I think the direction is right.

But the default path — npx, no sandbox, full host access, hardcoded tokens — has already produced real incidents with real consequences.

The good news is the fix is not complicated. If you're running MCP tools today, in 15 minutes you can be running them in containers instead. The docker mcp install command handles the config. The catalog handles the images. The Gateway handles the audit trail.

If you're a security engineer who's been handed an MCP approval request and you don't know where to start, use the failure modes from this post as your threat model agenda.

And if you're building MCP servers — please, publish them as container images. With provenance. With signed digests. Your users are trusting you with access to their systems.


Resources: