Module B7 — Sandboxes and Execution Controls

Course: 2B — Securing & Attacking Harnesses and LLMs Module: B7 — Sandboxes and Execution Controls Duration: 60 minutes Level: Senior Engineer and above Prerequisites: Course 1 Module 5 (Sandboxing & Isolation); Course 2B B0–B6; DD-09 (NemoClaw), DD-19 (CrabTrap), DD-20 (IronCurtain)

Injection will succeed. The agent will be coerced into running code. The sandbox is the layer that decides whether that means "a confused log line" or "a reverse shell on your jump host." Design it as if the sandbox will be escaped — because the same vuln class that lets the agent run attacker code will, eventually, let that code walk out of the sandbox.


Learning Objectives

After completing this module, you will be able to:

  1. Place any sandbox technology on the isolation hierarchy — OS process isolation, containers (Docker, gVisor), microVMs (Firecracker), WASM/V8 isolates, and full VMs — and defend a choice by stating its blast radius, overhead, and escape surface.
  2. Name the four attack vectors against an agent sandbox — provider escape, network egress, resource exhaustion (OWASP ASI09), and sidecar/subprocess compromise — and apply the control for each.
  3. Implement a code-execution policy: allowlist-based command gating (and articulate why denylists fail), per-tool execution scopes, and deterministic rule compilation (the IronCurtain model), rejecting LLM-at-runtime enforcement for security-critical decisions.
  4. Enforce default-deny network egress at the network layer (distinct from the LLM-judge layer in CrabTrap), with per-task allowlisting, and explain why this is the single most under-applied control in shipped agent sandboxes.
  5. Operate a sidecar monitor that watches the sandbox for anomalous behavior — resource spikes, unexpected syscalls, repeated failed operations — and feeds B8's observability pipeline.
  6. Apply the blast-radius principle: assume the sandbox will be escaped, and design so that an escaped sandbox still has no credentials and no network reachability. This is the central design philosophy of the module.

Why this module exists

Course 1 Module 5 established the sandbox as a fact of harness engineering: any agent that runs shell, writes files, or calls APIs has a blast radius, and the inside-vs-outside-sandbox split plus filesystem/network scoping are the baseline controls. That module taught you how to build a sandbox. This module teaches you how the sandbox fails under an adversary who controls the agent's inputs, and what execution controls must sit inside and around it to survive that adversary.

The inversion from Course 1 is the same one that defines all of 2B: in Course 1 the threat was a misbehaving-but-honest agent (an over-long loop, a mis-scoped write); in 2B the threat is an agent under prompt-injection coercion, induced to run attacker-chosen code inside your sandbox. The difference is the adversary. A sandbox tuned to survive "the agent wrote to ~/.ssh by accident" is not tuned to survive "the agent was instructed to compile and run a C implant that exploits CVE-in-your-runtime." The controls are stronger, the threat model is adversarial, and the design philosophy inverts: you do not harden the sandbox to prevent escape; you harden the environment so that escape does not matter.

Three sub-sections, twenty minutes each:


B7.1 — The Sandbox Hierarchy and the Escape Threat

Where the sandbox sits on the isolation hierarchy, and the four ways an adversary controlling the agent will attack it.

The isolation hierarchy

A sandbox is a containment boundary between untrusted code and the rest of the system. Every sandbox technology is a point on a tradeoff curve between isolation strength (the blast radius if the contained code is hostile) and overhead (latency, density, operational cost). The hierarchy, weakest to strongest:

  1. OS process isolation. The agent runs in a separate process; isolation is the kernel's process boundary plus UID/gid separation. Cold start: instant. Blast radius: the entire user account, every file the UID can read, every syscall the UID can make. This is local bash in Course 1 Module 5. Fine for a single-tenant dev tool. Catastrophic for an agent running attacker code.

  2. Containers (Docker, Podman). Linux namespaces + cgroups + a layered filesystem. The process sees its own mount table, network namespace, PID tree. Cold start: seconds. Blast radius: the container's filesystem and the namespace's network — plus anything reachable through a container escape (kernel CVEs, misconfigured capabilities, privileged mode, mounted Docker socket). Default Docker is process isolation with a stronger boundary; it is not a security boundary against a kernel exploit. This is where most production harnesses start and, dangerously, where many stop.

  3. Hardened container runtimes (gVisor, Kata). gVisor implements a userspace kernel (runsc) that intercepts the sandboxed process's syscalls — the guest never touches the host kernel directly, so most kernel CVEs do not cross the boundary. Kata runs the container inside a lightweight VM. These raise the escape bar substantially at modest overhead (gVisor: ~10–30% on syscall-heavy workloads). For an agent sandbox that will run attacker-influenced code, plain Docker is a defect; gVisor or Kata is the floor.

  4. MicroVMs (Firecracker). A minimal KVM-based VM (AWS, used by Lambda and Fargate). ~125ms cold start, ~5MB memory footprint, a stripped device model (only virtio-net, virtio-block, serial). Blast radius: the VM, and the hypervisor's escape surface — which is small and aggressively audited. The isolation strength per dollar of overhead is the best in the industry; this is what you reach for when the sandbox will run genuinely untrusted code (E2B's cloud sandbox is Firecracker-based).

  5. WASM / V8 isolates (Wasmtime, isolated-vm). Language-level sandboxing. WASM modules and V8 isolates are confined to a linear memory and a capability-based import table; there is no syscall surface by default, only what the host explicitly exports. Cold start: milliseconds. Blast radius: the isolate's memory and the exported functions. This is what IronCurtain's "Code Mode" uses (isolated-vm) for its builtin agent's TypeScript. The escape surface is the runtime itself (V8/Wasmtime CVEs) and the host's exported functions — which you control.

  6. Full VMs (KVM, Hyper-V, ESX). The strongest isolation short of separate hardware. Blast radius: the VM. Escape surface: the hypervisor. Overhead: seconds of cold start, gigabytes of memory. Almost never the right answer for an agent sandbox — microVMs give you 95% of the isolation at 5% of the cost — but it is the correct answer when the sandbox runs code from mutually-distrusting tenants and a hypervisor escape would be catastrophic.

The reference implementation: NemoClaw's OpenShell (DD-09). OpenShell is NemoClaw's sandbox layer: the agent never touches the sandbox directly; every execution request goes through the governed API, which brokers access to an isolated execution environment. The architectural principle is the load-bearing one — the agent cannot reach the sandbox to reconfigure it — and the same principle applies to whichever technology you pick from the hierarchy above.

The blast-radius-vs-overhead tradeoff

The hierarchy is a curve. Moving down (stronger isolation) buys you a smaller blast radius at the cost of cold-start latency, memory density, and operational complexity. The correct point on the curve is determined by what code will run in the sandbox and who chose it:

Most shipped agent sandboxes sit at level 2 (plain Docker) regardless of what code they run. That is the defect this module exists to fix.

The four attack vectors

An adversary who controls the agent's inputs (via prompt injection, indirect injection, or a poisoned tool response) can coerce the agent into running attacker-chosen code inside the sandbox. Four vectors by which that code attacks the system, not just the sandbox:

1. Sandbox escape

The contained code exploits a bug in the sandbox provider itself to reach the host. Historical examples: V8 isolate escapes (type confusion in the JIT, prototype-pollution chains that reach across the isolate boundary), container breakouts (kernel CVEs like Dirty Pipe, CAP_SYS_ADMIN abuses, the mounted-Docker-socket footgun), gVisor syscall-emulation bugs. The vector succeeds when the boundary between sandbox and host is a piece of software with its own CVEs — and every boundary is.

The control is twofold: pick a stronger point on the hierarchy (Firecracker's escape surface is smaller than Docker's), and assume the escape succeeds — which is the blast-radius principle in B7.3.

2. Network egress from the sandbox

The sandbox runs the code fine — too fine. The code reaches the network: data exfiltration to an attacker-controlled endpoint, callbacks to a C2, lateral movement into the internal network the sandbox can reach, SSRF against the cloud metadata service (169.254.169.254). This is the most under-applied control in shipped agent sandboxes. The default in most container and even microVM deployments is some degree of network reachability, because the agent's tools (web search, API calls) need it.

The control is default-deny egress with per-task allowlisting (B7.2). The CrabTrap (DD-19) project governs egress at the LLM-judge layer; this module's concern is the network-layer default-deny that must hold even if the LLM judge is bypassed or absent.

3. Resource exhaustion (OWASP ASI09)

The contained code is induced to run indefinitely, fill the disk, exhaust memory, fork-bomb, or spawn unbounded subprocesses. OWASP ASI09 (Excessive Agency / Resource Exhaustion in agentic systems) is the formal framing: an agent coerced into while true: pass, or into downloading a multi-gigabyte file into /tmp, or into a tight loop that bills the LLM provider — all of these are the sandbox's problem because the sandbox is where the code executes.

The control is hard resource caps: CPU, memory, wall-clock, process count, disk-write quota — enforced by cgroups (container), by the hypervisor (microVM), or by the runtime (V8 fuel, WASM gas). A sandbox without resource caps is a sandbox that can be made to fail-open (OOM-kill the monitor) or fail-costly (run up the bill).

4. Sidecar / subprocess compromise

The sandbox spawns a helper process — a language server, a file-format converter, a package installer, a TLS-terminating proxy — that runs with more privilege than the contained code. The contained code attacks the helper, and through the helper reaches something the sandbox itself could not. The classic instance: a package installer that runs as root inside the container and is tricked into running a malicious postinst script from a typosquatted npm/PyPI package. IronCurtain's mitigating-proxy pattern (the agent's npm/PyPI installs go through a validating registry proxy, not the public registry) exists because of this vector.

The control is privilege minimization on every helper: no helper runs as root; every helper is itself sandboxed; package installation goes through a vetting proxy; the helper's IPC surface to the contained code is minimal and capability-scoped.


B7.2 — Execution Policies and Network Segmentation

What code the agent is allowed to run, and the network the sandbox is allowed to reach — the two controls that contain an agent under coercion even before you get to monitoring.

Code execution policies

"Sandboxing" answers where code runs. An execution policy answers what code is allowed to run at all. The two are complementary and both are required. A sandbox without an execution policy will dutifully isolate whatever attacker-chosen code the agent was coerced into running; an execution policy without a sandbox will be bypassed the moment the policy checker has a bug.

Allowlists, not denylists

The foundational rule: allowlist, do not denylist. A denylist ("block rm, curl, wget, nc, python -c...") is a list of things you have thought of. An allowlist (ls, cat, grep, git status, make, pytest — the commands this specific task needs) is a list of things the task needs. The attacker's job against a denylist is to find one command you did not think of; against an allowlist, the attacker must find a way to make an allowed command do harm (argument injection, a vulnerable subcommand), which is a much smaller surface.

Denylists fail in characteristic ways. The curl block is bypassed by wget, then by python -c "import urllib", then by perl, then by /lib/x86_64-linux-gnu/libc.so.6 being directly invokable, then by git clone of an attacker repo that runs a hook. Each addition to the denylist is one more command you have thought of; the space of "things that can fetch a URL or run arbitrary code" is unbounded. Allowlists invert the failure mode: the question becomes "can make be made to do something harmful?" — a bounded, auditable question.

Per-tool execution scopes

A monolithic "what can the sandbox run" allowlist is coarse. The stronger pattern is a per-tool execution scope: the run_tests tool may invoke pytest and make with specific argument patterns; the format_code tool may invoke prettier and black; the install_deps tool may invoke pip install but only through the validating proxy. Each tool carries its own allowlist; the sandbox enforces "the command must match the calling tool's scope" before exec. This is how you prevent a coerced agent from invoking pytest from inside the install_deps tool — the tool's scope is the enforcement.

The IronCurtain deterministic-enforcement model

How is the allowlist itself specified? Two schools, and they are the same debate that runs through DD-19 (CrabTrap) and DD-20 (IronCurtain):

For execution policies, the deterministic model is the correct default. The allowlist for a tool's command surface is a finite, enumerable set; there is no semantic question the LLM needs to answer at runtime. Compile the policy, verify it, and enforce it with string matching. Reserve LLM-as-judge (if you use it at all) for the arguments, and even then prefer structured argument validators (regex, schema, path-prefix checks) over an LLM. The principle, restated: security-critical decisions at runtime must be deterministic. The blast radius of a probabilistic enforcement layer is the set of inputs on which the LLM happens to be wrong.

Network segmentation

The sandbox should have no network egress by default. This is the single most under-appreciated control in shipped agent sandboxes, and it is the one that contains the worst outcomes.

Consider the failure mode: a sandboxed agent under coercion reads a secret from a mis-scoped file (filesystem scope defect, a separate bug), then exfiltrates it by curl-ing an attacker endpoint. Two bugs were required — the filesystem mis-scope and the network reachability. Kill either and the exfiltration fails. Operators obsess over the filesystem bug (because it is the one that coughs up the secret) and ignore the network bug (because the network "has to work, the agent needs web search"). The network bug is the easier one to fix and the one that contains the larger class of outcomes — not just secret exfiltration, but C2 callbacks, lateral movement, and SSRF against the metadata service.

The control is the default-deny egress with per-task allowlisting model:

This is distinct from CrabTrap. CrabTrap (DD-19) governs egress at the LLM-judge layer: every outbound request is evaluated by an LLM against a natural-language policy. That is valuable for the semantic question ("is this request something this agent should be making?") but it is the wrong tool for the existence question ("should this sandbox be able to reach the network at all?"). The network-layer default-deny answers the existence question deterministically; CrabTrap answers the semantic question probabilistically on top. You want both, and the network-layer control is the one that must hold when the LLM judge is bypassed, confused, or absent.

The cloud-metadata-service case deserves special mention. 169.254.169.254 (AWS/Azure/GCP metadata) hands out instance credentials to anything that can reach it. A sandboxed agent that can reach the metadata service can steal the host's cloud role — a far worse outcome than reading a local secret. Default-deny egress blocks this for free; an allowlist that does not explicitly list the metadata endpoint blocks it for free; an LLM-judge-only model is one prompt injection away from failing to block it. Block the metadata service at the network layer, always.


B7.3 — Sidecar Monitors and the Blast-Radius Principle

The monitor that watches the sandbox, and the design discipline that makes escape survivable.

Sidecar monitors

A sandbox with no monitoring is a sandbox whose compromise you discover from an incident report. A sidecar monitor is a process that observes the sandbox from outside — outside the sandbox's trust boundary, so the contained code cannot reach it to disable it — and flags anomalous behavior. The monitor is the early-warning layer that ties this module to B8 (Observability and Attack Detection).

The monitor watches four signal classes:

  1. Resource signals — CPU, memory, disk, network-byte, and process-count time series. A resource spike (the sandbox suddenly allocating 2GB, spawning 50 processes, or writing 10GB to disk) is the signature of ASI09 resource exhaustion or of an escape attempt in progress. Thresholds are per-task (a build tool is allowed to spike CPU; a read_file tool is not).

  2. Syscall signals (where observable — seccomp-bpf, eBPF, or gVisor's syscall log). The contained code's syscall distribution should match the task's profile. A grep invocation that issues socket, connect, and fork is not behaving like grep. eBPF (bcc, bpftrace, Falco) gives you kernel-level observability into the sandboxed process tree with negligible overhead.

  3. Network signals — connection attempts (allowed and denied), DNS queries, byte volumes. Denied connection attempts are especially informative: they are the agent (or the contained code) trying to reach something the egress policy blocked. A burst of denied connections to a single external host is a C2-attempt signature.

  4. Behavioral signals — repeated failed operations (a brute-force pattern), unusual command sequences (the agent invoking git from inside the pytest tool), unexpected file paths accessed. These overlap with execution-policy enforcement but the monitor's job is detection, not prevention: the policy blocks; the monitor logs and alerts.

The monitor's output feeds B8's observability pipeline — structured events, correlated with the agent's tool-call trace, so an incident investigator can reconstruct "the agent was coerced at step 14, ran this command at step 15, the sandbox denied egress at step 16, the monitor flagged it at step 17." The monitor is not a substitute for the controls in B7.2; it is the layer that tells you when those controls engaged on a real attack.

The blast-radius principle

This is the central design philosophy of the module, and it inverts the naive intuition.

The naive intuition is: harden the sandbox so it cannot be escaped. This is the wrong goal. Every sandbox is software; every sandbox has CVEs; every sandbox will, eventually, be escaped by an adversary who controls the code inside it. The V8 isolate, the Firecracker microVM, gVisor's runsc — all have had escape-class bugs, all will have more. Hardening reduces the probability of escape; it does not eliminate the outcome.

The blast-radius principle is: assume the sandbox will be escaped, and design so that an escaped sandbox still has no credentials and no network. The escape becomes a contained event — the contained code reaches the host, finds no credentials to steal, finds no network to reach, finds no privileged helper to suborn, and is killed by the resource caps before it can do anything useful. The escape is detected by the monitor (the process tree just grew unexpectedly; syscalls just appeared that the task profile does not include) and the sandbox is destroyed.

Three concrete realizations:

  1. No credentials in the sandbox. The sandboxed process runs with no API keys, no cloud-instance role reachability (network-layer block on the metadata service, per B7.2), no secrets in its environment or filesystem. This is IronCurtain's credential-quarantine model: the agent is configured with fake keys, and a TLS-terminating MITM proxy hot-swaps fakes for reals at the boundary. Even a fully-compromised, fully-escaped agent cannot exfiltrate real credentials because it does not have them. If your architecture requires credentials in the sandbox (the inside-sandbox model from Course 1 Module 5), you have accepted a worse blast radius — document the decision and add the other two realizations.

  2. No network from the sandbox. Even after escape, the contained code is in the sandbox's network namespace (container) or the microVM's virtual NIC — which is default-deny with per-task allowlists. An escape that reaches the host kernel does not magically gain a new NIC; it is still constrained by the network policy the namespace enforces. (An escape that reaches the host userspace and can reconfigure the network policy is a worse escape; the monitor should catch the policy-modification syscall.)

  3. No privileged helpers. Every subprocess the sandbox spawns runs with the minimum privilege to do its job — no root, no capabilities beyond what the task needs, no ambient authority. The contained code that escapes and tries to suborn a helper finds helpers that are themselves locked down.

The principle, compressed: the value of escape is what you can reach after escaping. Design so the answer is "nothing." Then the escape is a detected-and-killed anomaly, not an incident.


Code — a sandbox policy enforcer

The lab builds a single TypeScript module that enforces the three controls from B7.2: an allowlist gate, resource caps, and a default-deny egress gate. The shape (full implementation in 07-lab-spec.md):

// sandbox-policy.ts — deterministic enforcement. No LLM at runtime.
export type PolicyDecision = { decision: "allow" | "deny" | "escalate"; reason: string };

// (1) Allowlist gate: command must match the calling tool's scope.
export function checkCommandPolicy(
  tool: string, command: string, args: readonly string[],
  policy: CommandPolicy
): PolicyDecision {
  const scope = policy.toolScopes[tool];
  if (!scope) return { decision: "deny", reason: `no scope for tool ${tool}` };
  for (const pattern of scope.allow) {
    if (matchCommand(command, args, pattern)) {
      return { decision: "allow", reason: `matched ${pattern.name}` };
    }
  }
  return { decision: "deny", reason: `${command} not in allowlist for ${tool}` };
}

// (2) Resource caps: cgroup-style limits enforced before exec.
export interface ResourceCaps {
  readonly cpuSeconds: number;    // wall-clock kill
  readonly memoryMb: number;      // OOM-kill
  readonly processCount: number;  // fork limit
  readonly diskWriteMb: number;   // write quota
}
export function enforceCaps(caps: ResourceCaps): SpawnOptions { /* ... cgroups / ulimit ... */ }

// (3) Default-deny egress: per-task allowlist at the network layer.
export function egressGate(taskId: string, host: string, egress: EgressPolicy): PolicyDecision {
  const allow = egress.taskAllowlist[taskId] ?? [];
  return allow.some(h => host === h || host.endsWith("." + h))
    ? { decision: "allow", reason: `${host} in allowlist for ${taskId}` }
    : { decision: "deny", reason: `default-deny: ${host} not allowlisted for ${taskId}` };
}

The full lab wires this into an actual sandboxed execution: a script is run in a child process with cgroup-enforced caps, its outbound connections are intercepted by an egress gate, and every command is checked against the calling tool's scope. The sandbox-escape attempt at the end of the lab has the student try to reach the network from inside — and watch the egress gate deny it.


Anti-Patterns

Plain Docker as the security boundary

Running attacker-influenced code in a default Docker container and calling it "sandboxed." The Linux kernel is the boundary, and the kernel has CVEs. Cure: gVisor or Kata at minimum for agent code; Firecracker for genuinely untrusted code. The container is the packaging; the security boundary is what sits between the contained process and the host kernel.

Allow-all network egress ("the agent needs web search")

Defaulting the sandbox's network namespace to some reachability because a tool needs it. Cure: default-deny at the network layer; per-tool/per-task allowlist. The web-search tool reaches one host; that is not an argument for reaching the whole internet.

Denylist command policies

"Block curl, wget, nc, python -c." The attacker finds perl, then awk, then git clone with a hook. Cure: allowlist — the commands the task needs, validated against the calling tool's scope. The surface is bounded and auditable.

LLM-as-judge for the command allowlist

Evaluating each command with a runtime LLM and trusting its allow/deny. Cure: deterministic compilation (IronCurtain). Compile the policy offline; enforce with string matching at runtime. Security-critical runtime decisions must be deterministic.

Credentials in the sandbox environment

API keys in the sandboxed process's env, "because the agent needs them." An escape — or a filesystem mis-scope the agent was coerced into exploiting — reads them. Cure: credential quarantine (IronCurtain's fake-key swap) or the outside-sandbox architecture (Course 1 Module 5) where credentials never enter the sandbox.

No resource caps

A sandbox with CPU/memory/disk/process limits unset. The contained code is induced into while true or a multi-gigabyte download (ASI09). Cure: cgroup-enforced caps on every resource class, per-task. A sandbox without caps is a sandbox that can be made to fail-open by OOM-killing the monitor.

The unsupervised sidecar

A helper (package installer, format converter) that runs as root or with broad capabilities inside the sandbox. The contained code suborns it. Cure: every helper is itself sandboxed, runs with minimum privilege, and its IPC surface to the contained code is capability-scoped. Package installs go through a validating registry proxy.


Key Terms

Term Definition
Isolation hierarchy The spectrum of sandbox strength: OS process → container → hardened container (gVisor/Kata) → microVM (Firecracker) → WASM/V8 isolate → full VM. Blast radius vs. overhead.
Blast radius The scope of damage if the contained code reaches the host. The quantity the hierarchy trades off against overhead.
Sandbox escape Attack vector 1: the contained code exploits a bug in the sandbox provider (V8, the kernel, gVisor) to reach the host.
Network egress Attack vector 2: the contained code reaches the network. The most under-applied control; contained by default-deny.
Resource exhaustion (ASI09) Attack vector 3: the contained code runs indefinitely, fills disk, exhausts memory, fork-bombs. Contained by hard resource caps.
Sidecar compromise Attack vector 4: the contained code suborns a privileged helper the sandbox spawned. Contained by privilege minimization on every helper.
Allowlist policy The command-execution model: only enumerated commands run. Contrast denylist, which fails on the unbounded space of "commands you didn't think of."
Per-tool execution scope An allowlist scoped to the calling tool — pytest from the run_tests tool, not from install_deps.
Deterministic enforcement The IronCurtain model: policy compiled offline to JSON rules; zero LLM at runtime. Security-critical runtime decisions must be deterministic.
Default-deny egress The network policy: the sandbox reaches nothing by default; each task carries an allowlist. Enforced at the network layer, not the LLM-judge layer.
Sidecar monitor A process outside the sandbox's trust boundary that watches for anomalous behavior (resource spikes, unexpected syscalls, denied-connection bursts). Feeds B8.
Blast-radius principle Assume the sandbox will be escaped; design so an escaped sandbox has no credentials and no network. The central design philosophy.
Credential quarantine IronCurtain's fake-key-swap: the agent holds only fake keys; a MITM proxy substitutes reals at the boundary. An escaped agent has no real credentials to exfiltrate.
OpenShell NemoClaw's (DD-09) sandbox layer: the agent never touches the sandbox directly; execution goes through the governed API.
CrabTrap (DD-19) LLM-as-judge egress proxy. Governs the semantic egress question probabilistically; complements (does not replace) network-layer default-deny.
IronCurtain (DD-20) Deterministic-enforcement runtime. Credential quarantine + offline policy compilation + zero-LLM runtime. The reference defense architecture for this module.

Lab Exercise

See 07-lab-spec.md. "Build the Sandbox Policy Enforcer": students implement (a) an allowlist-based command-execution gate with per-tool scopes, (b) a resource-cap enforcer (CPU/memory/process/disk limits via cgroups/ulimits), and (c) a default-deny egress gate with per-task allowlisting. A closing demo has the student attempt to reach the network from inside the sandbox and watch the egress gate deny it. TypeScript, type-hinted, no GPU, ~60–75 minutes.


References

  1. OWASPTop 10 for Agentic Applications (2026). genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/. ASI07 (Insecure Output Handling — the injection vector that coerces the agent into running code) and ASI09 (Excessive Agency / Resource Exhaustion — vector 3 in this module).
  2. Course 1 Module 5 — Sandboxing and Execution Isolation. The inside-vs-outside split, the provider table, filesystem and network scoping. This module's prerequisite; the "how to build a sandbox" to B7's "how the sandbox fails under an adversary."
  3. DD-09 — NemoClaw. NVIDIA's hardened OpenClaw fork; OpenShell is the reference sandbox layer. The agent never touches the sandbox directly; execution goes through the governed API.
  4. DD-19 — CrabTrap. Brex's LLM-as-judge egress proxy. The probabilistic egress-governance layer; this module's network-layer default-deny is the deterministic complement.
  5. DD-20 — IronCurtain. Niels Provos's deterministic-enforcement runtime. Credential quarantine (fake-key swap), offline policy compilation (zero LLM at runtime), V8-isolate Code Mode. The reference defense architecture for B7.
  6. Amazon Firecrackerfirecracker-microvm.github.io/. The microVM reference; ~125ms cold start, minimal device model. Used by Lambda, Fargate, and E2B.
  7. Google gVisorgvisor.dev. The userspace-kernel sandbox (runsc). Intercepts syscalls so most kernel CVEs do not cross the boundary.
  8. Kata Containerskatacontainers.io. Lightweight-VM-based container runtime; the alternative to gVisor for a stronger container boundary.
  9. Wasmtimewasmtime.dev. The Bytecode Alliance's WASM runtime. Capability-based isolation, gas-metered execution.
  10. isolated-vm (Node.js)github.com/laverdet/isolated-vm. V8 isolate sandboxing; the runtime IronCurtain's Code Mode uses.
  11. Falco / bpftrace / bcc (eBPF)falco.org. Kernel-level observability for the sidecar monitor; syscall and network-event detection at negligible overhead.
  12. Linux cgroups v2 / seccomp-bpf — the kernel primitives for resource caps (vector 3 control) and syscall filtering (sidecar-monitor signal source).
  13. NIST SP 800-190Application Container Security Guide. The reference for container-boundary threats; the foundation for "plain Docker is a defect."
# Module B7 — Sandboxes and Execution Controls

**Course**: 2B — Securing & Attacking Harnesses and LLMs
**Module**: B7 — Sandboxes and Execution Controls
**Duration**: 60 minutes
**Level**: Senior Engineer and above
**Prerequisites**: Course 1 Module 5 (Sandboxing & Isolation); Course 2B B0–B6; DD-09 (NemoClaw), DD-19 (CrabTrap), DD-20 (IronCurtain)

> *Injection will succeed. The agent will be coerced into running code. The sandbox is the layer that decides whether that means "a confused log line" or "a reverse shell on your jump host." Design it as if the sandbox will be escaped — because the same vuln class that lets the agent run attacker code will, eventually, let that code walk out of the sandbox.*

---

## Learning Objectives

After completing this module, you will be able to:

1. Place any sandbox technology on the **isolation hierarchy** — OS process isolation, containers (Docker, gVisor), microVMs (Firecracker), WASM/V8 isolates, and full VMs — and defend a choice by stating its blast radius, overhead, and escape surface.
2. Name the **four attack vectors against an agent sandbox** — provider escape, network egress, resource exhaustion (OWASP ASI09), and sidecar/subprocess compromise — and apply the control for each.
3. Implement a **code-execution policy**: allowlist-based command gating (and articulate why denylists fail), per-tool execution scopes, and deterministic rule compilation (the IronCurtain model), rejecting LLM-at-runtime enforcement for security-critical decisions.
4. Enforce **default-deny network egress** at the network layer (distinct from the LLM-judge layer in CrabTrap), with per-task allowlisting, and explain why this is the single most under-applied control in shipped agent sandboxes.
5. Operate a **sidecar monitor** that watches the sandbox for anomalous behavior — resource spikes, unexpected syscalls, repeated failed operations — and feeds B8's observability pipeline.
6. Apply the **blast-radius principle**: assume the sandbox will be escaped, and design so that an escaped sandbox still has no credentials and no network reachability. This is the central design philosophy of the module.

---

## Why this module exists

Course 1 Module 5 established the sandbox as a fact of harness engineering: any agent that runs shell, writes files, or calls APIs has a blast radius, and the inside-vs-outside-sandbox split plus filesystem/network scoping are the baseline controls. That module taught you *how to build a sandbox*. This module teaches you *how the sandbox fails under an adversary who controls the agent's inputs*, and what execution controls must sit inside and around it to survive that adversary.

The inversion from Course 1 is the same one that defines all of 2B: in Course 1 the threat was a misbehaving-but-honest agent (an over-long loop, a mis-scoped write); in 2B the threat is an **agent under prompt-injection coercion**, induced to run attacker-chosen code inside your sandbox. The difference is the adversary. A sandbox tuned to survive "the agent wrote to `~/.ssh` by accident" is not tuned to survive "the agent was instructed to compile and run a C implant that exploits CVE-in-your-runtime." The controls are stronger, the threat model is adversarial, and the design philosophy inverts: **you do not harden the sandbox to prevent escape; you harden the *environment* so that escape does not matter.**

Three sub-sections, twenty minutes each:

- **B7.1 — The Sandbox Hierarchy and the Escape Threat.** The isolation hierarchy from process isolation to full VMs, the blast-radius-vs-overhead tradeoff, and the four attack vectors against a sandbox that contains an agent under coercion.
- **B7.2 — Execution Policies and Network Segmentation.** What code the agent is allowed to run (allowlists, per-tool policies, the IronCurtain deterministic-compilation model) and the network default-deny rule with per-task egress allowlisting.
- **B7.3 — Sidecar Monitors and the Blast-Radius Principle.** The monitor that watches for anomalous behavior, and the design discipline of assuming escape: no credentials, no network, no ambient authority — even after the sandbox is compromised.

---

# B7.1 — The Sandbox Hierarchy and the Escape Threat

*Where the sandbox sits on the isolation hierarchy, and the four ways an adversary controlling the agent will attack it.*

## The isolation hierarchy

A sandbox is a containment boundary between untrusted code and the rest of the system. Every sandbox technology is a point on a tradeoff curve between **isolation strength** (the blast radius if the contained code is hostile) and **overhead** (latency, density, operational cost). The hierarchy, weakest to strongest:

1. **OS process isolation.** The agent runs in a separate process; isolation is the kernel's process boundary plus UID/gid separation. Cold start: instant. Blast radius: the entire user account, every file the UID can read, every syscall the UID can make. This is `local bash` in Course 1 Module 5. Fine for a single-tenant dev tool. Catastrophic for an agent running attacker code.

2. **Containers (Docker, Podman).** Linux namespaces + cgroups + a layered filesystem. The process sees its own mount table, network namespace, PID tree. Cold start: seconds. Blast radius: the container's filesystem and the namespace's network — *plus* anything reachable through a container escape (kernel CVEs, misconfigured capabilities, privileged mode, mounted Docker socket). Default Docker is process isolation with a stronger boundary; it is **not** a security boundary against a kernel exploit. This is where most production harnesses start and, dangerously, where many stop.

3. **Hardened container runtimes (gVisor, Kata).** gVisor implements a userspace kernel (`runsc`) that intercepts the sandboxed process's syscalls — the guest never touches the host kernel directly, so most kernel CVEs do not cross the boundary. Kata runs the container inside a lightweight VM. These raise the escape bar substantially at modest overhead (gVisor: ~10–30% on syscall-heavy workloads). For an agent sandbox that will run attacker-influenced code, plain Docker is a defect; gVisor or Kata is the floor.

4. **MicroVMs (Firecracker).** A minimal KVM-based VM (AWS, used by Lambda and Fargate). ~125ms cold start, ~5MB memory footprint, a stripped device model (only virtio-net, virtio-block, serial). Blast radius: the VM, and the hypervisor's escape surface — which is small and aggressively audited. The isolation strength per dollar of overhead is the best in the industry; this is what you reach for when the sandbox will run genuinely untrusted code (E2B's cloud sandbox is Firecracker-based).

5. **WASM / V8 isolates (Wasmtime, `isolated-vm`).** Language-level sandboxing. WASM modules and V8 isolates are confined to a linear memory and a capability-based import table; there is no syscall surface by default, only what the host explicitly exports. Cold start: milliseconds. Blast radius: the isolate's memory and the exported functions. This is what IronCurtain's "Code Mode" uses (`isolated-vm`) for its builtin agent's TypeScript. The escape surface is the runtime itself (V8/Wasmtime CVEs) and the host's exported functions — which you control.

6. **Full VMs (KVM, Hyper-V, ESX).** The strongest isolation short of separate hardware. Blast radius: the VM. Escape surface: the hypervisor. Overhead: seconds of cold start, gigabytes of memory. Almost never the right answer for an agent sandbox — microVMs give you 95% of the isolation at 5% of the cost — but it is the correct answer when the sandbox runs code from mutually-distrusting tenants and a hypervisor escape would be catastrophic.

**The reference implementation: NemoClaw's OpenShell (DD-09).** OpenShell is NemoClaw's sandbox layer: the agent never touches the sandbox directly; every execution request goes through the governed API, which brokers access to an isolated execution environment. The architectural principle is the load-bearing one — **the agent cannot reach the sandbox to reconfigure it** — and the same principle applies to whichever technology you pick from the hierarchy above.

## The blast-radius-vs-overhead tradeoff

The hierarchy is a curve. Moving down (stronger isolation) buys you a smaller blast radius at the cost of cold-start latency, memory density, and operational complexity. The correct point on the curve is determined by **what code will run in the sandbox and who chose it**:

- Code the *operator* wrote and the agent merely invokes: process isolation or plain containers are acceptable.
- Code the *agent* wrote (a generated script): containers minimum, gVisor preferred.
- Code the *agent* was coerced into running by an injected instruction: microVM (Firecracker) or WASM isolate, *and* the controls in B7.2/B7.3 layered on top.

Most shipped agent sandboxes sit at level 2 (plain Docker) regardless of what code they run. That is the defect this module exists to fix.

## The four attack vectors

An adversary who controls the agent's inputs (via prompt injection, indirect injection, or a poisoned tool response) can coerce the agent into running attacker-chosen code inside the sandbox. Four vectors by which that code attacks the *system*, not just the sandbox:

### 1. Sandbox escape

The contained code exploits a bug in the sandbox provider itself to reach the host. Historical examples: V8 isolate escapes (type confusion in the JIT, prototype-pollution chains that reach across the isolate boundary), container breakouts (kernel CVEs like Dirty Pipe, `CAP_SYS_ADMIN` abuses, the mounted-Docker-socket footgun), gVisor syscall-emulation bugs. The vector succeeds when the boundary between sandbox and host is a piece of software with its own CVEs — and every boundary is.

The control is twofold: pick a stronger point on the hierarchy (Firecracker's escape surface is smaller than Docker's), and assume the escape succeeds — which is the blast-radius principle in B7.3.

### 2. Network egress from the sandbox

The sandbox runs the code fine — *too fine*. The code reaches the network: data exfiltration to an attacker-controlled endpoint, callbacks to a C2, lateral movement into the internal network the sandbox can reach, SSRF against the cloud metadata service (`169.254.169.254`). This is the **most under-applied control** in shipped agent sandboxes. The default in most container and even microVM deployments is some degree of network reachability, because the agent's tools (web search, API calls) need it.

The control is default-deny egress with per-task allowlisting (B7.2). The CrabTrap (DD-19) project governs egress at the LLM-judge layer; this module's concern is the *network-layer* default-deny that must hold even if the LLM judge is bypassed or absent.

### 3. Resource exhaustion (OWASP ASI09)

The contained code is induced to run indefinitely, fill the disk, exhaust memory, fork-bomb, or spawn unbounded subprocesses. OWASP ASI09 (Excessive Agency / Resource Exhaustion in agentic systems) is the formal framing: an agent coerced into `while true: pass`, or into downloading a multi-gigabyte file into `/tmp`, or into a tight loop that bills the LLM provider — all of these are the sandbox's problem because the sandbox is where the code executes.

The control is hard resource caps: CPU, memory, wall-clock, process count, disk-write quota — enforced by cgroups (container), by the hypervisor (microVM), or by the runtime (V8 fuel, WASM gas). A sandbox without resource caps is a sandbox that can be made to fail-open (OOM-kill the monitor) or fail-costly (run up the bill).

### 4. Sidecar / subprocess compromise

The sandbox spawns a helper process — a language server, a file-format converter, a package installer, a TLS-terminating proxy — that runs with *more* privilege than the contained code. The contained code attacks the helper, and through the helper reaches something the sandbox itself could not. The classic instance: a package installer that runs as root inside the container and is tricked into running a malicious `postinst` script from a typosquatted npm/PyPI package. IronCurtain's mitigating-proxy pattern (the agent's npm/PyPI installs go through a *validating registry proxy*, not the public registry) exists because of this vector.

The control is privilege minimization on every helper: no helper runs as root; every helper is itself sandboxed; package installation goes through a vetting proxy; the helper's IPC surface to the contained code is minimal and capability-scoped.

---

# B7.2 — Execution Policies and Network Segmentation

*What code the agent is allowed to run, and the network the sandbox is allowed to reach — the two controls that contain an agent under coercion even before you get to monitoring.*

## Code execution policies

"Sandboxing" answers *where* code runs. An **execution policy** answers *what* code is allowed to run at all. The two are complementary and both are required. A sandbox without an execution policy will dutifully isolate whatever attacker-chosen code the agent was coerced into running; an execution policy without a sandbox will be bypassed the moment the policy checker has a bug.

### Allowlists, not denylists

The foundational rule: **allowlist, do not denylist.** A denylist ("block `rm`, `curl`, `wget`, `nc`, `python -c`...") is a list of things you have thought of. An allowlist (`ls`, `cat`, `grep`, `git status`, `make`, `pytest` — the commands this specific task needs) is a list of things the task needs. The attacker's job against a denylist is to find one command you did not think of; against an allowlist, the attacker must find a way to make an allowed command do harm (argument injection, a vulnerable subcommand), which is a much smaller surface.

Denylists fail in characteristic ways. The `curl` block is bypassed by `wget`, then by `python -c "import urllib"`, then by `perl`, then by `/lib/x86_64-linux-gnu/libc.so.6` being directly invokable, then by `git clone` of an attacker repo that runs a hook. Each addition to the denylist is one more command you have thought of; the space of "things that can fetch a URL or run arbitrary code" is unbounded. Allowlists invert the failure mode: the question becomes "can `make` be made to do something harmful?" — a bounded, auditable question.

### Per-tool execution scopes

A monolithic "what can the sandbox run" allowlist is coarse. The stronger pattern is a **per-tool execution scope**: the `run_tests` tool may invoke `pytest` and `make` with specific argument patterns; the `format_code` tool may invoke `prettier` and `black`; the `install_deps` tool may invoke `pip install` *but only through the validating proxy*. Each tool carries its own allowlist; the sandbox enforces "the command must match the calling tool's scope" before exec. This is how you prevent a coerced agent from invoking `pytest` from inside the `install_deps` tool — the tool's scope is the enforcement.

### The IronCurtain deterministic-enforcement model

How is the allowlist itself specified? Two schools, and they are the same debate that runs through DD-19 (CrabTrap) and DD-20 (IronCurtain):

