Security — Intensive
Every other masterclass in this book takes the isolation boundary for granted. The KVM masterclass runs guest code on a real CPU and trusts that VT-x keeps it contained; the virtio masterclass emulates devices and trusts that a malformed descriptor chain only crashes the microVM, not the host. This intensive is about why you are allowed to trust those things — and, more importantly, about everything Firecracker layers on top of the bare KVM boundary so that you can run a hostile, untrusted guest kernel next to thousands of other hostile, untrusted guest kernels on the same machine and still sleep at night.
Firecracker's security argument is not "we use hardware virtualization." Hardware virtualization is necessary but not sufficient: the VMM is privileged host code that the guest talks to constantly (every MMIO write, every virtqueue kick, every metadata request), and a bug in that code is a bug in the host. The real argument is defense in depth — four overlapping layers, each of which assumes the layer above it has already failed:
┌─────────────────────────────────────────────────────────────────────────┐
│ 1. KVM boundary guest runs in non-root mode; can't touch host RAM │ ← hardware
│ 2. Minimal VMM ~5 emulated devices in memory-safe Rust │ ← small attack surface
│ 3. The jailer chroot + cgroups + namespaces + drop privileges │ ← process sandbox
│ 4. seccomp-BPF ~40-syscall allow-list on every thread │ ← syscall sandbox
└─────────────────────────────────────────────────────────────────────────┘
A guest that defeats layer 1 hits 2; that defeats 2 hits 3; that defeats 3 hits 4.
This intensive makes all four layers concrete and runnable. You will run Firecracker under the real
jailer and watch it construct each piece of the process sandbox, observing the namespaces and cgroup
from the outside. You will read the shipped seccomp filters, compile them, deny a syscall on purpose,
and watch the VMM die with SIGSYS the instant it tries to make it. And you will do a structured
threat-model audit of the whole attack surface — thinking like the attacker who wants out and the
defender who has to stop them — anchored on a real, recently-fixed vulnerability in the virtio-PCI
transport (CVE-2026-5747).
Note: This curriculum will not hold your hand here either. Security is the one area where a contributor who "mostly understands" is a liability — a seccomp filter that is one syscall too loose or a jailer step done in the wrong order is a silent hole. Every claim in this intensive comes with the
rg/find/lsnscommand that proves it on your checkout and your running process. Run them. Do not trust the prose.
What you will be able to do
By the end of this intensive you will be able to:
- Run a microVM in production posture — under the jailer, in a dedicated network namespace, with
cgroup limits, a chroot, and dropped privileges — and explain every step in terms of the exact
Linux mechanism (
unshare,setns,pivot_root,mknod,setgid/setuid, cgroup writes) it maps to. - Observe the sandbox from outside — find the jailed process, list its namespaces with
lsnsand/proc/<pid>/ns, read its cgroup, and confirm it cannot see the host filesystem or host processes. - Break the barrier deliberately to see what each layer actually stops — remove a device node, skip the netns, loosen a cgroup — and read the failure each one produces.
- Read and modify the seccomp filters in
resources/seccomp/<arch>.json: the three thread categories (vmm/api/vcpu), theSyscallRulemodel with argument operators, compile them withseccompiler-bin, and add a syscall to a filter and rebuild. - Trigger and diagnose a
SIGSYS— deny a syscall the VMM needs, watch it die, and read the evidence three ways (the FC log,strace,dmesg/auditd) — and explain why--no-seccompis a debugging tool and never a production posture. - Enumerate Firecracker's full attack surface — the emulated devices, the API socket, and the two trust boundaries (guest↔VMM and VMM↔host) — and reason about how a device-emulation bug becomes (or fails to become) a host compromise.
- Audit a real host against
docs/prod-host-setup.md— SMT/KSM off, per-instance UID/GID, cgroup limits, the169.254.169.254egress block, ECC+TRR RAM — and produce a defensible audit checklist you could hand a security reviewer.
The threat model, stated precisely
Internalize this before the labs, because every design decision in the codebase falls out of it. The
canonical statement lives in SECURITY.md and the NSDI '20 paper; the operational consequences live
in docs/prod-host-setup.md.
# Read the project's own threat-model statement and the production hardening guide.
sed -n '1,80p' SECURITY.md
sed -n '1,120p' docs/prod-host-setup.md
The guest is entirely untrusted — including the guest kernel. Firecracker does not assume a cooperative or even a non-malicious guest. The guest may be running attacker-controlled code in ring 0 of its own virtual machine, deliberately sending malformed virtio descriptors, hammering MMIO registers, and probing for VMM bugs. The asset being protected is the host and, transitively, every other microVM on it. A successful attack is one where guest code influences host state outside its own microVM — reads another tenant's memory, executes host code, escalates to root, crashes the host, or exfiltrates data through a side channel.
UNTRUSTED │ TRUSTED (must be protected)
┌──────────────────┐ │ ┌──────────────────────────────┐
│ guest kernel + │ boundary A │ │ Firecracker VMM (host code) │ boundary B ┌──────────┐
│ guest userspace │◄────────────►│ │ jailed + seccomp'd │◄─────────────►│ host │
│ (attacker) │ virtio/MMIO │ │ │ syscalls │ kernel │
└──────────────────┘ KVM exits │ └──────────────────────────────┘ to /dev/kvm └──────────┘
│ other microVMs ── also trusted relative to this guest
There are two boundaries, and the labs treat them separately:
| Boundary | Who is on each side | Crossed by | Defended by |
|---|---|---|---|
| A — guest ↔ VMM | attacker guest ↔ Firecracker device emulation | virtio descriptors, MMIO/PIO accesses, MMDS requests | KVM (non-root mode), Rust memory safety, bounds-checked guest-memory access, the minimal device model |
| B — VMM ↔ host | Firecracker (assume compromised) ↔ host kernel | syscalls, file access, device-node access | the jailer (chroot/namespaces/cgroups/priv-drop), seccomp-BPF |
The crucial design move is that boundary B assumes boundary A has already failed. Firecracker is engineered as if the guest will eventually find a device-emulation bug and achieve code execution inside the VMM process. The jailer and seccomp exist to make that hollow victory: a guest that owns the VMM finds itself in a near-empty chroot, in a private network namespace, capped by a cgroup, unprivileged, and allowed to make roughly forty syscalls — none of which let it touch another tenant. That is what "defense in depth" means here, and it is the single most important idea in this intensive.
The three labs
This intensive has three labs. Do them in order — the threat-model audit in Lab 3 only lands once you have felt the jailer and seccomp work (and break) in Labs 1 and 2.
-
Lab 1: The jailer in depth — trace-it / break-it. Set up
/srv/jailer, run Firecracker under the jailer with cgroups, namespaces, chroot, privilege drop, and a dedicated network namespace.mknodthe device nodes. Observe the process tree, the namespaces (lsns,/proc/<pid>/ns), and the cgroup from outside. Then break things on purpose — remove a device node, drop the netns — to see exactly which barrier each step is. Map every jailer action to the Linux mechanism that implements it, readingsrc/jailer/withrg. -
Lab 2: Seccomp filters — trace-it / build-it. Read
resources/seccomp/<arch>.json: thevmm/api/vcpucategories, theSyscallRulemodel, the argument operators. Compile a filter withseccompiler-bin. Run Firecracker with a custom--seccomp-filterthat denies a syscall it needs and watch it die withSIGSYS— observed via the FC log,strace, anddmesg/auditd. Then run with--no-seccompand reason about why that is dangerous. Add a syscall to a filter and rebuild. Understand the build-time bake-in. -
Lab 3: A threat-model audit — review-it. Enumerate the full attack surface: every emulated device, the API socket, and both trust boundaries. Walk the
docs/prod-host-setup.mdhardening line by line on a real (or simulated) host. Reason through a hypothetical device vulnerability anchored on the real CVE-2026-5747 in the virtio-PCI transport — how it would be reached, what each defense layer would (and would not) stop, and what the fix and disclosure looked like. Produce a reusable audit checklist. Learn to think like an attacker and a defender at the same time.
Prerequisites
This is a masterclass, not an introduction. You must have completed Level 9, Lab 9.1: Seccomp and the Jailer — that lab gives you a first, guided pass over both mechanisms; this intensive assumes you have it and goes far deeper, breaking things and reading source. You should also have internalized the two deep dives this intensive expands on:
| Read first | Why |
|---|---|
| The Jailer (deep dive) | The barrier the jailer constructs, step by step, and the one thing it deliberately does not do. |
| Seccomp Filtering (deep dive) | The three filter categories, the SyscallRule model, the build-time compile, and how a denied syscall manifests. |
| The threat model (introduction) | The "do almost nothing" security argument and where the VMM sits in the trust diagram. |
You also need a working Firecracker build and the ability to boot a microVM by hand, exactly as in Level 1, Lab 1.3. Confirm your environment now:
# 1. You can build firecracker AND the jailer (the jailer is a separate binary).
tools/devtool build --release
ls build/cargo_target/$(uname -m)-unknown-linux-musl/release/{firecracker,jailer}
# 2. The seccompiler binary exists (you'll compile filters with it in Lab 2).
ls build/cargo_target/$(uname -m)-unknown-linux-musl/release/seccompiler-bin \
|| cargo build --release -p seccompiler 2>/dev/null \
|| echo "build seccompiler before Lab 2"
# 3. The seccomp JSON for your arch exists.
find resources/seccomp -name "*.json"
# 4. You are root or can sudo — the jailer must start as root to set up the barrier.
id -u # the jailer needs to begin life as uid 0; it drops privileges itself
Warning: Run these labs on a disposable host or VM you own — a bare-metal instance, a nested KVM dev box, a throwaway cloud instance. You will create cgroups, network namespaces, TAP devices, and
mknoddevice nodes as root, and you will deliberately break isolation to observe failures. Do not do this on a shared or production machine. None of it persists if you tear the box down.
How the four layers map to the rest of the curriculum
This intensive is the security capstone of the whole book; it pulls threads from everywhere. Keep these cross-links handy — when a lab says "the device is part of the attack surface," the deep dive is where you learn why:
| Layer / topic | Deep dive | Masterclass |
|---|---|---|
| KVM boundary | KVM fundamentals, vCPU run loop & VM exits | KVM & vCPUs |
| Minimal device model | virtio transport, virtio-net, virtio-block | virtio devices |
| The jailer | The Jailer | Lab 1 (this intensive) |
| seccomp-BPF | Seccomp Filtering | Lab 2 (this intensive) |
| MMDS attack surface | MMDS | Networking Lab 3 |
| Rate limiting (DoS defense) | Rate limiting | Networking Lab 2 |
| Signals & fail-closed exit | Signals, shutdown & reset | Lab 2 (this intensive) |
The minimal-device-model philosophy — fewer devices means less host code a guest can attack — is the unifying thread. Firecracker emulates roughly five devices not out of laziness but because every emulated device is a parser that processes attacker-controlled bytes inside the host. When you audit the attack surface in Lab 3, you are really counting parsers.
Common mistakes contributors make in this area
| Mistake | Consequence | Fix |
|---|---|---|
Running firecracker directly and calling it "secure" | No chroot, no cgroups, no namespaces, no priv-drop — only seccomp | Production = jailer + seccomp + host hardening; bare firecracker is for dev only |
| Adding a device that needs a new syscall, forgetting the filter | The VMM dies with SIGSYS the first time the path runs | Add the syscall to resources/seccomp/<arch>.json and rebuild (Lab 2) |
| Hard-coding a syscall number in a filter | Breaks on the other architecture (x86_64 ≠ aarch64 numbering) | Use syscall names in the JSON; there is a file per arch |
Loosening an ioctl arg condition "to make it work" | Opens the whole ioctl multiplexer; defeats the point of the filter | Pin ioctl to the exact request numbers the path needs |
setuid before setgid in jailer-like code | Privilege drop is incomplete — can't drop the gid after dropping the uid | Always gid, then uid (Lab 1) |
Treating --no-seccomp as a config option | Removes the entire syscall sandbox in production | --no-seccomp is a debugging flag only; reviewers reject it in prod configs |
| Assuming MMDS is harmless because there's "no real server" | dumbo parses guest packets — it is attack surface | Block guest egress to 169.254.169.254; keep dumbo minimal (Lab 3) |
How to verify you are ready to start
# You should be able to answer each of these from the deep dives BEFORE Lab 1.
# 1. Which sandbox layer does the jailer own, and which does it NOT?
rg -ni "seccomp" src/jailer/src/ || echo "correct: the jailer does NOT do seccomp"
# 2. Where do the three seccomp categories live, and what are their names?
rg -n '"vmm"|"api"|"vcpu"' resources/seccomp/$(uname -m)*.json | head
# 3. Where does Firecracker install the filter (NOT the jailer)?
rg -n "install_filter|apply_filter|seccomp" src/vmm/src/seccomp.rs
# 4. Which two device nodes does the jailer mknod, and why exactly those two?
rg -n "/dev/kvm|/dev/net/tun|mknod" src/jailer/src/
If you cannot explain, from memory, the difference between what the jailer does and what seccomp does, re-read the jailer deep dive and the seccomp deep dive before continuing. The labs assume that division is second nature.
Next: Lab 1: The jailer in depth — run Firecracker under the real jailer, construct the process sandbox by hand, observe it from outside, and break it deliberately to see what each barrier actually stops.