Warmup Guide — Sandboxing and Isolation from First Principles

Zero-to-expert primer for Phase 06. It builds isolation from the ground up: what a sandbox is (a controlled reduction of authority), Linux process authority, namespaces, capabilities, seccomp, cgroups, filesystem/device/network isolation, the OCI/runc container boundary, gVisor's user-space kernel, KVM/Firecracker microVMs, and the control plane. Assumes Phase 01 and Phase 05. The through-line: a sandbox is not a product name or a boolean called secure — it is a composed, verified set of minimal grants, each enforced outside the attacker's code. By the end you should be able to explain exactly what is shared by a process, a container, a gVisor sandbox, and a microVM, and build a runner whose security properties you can test.

Table of Contents


Chapter 1: What a Sandbox Actually Is

Zero background. A "sandbox" is a place where you can run code you don't trust and bound the damage it can do. Defined precisely: a sandbox is a controlled reduction of a process's authority — you start from "this code can do everything the process model allows" and you take away everything it doesn't strictly need. The security comes from what you removed, not from any single feature you turned on.

Why this definition matters. Beginners think "I put it in a container, so it's sandboxed." A container is a bundle of authority-reductions (namespaces + cgroups + maybe seccomp) — but if any one reduction is missing or misconfigured (privileged mode, a mounted Docker socket, host networking), the sandbox is wide open. The reduction is only as strong as its weakest dimension.

The composed-proof view (the mental model for the whole phase). Isolation is a conjunction of independent grants, each of which must be (a) minimal, (b) enforced outside the attacker-controlled code, and (c) verified after a failed attempt:

DimensionEnforced byVerified by
identityuser namespace / VMinspect credentials & mappings
filesystemmounts / VFS / device policya denied canary read
kernel surfaceseccomp / gVisor / guest kerneltrace + a blocked syscall
networknetns / firewall / proxypacket capture + denial logs
resourcescgroup / controllera stress fixture staying in bounds
lifecyclesupervisor / reconcilerorphan & leaked-resource scan

Misconception to kill now. "Namespaces are a security boundary." A single namespace is one dimension of reduction; calling it the boundary (without capabilities, seccomp, cgroups, and a patched kernel underneath) is the most common and most dangerous mistake in this field.

Chapter 2: The Isolation Threat Model and Invariants

The adversary. Assume a customer can submit arbitrary code and input (the multi-tenant code execution problem — what AWS Lambda, CI runners, and Vercel Sandbox solve). That workload may try to:

  • consume unbounded CPU, memory, PIDs, disk, or output (denial of service / noisy neighbor);
  • read host files, environment variables, credentials, sockets, or devices;
  • reach metadata services (169.254.169.254 — the cloud credential endpoint), internal APIs, other tenants, or the internet;
  • exploit kernel or runtime attack surface (the shared kernel is the prize);
  • act as a confused deputy — trick the scheduler into attaching another tenant's data/credential;
  • survive cancellation through descendants, detached processes, mounts, or namespaces;
  • abuse observability gaps or malicious output handling.

The six security invariants your design must hold (these are the acceptance criteria):

  1. A job cannot access another tenant's input, output, identity, or process.
  2. A job receives only declared files, devices, syscalls, network paths, and credentials.
  3. Resource ceilings are enforced outside the guest/workload.
  4. Cancellation destroys the full process tree and releases every resource.
  5. The workload cannot administer the runtime, host, scheduler, image store, or telemetry.
  6. Every policy decision and lifecycle transition is attributable to tenant and job.

Why invariant 4 is so hard. A process can fork descendants, detach them, create mounts and TAP devices (software network interfaces that bridge a VM or namespace onto host networking — the host-side end of a guest's virtual NIC), and leave them behind. "Kill the process I started" is not "clean up everything it did." The microVM-lifecycle lab exists because complete cleanup is a state-machine problem.

Misconception to kill now. "The guest boundary is what I have to get right." Often the control plane (Chapter 12) is the bigger risk — it chooses images, mounts, credentials, and tenants.

Chapter 3: Linux Process Authority — the Thing You Are Reducing

You cannot reduce authority you can't enumerate. A Linux process's authority is the composition of all of these:

  • UID/GID and supplementary groups;
  • filesystem DAC/ACLs (Phase 05);
  • capabilities (Chapter 5);
  • namespaces and their mappings (Chapter 4);
  • LSM policy (SELinux/AppArmor/Landlock);
  • seccomp syscall policy (Chapter 6);
  • open file descriptors inherited at launch (Phase 01 — capabilities you can't see in the code);
  • environment, working directory, mounts, devices, sockets, network routes;
  • cgroup placement and limits (Chapter 7).

The leak principle. Hardening one dimension while leaking another fails completely. Two canonical examples you must internalize:

  • A non-root process with a mounted Docker/runtime socket can tell the daemon to start a privileged container — it owns the host. UID 1000 means nothing here.
  • A seccomp-filtered process that already holds an open file descriptor to a secret doesn't need openat — the capability is already in hand.

Misconception to kill now. "It runs unprivileged, so it's contained." Containment is the whole authority set being minimal — a single over-broad mount, descriptor, or group membership undoes the rest.

Chapter 4: Namespaces — Changing What a Process Can See

Zero background. A namespace is a kernel feature that gives a process its own private view of some global resource. Two processes in different PID namespaces literally cannot see each other's process IDs. Namespaces virtualize visibility and naming, which is the foundation of containers.

NamespaceIsolatesCommon mistake
PIDprocess IDs & hierarchythe init of a PID ns must reap orphans, or you leak zombies/descendants
mountthe mount tablepropagation not set private leaks mounts back to the host
userUID/GID mappingmapping namespace-root to host root defeats the purpose
networkinterfaces, routes, firewallattaching a broad bridge restores host reachability
UTShostname/domaincosmetic on its own
IPCSysV/POSIX IPCinherited descriptors still cross
cgroupcgroup viewdoes not apply resource limits (Chapter 7 does)
timeselected clockspartial; not an anti-timing boundary

User namespaces — powerful and subtle. A user namespace lets a process be root inside the namespace while mapping to an unprivileged host UID. Benefit: rootless containers — you get "root-like" container behavior without granting real host root, shrinking the blast radius of an escape. Risk: being root-in-namespace exposes namespaced kernel code paths that historically had their own bugs, so the kernel version and distro threat model matter. (The README flags "explain user-namespace benefits and historical risk tradeoffs.")

How namespaces are created. clone(), unshare(), and setns() create or join them. The first-process problem: the PID-namespace init adopts orphaned descendants and must reap them — killing only the visible workload can leave a forest of helpers, mounts, and network resources outside your lifecycle (invariant 4).

Misconception to kill now. "Containers use namespaces, so namespaces isolate tenants." They isolate naming/visibility. They share the kernel. Tenant isolation needs the full composition plus a patched kernel — or a stronger boundary (gVisor/microVM).

Chapter 5: Capabilities and no_new_privs

Recap from Phase 05, applied here. Linux capabilities split root into ~40 units. For a sandbox: start from all capabilities dropped, add back only the measured minimum. Treat CAP_SYS_ADMIN as host-admin-equivalent — granting it is almost never least privilege.

no_new_privs — close the setuid escalation door. Before executing untrusted code, set the no_new_privs bit so a subsequent execve cannot gain privilege via a setuid/setgid binary or file capabilities. Without it, an attacker who can exec a setuid helper inside the sandbox can climb back up. This single bit is mandatory for any untrusted-code sandbox.

Misconception to kill now. "I dropped to a non-root user, capabilities don't matter." A non-root process can still hold capabilities (file capabilities, ambient caps) — drop them explicitly.

Chapter 6: seccomp-BPF — Shrinking the Syscall Surface

The principle (from Phase 01). The syscall interface is the OS security boundary; seccomp lets a process install a BPF filter that allow/deny/errno/trap/kill/log on each syscall by number, architecture, and raw argument values. Forbid the syscalls the workload never needs and a code-execution bug loses its tools (ptrace, mount, keyctl, …).

How it executes (the limits you must know). Seccomp filters are classic BPF evaluated on syscall entry. They see the syscall number, architecture, instruction pointer, and raw arguments — they cannot dereference user pointers, so seccomp cannot know which path openat targets or which address connect reaches. Therefore:

  • seccomp is not semantic authorization. Allowing openat doesn't decide which file; *filesystem
    • LSM* policy must. Allowing connect doesn't decide the destination; network policy must.
  • architecture checks are mandatory — without them, a process can make syscalls via a different ABI to bypass your number-based filter.
  • user-notification mode (a privileged supervisor handles flagged syscalls) introduces race, availability, and confused-deputy risk — powerful but a new attack surface.

Building a policy. Auto-generating an allowlist from one successful trace is a starting observation, not proof — error paths, locale, DNS, threading, the allocator, and post-upgrade behavior may need other calls. Review and regression-test against both required behavior and prohibited calls. The Seccomp Regression Analyzer lab is exactly this: after an upgrade, separate approved syscall additions from forbidden/unexplained ones (syscall drift).

Misconception to kill now. "Seccomp blocks the dangerous file access." It blocks the syscall number, not the target. Path/destination authorization is a different layer.

Chapter 7: cgroups v2 — Resource Limits as a Security Control

Why resources are security. Denial of service is a security failure (the 'A' in CIA). An attacker who can't read your data but can exhaust CPU/memory/PIDs and take down every co-tenant has breached availability. cgroups v2 enforce resource budgets externally (invariant 3 — outside the guest):

  • memory.max (hard cap), memory.high (pressure), memory.swap.max;
  • pids.max (caps tasks — defuses fork bombs);
  • cpu.max (quota) and weights;
  • I/O controls; and PSI (Pressure Stall Information) to detect overload.

Resource security is broader than RAM/CPU. Also budget: wall-clock time, scratch storage, open file descriptors, sockets, output bytes, result size, queue depth, and control-plane work. The classic gap: memory.max without pids.max still allows process-table exhaustion. Place and verify every limit before running untrusted descendants (a fork bomb under a PID limit is a required benign fixture).

Misconception to kill now. "A memory limit handles resource abuse." One controller closes one exhaustion path. Enumerate them all — PIDs, I/O, output, time, descriptors.

Chapter 8: Filesystem, Device, and Network Isolation

Filesystem baseline:

  • immutable image / read-only root;
  • a fresh per-job writable scratch with a size limit;
  • none of: host home, runtime socket, package-manager credentials, SSH agent, kubeconfig, cloud config, device nodes;
  • mount propagation private (Chapter 4);
  • consider symlinks, hard links, archive extraction, and path traversal at staging boundaries (Phase 01/02 — the place untrusted content enters).

Device baseline: no devices by default. /dev/null, /dev/zero, and a few pseudo-devices are safe to provide. GPU, KVM, FUSE, block, and raw-network devices materially change the threat model — each is extra kernel attack surface.

Network baseline: no network by default. If required, route through a policy-enforcing proxy with an explicit allowlist (DNS names/IPs, ports, protocols, request limits, logs). Block link-local and metadata ranges, RFC1918 private networks, control planes, and DNS-rebinding paths — this is the Phase 02 SSRF defense applied to sandbox egress (and the #1 cloud pivot you must prevent).

Misconception to kill now. "Read-only root is enough for the filesystem." A read-only root with a mounted runtime socket or a writable host bind mount is fully compromised. The absence of dangerous mounts matters as much as read-only-ness.

Chapter 9: OCI, runc, and the Container Boundary

What a container actually is. The OCI specs standardize the image format and a runtime config (JSON describing namespaces, mounts, capabilities, seccomp, cgroups). runc is the reference runtime that turns that config into a process using the host-kernel primitives from Chapters 4–8. The defining fact: a standard container shares the host kernel. Its security is the composition of namespaces + user namespaces + capabilities + seccomp + LSM + read-only FS + device/network policy + cgroups — plus a patched kernel and no privileged mode/host namespaces/host mounts/runtime sockets.

Container escape classes (what you're defending against): kernel vulnerabilities reachable via syscalls; misconfigurations (privileged, CAP_SYS_ADMIN, host PID/net, /var/run/docker.sock mounted, writable host paths); and runtime bugs (historic runc CVEs). The shared kernel is why a single kernel 0-day breaks every container on the host — and why high-risk multi-tenancy reaches for gVisor or microVMs.

Misconception to kill now. "Containers isolate like VMs." They share the kernel; a VM doesn't. That one difference reshapes the entire multi-tenant threat model.

Chapter 10: gVisor — Moving the Syscall Surface to User Space

The idea. A standard container's workload calls the host kernel directly — a huge attack surface. gVisor inserts a user-space application kernel called Sentry between the workload and the host. Most application syscalls are intercepted and handled or translated by Sentry, so the workload reaches only a small, mediated set of host syscalls. Filesystem access is mediated by a separate process (the Gofer). It's run via runsc and integrates with container/Kubernetes tooling.

The honest tradeoff (the design-review framing). The right question is not "is gVisor secure?" — it's "which attack surface moved, which remained, and is compatibility/telemetry adequate?"

  • Moved: direct host-kernel syscall exposure → now mostly Sentry's surface.
  • Remained trusted: the host kernel (Sentry still uses some syscalls), the KVM/ptrace platform mode, the runtime, and the control plane.
  • Cost: compatibility gaps (syscall-heavy, ptrace, unusual kernel behavior) and performance overhead, especially for I/O-heavy workloads.

The lab exercise: trace a file/network op under runc vs runsc and compare host syscalls — the lesson is attack-surface movement, not elimination.

Misconception to kill now. "gVisor removes the kernel attack surface." It shrinks and relocates it; Sentry and the host platform remain trusted code with their own surface.

Chapter 11: KVM, Firecracker, and microVMs

Why a VM is a stronger boundary. With containers/gVisor the host kernel (or Sentry) is shared. A virtual machine gives the guest its own kernel, and the boundary becomes the hardware- virtualization interface (KVM) plus a small device-emulation layer — a much narrower, more auditable surface than a full shared kernel.

KVM execution (first principles). A userspace VMM (virtual machine monitor) asks KVM to create guest memory and vCPUs and enters guest execution. Guest code runs on the real CPU until an event — a privileged instruction, an interrupt, or device access — causes a VM exit back to the host to handle it. The attack surface is "what the host must emulate at VM exits."

Firecracker. A KVM-based VMM purpose-built for microVMs (fast-booting, minimal VMs for serverless/CI). Its key security property is an intentionally tiny device model (just a few virtio devices: block, net, etc.), which minimizes emulation attack surface. Components and their hardening:

  • the Firecracker/VMM process; the KVM host interface; the guest kernel + rootfs;
  • the API socket used to configure/start the VM — must be inaccessible to the workload and other tenants;
  • the jailer: applies host-side chroot (restricting the VMM's filesystem view to a minimal directory tree so a compromised VMM cannot see the rest of the host), namespaces, cgroups, dedicated UID/GID, and resource limits to the VMM itself (running Firecracker without the jailer is a flagged unsafe mode);
  • virtio block/net devices.

Hardening priorities: dedicated unprivileged UID/GID per isolation domain; jailer + chroot + cgroups

  • seccomp + private netns; immutable, verified kernel/rootfs; no shared writable host dirs; TAP/ network policy that blocks metadata/control planes and defaults to no egress; a lifecycle controller that kills the VMM and removes TAP, namespace, jail, cgroup, socket, and scratch; a patched/ dedicated host KVM/kernel; and snapshots treated as sensitive memory/disk state bound to image/ config identity. The MicroVM Lifecycle lab enforces allocation→start→cancel→collect→complete cleanup as a state machine.

Misconception to kill now. "A microVM has its own kernel, so the host is safe." The VMM, KVM/ host kernel, device emulation, jailer, API socket, network, and image pipeline all remain trusted — a VM exit into a buggy device emulator is the escape path.

Chapter 12: The Control Plane — Often More Dangerous Than the Guest

The reframe. Everyone focuses on the guest boundary; the scheduler/launcher is frequently the bigger risk because it chooses: which image, which mounts, which network, which credentials, which policy, which tenant, and how cleanup happens. A bug there means cross-tenant compromise without ever breaking the guest.

Protect the control plane with:

  • typed, immutable job intent (no user-controlled host paths or command strings — Phase 01/02 injection lessons);
  • policy decision before allocation (deny-by-default, like Phase 00);
  • a short-lived capability scoped to one job (no ambient, reusable credentials);
  • separate control and data planes;
  • signed images/configuration (Phase 03 provenance);
  • idempotent state transitions and independent enforcement of limits;
  • append-only / tamper-evident audit (every decision attributable — invariant 6).

Misconception to kill now. "If the sandbox holds, the platform is secure." A confused-deputy scheduler that attaches tenant B's secret to tenant A's job breaches isolation with a perfectly intact guest boundary.

Lab Walkthrough Guidance

The runtime you build is educational and must carry a prominent not-production-ready warning. Run only benign adversarial fixtures (fork bombs under PID limits, memory/CPU consumers, forbidden file reads, blocked syscalls, network-policy tests). Build order:

  1. Lab 01 — Sandbox Policy Compiler (flagship). Compile a high-level policy into an OCI-like spec and reject dangerous combinations: privileged mode, broad capabilities, unsafe mounts, host namespaces, metadata egress, missing limits, seccomp allow-all, Firecracker without jailer, incomplete cleanup. Implement strict validation → default capability drop → namespace requirements → cgroup limits → read-only root + controlled mounts → no-network/proxy egress → syscall allowlist → cleanup manifest → per-mode (container/gVisor/microVM) risk findings. Every unsafe fixture must fail closed with an audit event.
  2. Lab 02 — Seccomp Regression Analyzer. Detect syscall drift after upgrades; separate approved additions from forbidden/unexplained calls (Chapter 6).
  3. Lab 03 — MicroVM Lifecycle State Machine. Enforce allocation→start→cancellation→result→full cleanup with orphan detection (Chapters 11, invariant 4).
pytest test_lab.py -v
LAB_MODULE=solution pytest test_lab.py -v

Success Criteria

You are ready for Phase 07 when you can, without notes:

  1. State exactly what is shared by a process, a container, a gVisor sandbox, and a microVM.
  2. Enumerate the dimensions of Linux process authority and give two "leak one dimension" failures.
  3. Explain why a single namespace is not a security boundary, and what composition is required.
  4. Explain why seccomp can't authorize a file path or network destination, and what must.
  5. Design a seccomp profile from observed and reviewed behavior without blind auto-approval.
  6. Explain user-namespace benefits and their historical risk tradeoff.
  7. Describe gVisor and Firecracker honestly as attack-surface movement, naming what stays trusted.
  8. Prevent access to host files, devices, network, metadata services, and runtime sockets — and prove cancellation leaves nothing behind.

Common Mistakes

  • Calling namespaces a security boundary without the full composition and a patched kernel.
  • Running as root / privileged for convenience; mounting the runtime (Docker) socket.
  • Writable host bind mounts; unlimited PIDs; missing no_new_privs.
  • Auto-approving a seccomp allowlist from one trace without review.
  • Hidden cleanup failures (orphaned processes, TAP devices, mounts, cgroups).
  • Benchmark claims without representative workloads.
  • Equating a toy runtime with runc/gVisor/Firecracker (state limitations explicitly).

Interview Q&A

Q: Design a system to run untrusted customer code multi-tenant. A: Deny-by-default at every dimension. Per job: a microVM (own kernel) or at least a gVisor sandbox for syscall mediation; rootless/user-namespaced; all capabilities dropped + no_new_privs; a tight seccomp allowlist (reviewed, regression-tested); read-only root + size-limited scratch, no host mounts/sockets/devices; no network or an allowlisting egress proxy that blocks metadata/RFC1918; cgroup ceilings on memory, PIDs, CPU, I/O, output, and time. A hardened control plane chooses everything via typed immutable intent with per-job short-lived credentials and append-only audit, and a lifecycle controller guarantees full teardown. I'd state which surfaces remain trusted (host kernel/VMM, control plane) and patch/dedicate accordingly.

Q: Container vs gVisor vs microVM — how do you choose? A: By workload trust and compatibility. Containers (shared kernel) for trusted/first-party workloads where kernel attack surface is acceptable and performance matters. gVisor when you want stronger syscall mediation with container ergonomics and can tolerate compatibility/perf costs (not syscall/IO-heavy). microVMs (Firecracker) for untrusted multi-tenant code needing a real kernel boundary, accepting higher overhead. The decision is "how much do I trust this code, and what does it need to run?"

Q: A compromised build job — how do you contain it? A: Treat the build as untrusted code: ephemeral isolated runner (microVM/gVisor), no host credentials or runtime socket, read-only base + scratch, egress allowlist (no metadata, no internal APIs), per-job short-lived credentials, cgroup ceilings, and full teardown after. The CI control plane must inject secrets without logging and never pass user-controlled host paths/commands.

Q: Explain seccomp TOCTOU and compatibility tradeoffs. A: seccomp sees raw syscall args, not dereferenced pointers, so it can't make path/destination decisions and a check on an argument can race with the memory it points to (TOCTOU) — semantic authorization must live in the filesystem/LSM/network layers. Compatibility: too tight a profile breaks error/locale/DNS/threading/upgrade paths; you need observation plus review plus regression tests, with a visible failure action (errno in dev) and architecture filtering.

Q: How do you secure the sandbox control plane? A: Typed immutable job intent (no user host paths/commands), policy decided before allocation, per-job short-lived scoped capabilities, separated control/data planes, signed images/config, idempotent transitions, independent limit enforcement, and tamper-evident audit attributing every decision to tenant+job. The control plane is higher-value than the guest because it chooses images, mounts, credentials, and tenants.

References

Kernel primitives

  • man pages: namespaces(7), user_namespaces(7), capabilities(7), seccomp(2), cgroups(7), unshare(2), clone(2).
  • Linux kernel cgroup-v2 and seccomp BPF documentation; Landlock LSM documentation.

Containers and runtimes

  • OCI Image and Runtime Specifications; runc source and documentation.
  • "Understanding and Hardening Linux Containers" (NCC Group, Aaron Grattafiori).
  • NIST SP 800-190 (Application Container Security Guide); CIS Docker/Kubernetes Benchmarks.

Stronger isolation

  • gVisor architecture documentation (Sentry, Gofer, platforms); "gVisor" (Young et al.).
  • Firecracker design and the NSDI 2020 paper "Firecracker: Lightweight Virtualization for Serverless Applications"; the jailer documentation.
  • KVM API documentation; Kata Containers architecture (for comparison).

Multi-tenancy and escapes

  • Selected runc/container-escape CVE writeups (e.g. CVE-2019-5736).
  • Cloud provider isolation papers (AWS Lambda/Firecracker; Google gVisor).