- **LLM-as-judge at runtime (CrabTrap's model, applied to execution).** Each tool invocation is evaluated by an LLM that sees the command, the arguments, and the policy, and returns allow/deny. Flexible — novel attacks are caught because the judge understands semantics. Probabilistic — a prompt injection *in the arguments* can manipulate the judge. For security-critical decisions, this is a defect, not a feature.

- **Deterministic compilation (IronCurtain's model).** Policy is written in plain English, compiled *offline* by an LLM into deterministic JSON rules (allowlist patterns, denylist patterns, escalate conditions). At runtime, enforcement is pure if/then matching — **zero LLM at runtime**. The LLM's probabilism is confined to build time, where a miscompilation can be caught by the verify-and-repair pipeline; runtime enforcement is predictable, auditable, and testable.

For execution policies, the deterministic model is the correct default. The allowlist for a tool's command surface is a finite, enumerable set; there is no semantic question the LLM needs to answer at runtime. Compile the policy, verify it, and enforce it with string matching. Reserve LLM-as-judge (if you use it at all) for the *arguments*, and even then prefer structured argument validators (regex, schema, path-prefix checks) over an LLM. The principle, restated: **security-critical decisions at runtime must be deterministic.** The blast radius of a probabilistic enforcement layer is the set of inputs on which the LLM happens to be wrong.

## Network segmentation

The sandbox should have **no network egress by default.** This is the single most under-appreciated control in shipped agent sandboxes, and it is the one that contains the worst outcomes.

Consider the failure mode: a sandboxed agent under coercion reads a secret from a mis-scoped file (filesystem scope defect, a separate bug), then exfiltrates it by `curl`-ing an attacker endpoint. Two bugs were required — the filesystem mis-scope and the network reachability. Kill either and the exfiltration fails. Operators obsess over the filesystem bug (because it is the one that *coughs up* the secret) and ignore the network bug (because the network "has to work, the agent needs web search"). The network bug is the easier one to fix and the one that contains the larger class of outcomes — not just secret exfiltration, but C2 callbacks, lateral movement, and SSRF against the metadata service.

The control is the **default-deny egress with per-task allowlisting** model:

- The sandbox's network namespace starts with egress denied to everything.
- Each task (or each tool) carries an egress allowlist: the `web_search` tool may reach `search.example-corp.net`; the `fetch_docs` tool may reach a specific doc host; the `install_deps` tool may reach the validating package registry and nothing else.
- The allowlist is enforced at the network layer (iptables/nftables in the namespace, an egress proxy the sandbox is forced through, or the microVM's virtual NIC restricted to allowlisted destinations) — **not** at the LLM-judge layer.

This is distinct from CrabTrap. CrabTrap (DD-19) governs egress at the LLM-judge layer: every outbound request is evaluated by an LLM against a natural-language policy. That is valuable for the *semantic* question ("is this request something this agent should be making?") but it is the wrong tool for the *existence* question ("should this sandbox be able to reach the network at all?"). The network-layer default-deny answers the existence question deterministically; CrabTrap answers the semantic question probabilistically on top. You want both, and the network-layer control is the one that must hold when the LLM judge is bypassed, confused, or absent.

The cloud-metadata-service case deserves special mention. `169.254.169.254` (AWS/Azure/GCP metadata) hands out instance credentials to anything that can reach it. A sandboxed agent that can reach the metadata service can steal the host's cloud role — a far worse outcome than reading a local secret. Default-deny egress blocks this for free; an allowlist that does not explicitly list the metadata endpoint blocks it for free; an LLM-judge-only model is one prompt injection away from failing to block it. Block the metadata service at the network layer, always.

---

# B7.3 — Sidecar Monitors and the Blast-Radius Principle

*The monitor that watches the sandbox, and the design discipline that makes escape survivable.*

## Sidecar monitors

A sandbox with no monitoring is a sandbox whose compromise you discover from an incident report. A **sidecar monitor** is a process that observes the sandbox from outside — *outside the sandbox's trust boundary*, so the contained code cannot reach it to disable it — and flags anomalous behavior. The monitor is the early-warning layer that ties this module to B8 (Observability and Attack Detection).

The monitor watches four signal classes:

1. **Resource signals** — CPU, memory, disk, network-byte, and process-count time series. A resource spike (the sandbox suddenly allocating 2GB, spawning 50 processes, or writing 10GB to disk) is the signature of ASI09 resource exhaustion or of an escape attempt in progress. Thresholds are per-task (a `build` tool is allowed to spike CPU; a `read_file` tool is not).

2. **Syscall signals** (where observable — seccomp-bpf, eBPF, or gVisor's syscall log). The contained code's syscall distribution should match the task's profile. A `grep` invocation that issues `socket`, `connect`, and `fork` is not behaving like grep. eBPF (bcc, bpftrace, Falco) gives you kernel-level observability into the sandboxed process tree with negligible overhead.

3. **Network signals** — connection attempts (allowed and denied), DNS queries, byte volumes. Denied connection attempts are especially informative: they are the agent (or the contained code) trying to reach something the egress policy blocked. A burst of denied connections to a single external host is a C2-attempt signature.

4. **Behavioral signals** — repeated failed operations (a brute-force pattern), unusual command sequences (the agent invoking `git` from inside the `pytest` tool), unexpected file paths accessed. These overlap with execution-policy enforcement but the monitor's job is detection, not prevention: the policy blocks; the monitor logs and alerts.

The monitor's output feeds B8's observability pipeline — structured events, correlated with the agent's tool-call trace, so an incident investigator can reconstruct "the agent was coerced at step 14, ran this command at step 15, the sandbox denied egress at step 16, the monitor flagged it at step 17." The monitor is not a substitute for the controls in B7.2; it is the layer that tells you when those controls engaged on a real attack.

## The blast-radius principle

This is the central design philosophy of the module, and it inverts the naive intuition.

The naive intuition is: *harden the sandbox so it cannot be escaped.* This is the wrong goal. Every sandbox is software; every sandbox has CVEs; every sandbox will, eventually, be escaped by an adversary who controls the code inside it. The V8 isolate, the Firecracker microVM, gVisor's `runsc` — all have had escape-class bugs, all will have more. Hardening reduces the probability of escape; it does not eliminate the outcome.

The blast-radius principle is: **assume the sandbox will be escaped, and design so that an escaped sandbox still has no credentials and no network.** The escape becomes a contained event — the contained code reaches the host, finds no credentials to steal, finds no network to reach, finds no privileged helper to suborn, and is killed by the resource caps before it can do anything useful. The escape is detected by the monitor (the process tree just grew unexpectedly; syscalls just appeared that the task profile does not include) and the sandbox is destroyed.

Three concrete realizations:

1. **No credentials in the sandbox.** The sandboxed process runs with no API keys, no cloud-instance role reachability (network-layer block on the metadata service, per B7.2), no secrets in its environment or filesystem. This is IronCurtain's credential-quarantine model: the agent is configured with *fake* keys, and a TLS-terminating MITM proxy hot-swaps fakes for reals at the boundary. Even a fully-compromised, fully-escaped agent cannot exfiltrate real credentials because it does not have them. If your architecture requires credentials *in* the sandbox (the inside-sandbox model from Course 1 Module 5), you have accepted a worse blast radius — document the decision and add the other two realizations.

2. **No network from the sandbox.** Even after escape, the contained code is in the sandbox's network namespace (container) or the microVM's virtual NIC — which is default-deny with per-task allowlists. An escape that reaches the host kernel does not magically gain a new NIC; it is still constrained by the network policy the namespace enforces. (An escape that reaches the *host* userspace and can reconfigure the network policy is a worse escape; the monitor should catch the policy-modification syscall.)

3. **No privileged helpers.** Every subprocess the sandbox spawns runs with the minimum privilege to do its job — no root, no capabilities beyond what the task needs, no ambient authority. The contained code that escapes and tries to suborn a helper finds helpers that are themselves locked down.

The principle, compressed: **the value of escape is what you can reach after escaping.** Design so the answer is "nothing." Then the escape is a detected-and-killed anomaly, not an incident.

---

## Code — a sandbox policy enforcer

The lab builds a single TypeScript module that enforces the three controls from B7.2: an allowlist gate, resource caps, and a default-deny egress gate. The shape (full implementation in `07-lab-spec.md`):

```typescript
// sandbox-policy.ts — deterministic enforcement. No LLM at runtime.
export type PolicyDecision = { decision: "allow" | "deny" | "escalate"; reason: string };

// (1) Allowlist gate: command must match the calling tool's scope.
export function checkCommandPolicy(
  tool: string, command: string, args: readonly string[],
  policy: CommandPolicy
): PolicyDecision {
  const scope = policy.toolScopes[tool];
  if (!scope) return { decision: "deny", reason: `no scope for tool ${tool}` };
  for (const pattern of scope.allow) {
    if (matchCommand(command, args, pattern)) {
      return { decision: "allow", reason: `matched ${pattern.name}` };
    }
  }
  return { decision: "deny", reason: `${command} not in allowlist for ${tool}` };
}

// (2) Resource caps: cgroup-style limits enforced before exec.
export interface ResourceCaps {
  readonly cpuSeconds: number;    // wall-clock kill
  readonly memoryMb: number;      // OOM-kill
  readonly processCount: number;  // fork limit
  readonly diskWriteMb: number;   // write quota
}
export function enforceCaps(caps: ResourceCaps): SpawnOptions { /* ... cgroups / ulimit ... */ }

// (3) Default-deny egress: per-task allowlist at the network layer.
export function egressGate(taskId: string, host: string, egress: EgressPolicy): PolicyDecision {
  const allow = egress.taskAllowlist[taskId] ?? [];
  return allow.some(h => host === h || host.endsWith("." + h))
    ? { decision: "allow", reason: `${host} in allowlist for ${taskId}` }
    : { decision: "deny", reason: `default-deny: ${host} not allowlisted for ${taskId}` };
}
```

The full lab wires this into an actual sandboxed execution: a script is run in a child process with cgroup-enforced caps, its outbound connections are intercepted by an egress gate, and every command is checked against the calling tool's scope. The sandbox-escape attempt at the end of the lab has the student try to reach the network from inside — and watch the egress gate deny it.

---

## Anti-Patterns

### Plain Docker as the security boundary
Running attacker-influenced code in a default Docker container and calling it "sandboxed." The Linux kernel is the boundary, and the kernel has CVEs. Cure: gVisor or Kata at minimum for agent code; Firecracker for genuinely untrusted code. The container is the *packaging*; the security boundary is what sits between the contained process and the host kernel.

### Allow-all network egress ("the agent needs web search")
Defaulting the sandbox's network namespace to some reachability because a tool needs it. Cure: default-deny at the network layer; per-tool/per-task allowlist. The web-search tool reaches one host; that is not an argument for reaching the whole internet.

### Denylist command policies
"Block `curl`, `wget`, `nc`, `python -c`." The attacker finds `perl`, then `awk`, then `git clone` with a hook. Cure: allowlist — the commands the task needs, validated against the calling tool's scope. The surface is bounded and auditable.

### LLM-as-judge for the command allowlist
Evaluating each command with a runtime LLM and trusting its allow/deny. Cure: deterministic compilation (IronCurtain). Compile the policy offline; enforce with string matching at runtime. Security-critical runtime decisions must be deterministic.

### Credentials in the sandbox environment
API keys in the sandboxed process's env, "because the agent needs them." An escape — or a filesystem mis-scope the agent was coerced into exploiting — reads them. Cure: credential quarantine (IronCurtain's fake-key swap) or the outside-sandbox architecture (Course 1 Module 5) where credentials never enter the sandbox.

### No resource caps
A sandbox with CPU/memory/disk/process limits unset. The contained code is induced into `while true` or a multi-gigabyte download (ASI09). Cure: cgroup-enforced caps on every resource class, per-task. A sandbox without caps is a sandbox that can be made to fail-open by OOM-killing the monitor.

### The unsupervised sidecar
A helper (package installer, format converter) that runs as root or with broad capabilities inside the sandbox. The contained code suborns it. Cure: every helper is itself sandboxed, runs with minimum privilege, and its IPC surface to the contained code is capability-scoped. Package installs go through a validating registry proxy.

---

## Key Terms

| Term | Definition |
| --- | --- |
| **Isolation hierarchy** | The spectrum of sandbox strength: OS process → container → hardened container (gVisor/Kata) → microVM (Firecracker) → WASM/V8 isolate → full VM. Blast radius vs. overhead. |
| **Blast radius** | The scope of damage if the contained code reaches the host. The quantity the hierarchy trades off against overhead. |
| **Sandbox escape** | Attack vector 1: the contained code exploits a bug in the sandbox provider (V8, the kernel, gVisor) to reach the host. |
| **Network egress** | Attack vector 2: the contained code reaches the network. The most under-applied control; contained by default-deny. |
| **Resource exhaustion (ASI09)** | Attack vector 3: the contained code runs indefinitely, fills disk, exhausts memory, fork-bombs. Contained by hard resource caps. |
| **Sidecar compromise** | Attack vector 4: the contained code suborns a privileged helper the sandbox spawned. Contained by privilege minimization on every helper. |
| **Allowlist policy** | The command-execution model: only enumerated commands run. Contrast denylist, which fails on the unbounded space of "commands you didn't think of." |
| **Per-tool execution scope** | An allowlist scoped to the calling tool — `pytest` from the `run_tests` tool, not from `install_deps`. |
| **Deterministic enforcement** | The IronCurtain model: policy compiled offline to JSON rules; zero LLM at runtime. Security-critical runtime decisions must be deterministic. |
| **Default-deny egress** | The network policy: the sandbox reaches nothing by default; each task carries an allowlist. Enforced at the network layer, not the LLM-judge layer. |
| **Sidecar monitor** | A process outside the sandbox's trust boundary that watches for anomalous behavior (resource spikes, unexpected syscalls, denied-connection bursts). Feeds B8. |
| **Blast-radius principle** | Assume the sandbox will be escaped; design so an escaped sandbox has no credentials and no network. The central design philosophy. |
| **Credential quarantine** | IronCurtain's fake-key-swap: the agent holds only fake keys; a MITM proxy substitutes reals at the boundary. An escaped agent has no real credentials to exfiltrate. |
| **OpenShell** | NemoClaw's (DD-09) sandbox layer: the agent never touches the sandbox directly; execution goes through the governed API. |
| **CrabTrap (DD-19)** | LLM-as-judge egress proxy. Governs the *semantic* egress question probabilistically; complements (does not replace) network-layer default-deny. |
| **IronCurtain (DD-20)** | Deterministic-enforcement runtime. Credential quarantine + offline policy compilation + zero-LLM runtime. The reference defense architecture for this module. |

---

## Lab Exercise

See `07-lab-spec.md`. "Build the Sandbox Policy Enforcer": students implement (a) an allowlist-based command-execution gate with per-tool scopes, (b) a resource-cap enforcer (CPU/memory/process/disk limits via cgroups/ulimits), and (c) a default-deny egress gate with per-task allowlisting. A closing demo has the student attempt to reach the network from inside the sandbox and watch the egress gate deny it. TypeScript, type-hinted, no GPU, ~60–75 minutes.

---

## References

1. **OWASP** — *Top 10 for Agentic Applications (2026)*. `genai.owasp.org/resource/owasp-top-10-for-agentic-applications-for-2026/`. ASI07 (Insecure Output Handling — the injection vector that coerces the agent into running code) and ASI09 (Excessive Agency / Resource Exhaustion — vector 3 in this module).
2. **Course 1 Module 5 — Sandboxing and Execution Isolation.** The inside-vs-outside split, the provider table, filesystem and network scoping. This module's prerequisite; the "how to build a sandbox" to B7's "how the sandbox fails under an adversary."
3. **DD-09 — NemoClaw.** NVIDIA's hardened OpenClaw fork; OpenShell is the reference sandbox layer. The agent never touches the sandbox directly; execution goes through the governed API.
4. **DD-19 — CrabTrap.** Brex's LLM-as-judge egress proxy. The probabilistic egress-governance layer; this module's network-layer default-deny is the deterministic complement.
5. **DD-20 — IronCurtain.** Niels Provos's deterministic-enforcement runtime. Credential quarantine (fake-key swap), offline policy compilation (zero LLM at runtime), V8-isolate Code Mode. The reference defense architecture for B7.
6. **Amazon Firecracker** — `firecracker-microvm.github.io/`. The microVM reference; ~125ms cold start, minimal device model. Used by Lambda, Fargate, and E2B.
7. **Google gVisor** — `gvisor.dev`. The userspace-kernel sandbox (`runsc`). Intercepts syscalls so most kernel CVEs do not cross the boundary.
8. **Kata Containers** — `katacontainers.io`. Lightweight-VM-based container runtime; the alternative to gVisor for a stronger container boundary.
9. **Wasmtime** — `wasmtime.dev`. The Bytecode Alliance's WASM runtime. Capability-based isolation, gas-metered execution.
10. **`isolated-vm` (Node.js)** — `github.com/laverdet/isolated-vm`. V8 isolate sandboxing; the runtime IronCurtain's Code Mode uses.
11. **Falco / bpftrace / bcc (eBPF)** — `falco.org`. Kernel-level observability for the sidecar monitor; syscall and network-event detection at negligible overhead.
12. **Linux cgroups v2 / `seccomp-bpf`** — the kernel primitives for resource caps (vector 3 control) and syscall filtering (sidecar-monitor signal source).
13. **NIST SP 800-190** — *Application Container Security Guide*. The reference for container-boundary threats; the foundation for "plain Docker is a defect."