Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Warden

License: MIT Node MCP

A sandboxed, egress-controlled MCP server. It exposes tools that let an agent (Claude, or any MCP client) execute shell commands, but every execution happens inside a locked-down, ephemeral Docker container instead of on the host, with no network access by default, no secrets baked into the image, and a full audit trail written before the command runs.

Built as a direct extension of TIF Score MCP, the first MCP server I authored. That one exposed trusted, internal scoring logic. This one is built for the harder case: giving an agent the ability to run arbitrary commands, where the interesting engineering problem is containing what happens if that ability is misused.

Demo

Every claim below was checked against a real, running Docker container in the same session this repo was built — not assumed working. The clip below is real captured terminal output, not staged.

Real terminal output: warden's sandbox verification checks running and passing against a live Docker container

A short narrated video walkthrough (same real footage, with captions) is at docs/demo/walkthrough-video.mp4.

Tools

Tool What it does
run_sandboxed Runs a shell command inside a locked-down container and returns stdout/stderr/exit code.
list_audit_log Returns the last N execution attempts and their results.
set_egress_allowlist Updates which domains a sandbox may reach. Empty by default — no outbound access.

Setup

npm install
cp .env.example .env    # fill in sandbox image / limits, never commit this file
docker pull alpine:3.20 # or your chosen base image
npm start                # runs the MCP server over stdio

Register it in your MCP client config (e.g. Claude Desktop claude_desktop_config.json, or a project-scoped .mcp.json — see this repo's own .mcp.json for a working example):

{
  "mcpServers": {
    "warden": {
      "command": "node",
      "args": ["/absolute/path/to/warden/src/index.js"]
    }
  }
}

Security model

Every control below is implemented in src/sandbox.js and src/egress.js, not just described here — read those files, they're short and commented inline with the reasoning for each flag. Every one of them was also verified against a real running container this session; see Project docs below for the evidence and the two real bugs that verification actually found and fixed.

  • No network by default. A sandbox can't exfiltrate data or call out to an attacker-controlled host unless a network mode is explicitly requested, and even then only through a domain allowlist.
  • Non-root, capabilities dropped. Runs as UID 1000 with --cap-drop=ALL and --security-opt=no-new-privileges. A compromised process inside the sandbox has no privileged Linux capabilities to escalate with.
  • Read-only root filesystem. Only a 64MB tmpfs scratch directory is writable. Nothing persists past the container's lifetime.
  • Resource limits. Memory, CPU, and PID caps prevent a runaway or fork-bombing process from affecting the host. Memory-swap is pinned equal to the memory limit, so there's no implicit 2x headroom from Docker's default swap behavior.
  • Hard timeout. A wall-clock kill switch independent of the process's own behavior — the container is force-stopped by name, not just the local docker client process, since a killed client doesn't otherwise guarantee the container stops.
  • Secrets never touch the image. All configuration (sandbox image name, resource limits, timeout) comes from environment variables via .env, which is gitignored. Nothing is hardcoded, nothing is logged.
  • Audit-before-execute. Every call to run_sandboxed is logged with its full parameters before the container starts, so a killed or crashed sandbox still leaves a record of what was attempted — the same gate-before-action pattern used in production TCS agents, applied here at the sandbox-execution layer.

Tradeoffs (the buy-vs-build reasoning)

Why Docker isolation and not a microVM (Firecracker, gVisor)?

Docker's isolation is namespace- and cgroup-based: it shares the host kernel. A microVM sandbox (Firecracker, as used by AWS Lambda and Fly.io; gVisor, used by Google Cloud Run) runs a minimal guest kernel per sandbox, which closes off an entire class of kernel-exploit escapes that Docker isolation doesn't.

For this project, Docker was the right call, not because it's equally safe, but because the threat model is bounded: this isn't running arbitrary untrusted code from the public internet at scale, it's running agent-requested commands in a controlled, single-operator context, where the combination of no-network-by-default + dropped capabilities + read-only filesystem + resource limits closes the realistic attack surface for that context. Reaching for Firecracker here would be solving a problem this deployment doesn't have yet, at real cost in setup and operational complexity.

The kill criterion for that decision: if this ever ran commands from untrusted, external, internet-facing input instead of a single operator's own agent, that's the point where the extra week for a microVM layer becomes worth spending, and I'd revisit this file and swap the isolation layer rather than patch around it.

Not yet built (documented honestly, not hidden)

  • The egress allowlist is stored and served to warden-cli allowlist, but the actual iptables/proxy enforcement on the warden-egress Docker network is not wired up yet — this is the next real piece of work, not a finished control.
  • No persistent identity/auth layer on the MCP server itself; it assumes a trusted local MCP client (Claude Code/Desktop), the same trust boundary as tif-score-mcp.

CLI

node cli/warden-cli.js status       # list running sandboxes
node cli/warden-cli.js logs 50      # last 50 audit log entries
node cli/warden-cli.js kill <id>    # force-kill a running sandbox
node cli/warden-cli.js allowlist    # show current egress allowlist

Project docs

Everything from the verification session that built and hardened this repo — nothing summarized away:

  • docs/BEHIND_THE_SCENES.pdf — the full build story, plain language, plan to finish.
  • docs/learning/ — technical write-ups of every real bug verification found: a container-leak on timeout, an implicit 2x memory-swap headroom, and a follow-up adversarial pressure-test pass (fork bombs, corrupted logs, unbounded stdout, misreported containers) that found and fixed four more, all with before/after evidence.
  • docs/evidence/ — raw captured command output backing every claim above.
  • docs/plan/PLAN.md — the plan this session was executed against.
  • docs/skills-used/ — what built this, and how.
  • scripts/ — the standalone verification scripts themselves; rerun them yourself against your own Docker daemon.

Stack

Node.js, @modelcontextprotocol/sdk, Zod, Docker CLI (invoked via child_process, no Docker Engine API dependency).

About

Sandboxed, egress-controlled MCP server. Runs agent-requested shell commands inside a locked-down, ephemeral Docker container: non-root, capabilities dropped, read-only fs, resource-limited, no network by default, audit-logged before execution. Every security claim verified against a live container, not just documented.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages