Architecture¶
This document summarises how Warden works. For the full design including non-goals and rationale, see the full design doc.
Component overview¶
MCP Client (Claude / IDE / agent)
│
│ stdio
▼
┌───────────────────────────────────────────────────────┐
│ warden CLI │
│ run · trace · init · logs │
└───────────┬───────────────────────────────┬───────────┘
│ │
▼ ▼
┌─────────────────┐ ┌───────────────────┐
│ Policy Engine │ │ Egress Proxy │
│ (parse, validate│ │ (hostname allow- │
│ resolve paths) │ │ list, DNS block) │
└────────┬────────┘ └─────────┬─────────┘
│ │
▼ │
┌─────────────────┐ │
│ Sandbox Backend │◄────────────────────┘
│ (bwrap / │ Unix socket /
│ Seatbelt / │ loopback TCP
│ Docker / │
│ AppContainer) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ MCP Server │ (sandboxed process)
│ process │
└─────────────────┘
│
▼
┌─────────────────┐
│ Audit Logger │ (JSONL: ~/.local/state/warden/audit.jsonl)
└─────────────────┘
Backend selection¶
warden run auto-selects the best available backend based on the host OS:
| OS | Preferred backend | Fallback |
|---|---|---|
| Linux | bubblewrap (bwrap) |
Docker |
| macOS | sandbox-exec (Seatbelt) |
Docker |
| Windows | AppContainer + WFP + ETW | Fail closed |
| Other | Docker | Fail closed |
Force a specific backend with --backend linux|seatbelt|docker|windows.
Docker is never preferred over a working native backend.
How each backend works¶
Linux — bubblewrap (bwrap)¶
Warden invokes bwrap with a carefully constructed argument list:
- Namespaces:
--unshare-user --unshare-ipc --unshare-pid --unshare-netgives the process isolated user, IPC, PID, and network namespaces. The process runs as UID/GID 0 inside the namespace, but cannot escape. - Runtime base:
/usrand/lib64are bind-mounted read-only so dynamically-linked binaries can execute. On merged-/usrsystems both must be bound explicitly because bwrap does not follow symlinks across bind mounts. - Pseudo-filesystems:
/dev,/proc, and a fresh/tmp(tmpfs) are mounted. - Policy grants: Each
filesystem.readpath is--ro-bind'd; eachfilesystem.writepath is--bind'd. Paths inside the runtime base cannot be granted write access. - Proxy bridge: The Warden binary is bind-mounted to
/.warden/proxy-bridgeinside the namespace and run with__proxy-bridgeto expose a loopback HTTP proxy on127.0.0.1:18080. The host-side egress proxy connects via a bind-mounted Unix socket at/.warden/host-proxy/egress.sock. - Environment: Only names in
env.alloware forwarded; all others are discarded. Proxy variables (HTTP_PROXY,HTTPS_PROXY,ALL_PROXY) are injected automatically. - Auditing: If a logger is configured, Warden wraps the bwrap
invocation in
strace -f -e trace=%file,%network, writing syscall traces to a temp file that is imported into the audit log after the sandbox exits. (On Windows the equivalent audit source is the real-time ETW session; on macOS the structured network events come from the egress proxy.)
macOS — sandbox-exec (Seatbelt)¶
Apple's sandbox-exec enforces a generated Seatbelt profile. The profile
denies all filesystem and network access by default; grants are added for
each policy-allowed path and for loopback TCP to the egress proxy bridge.
File denies produce EPERM; structured audit events come from the egress
proxy. Seatbelt is deprecated by Apple but remains functional.
Docker¶
When no native backend is available, Warden runs the command in a Docker
container with --network none. Host paths are bind-mounted deny-by-default
(only policy-granted paths are mounted). The in-container proxy bridge is
the current Warden ELF binary (Linux). On macOS/Windows hosts, set
WARDEN_DOCKER_BRIDGE to a cross-compiled Linux binary. The default image
is alpine:3.20 (override with WARDEN_DOCKER_IMAGE).
Windows — AppContainer + WFP + ETW (M5)¶
The backend runs the command under an AppContainer (LowBox) token derived
from the policy: filesystem capabilities are installed as DACL grants,
Windows Filtering Platform (WFP) rules on a per-run sublayer permit only
loopback traffic to the egress proxy and deny everything else outbound, a
Job Object enforces process-tree memory/time limits, and a real-time ETW
session audits Kernel-File activity for the process tree. Warden refuses to
run if any of these primitives fail to install — it never falls back to an
unrestricted process. Note that WFP and ETW both require an elevated (admin)
process; see docs/security.md for the known limitations.
Policy flow¶
policy.yaml
│
▼
Load(path) // Read file, decode YAML with KnownFields(true)
│
▼
ResolvePaths(dir) // Resolve relative paths against policy file's dir
│
▼
Validate() // Check semantic constraints (absolute paths, valid hosts, etc.)
│
▼
ResolveCommand(cli) // Merge CLI-specified command with policy.command
│
▼
BuildBwrapArgs(cmd, p) // Translate to backend-specific arguments
│
▼
spawn(process) // Execute with stdio passthrough
Key invariant: unknown YAML keys are rejected (KnownFields(true)), so
typos in policy files fail closed rather than being silently ignored.
Egress proxy architecture¶
The egress proxy is the only network出口 from the sandbox:
- On Linux, the sandbox gets a private network namespace — no default route,
no external interfaces.
HTTP_PROXY,HTTPS_PROXY, andALL_PROXYpoint to127.0.0.1:18080(the bridge inside the namespace). - The bridge accepts loopback TCP connections and forwards them over a bind-mounted Unix socket to the host-side proxy.
- The host-side proxy checks the hostname against
network.allowbefore performing any DNS lookup or TCP connection. Blocked requests return 403. - Allowed connections are forwarded to the upstream host. For HTTP CONNECT (HTTPS/TLS), the proxy hijacks the connection and bidirectionally pumps data.
- Every allow/deny decision is logged as a structured audit event.
DNS leakage is prevented because DNS resolution only happens inside the proxy, after the allowlist check. Direct DNS queries from the sandbox have no route.
Audit log flow¶
- Persistent log:
${XDG_STATE_HOME:-~/.local/state}/warden/audit.jsonl— append-only, mode0o600, written by the parent process. - Trace logs:
${XDG_STATE_HOME:-~/.local/state}/warden/traces/<timestamp>.jsonl— per-session logs created bywarden trace, used as input towarden init. - Linux strace temp:
/tmp/warden-strace-*.log— deleted after import.
Each event is a JSON object with fields: timestamp, type (file or
network), action, resource, allowed (bool), reason.
View with warden logs [--tail N] [--follow].
Limits enforcement (Linux)¶
limits.memory_mb is enforced by sampling the resident memory of the
launcher process group every 25 ms. limits.timeout_s is a wall-clock timer.
On breach:
- SIGTERM is sent to the process group.
- After 750 ms, SIGKILL is sent if the process has not exited.
- A structured
limitaudit event is logged.
Limits are not currently enforced on macOS or Docker backends.