all posts

Firecracker vs Firejail: confining apps vs containing strangers

Ajay Kumar··9 min read

Firejail is one of those tools that earns loyalty fast. You install it, you type `firejail firefox`, and suddenly your browser is running with a restricted view of your filesystem, a trimmed set of capabilities, and a seccomp filter — with no configuration written by you, because someone already wrote the profile. Multiply that across a few hundred bundled profiles and you have a real, pragmatic security improvement on a desktop Linux box for approximately zero effort. That is a good tool doing a good job.

Then someone asks the follow-up question that brought you here: "can I just Firejail the untrusted code?" Maybe it's a user-submitted script, maybe it's whatever your agent decided to run this time. It's a fair question — Firejail is right there, it's already installed, and it certainly *feels* like a sandbox. The honest answer is that Firejail was designed to confine programs you chose to install, and that is a meaningfully different threat model from containing a program written by a stranger or a language model. This post is about where that line sits and why. (I build PandaStack, which runs Firecracker microVMs, so I'm an interested party — I'll try to earn the benefit of the doubt by being specific about where Firejail is the better answer.)

What Firejail actually does

Firejail is a SUID-capable sandbox program for Linux that confines a process using the kernel's own isolation primitives. It sets up namespaces (mount, PID, network, IPC, UTS, and optionally user), builds the process a restricted filesystem view with private directories and read-only or blacklisted paths, drops Linux capabilities, applies a seccomp-bpf syscall filter, and can apply resource limits and cgroups. It integrates with AppArmor where a distro provides it. None of these mechanisms are exotic — they're the same building blocks nsjail, Bubblewrap, and every container runtime use. What Firejail adds on top is the part that actually gets it adopted: usability.

That usability is a serious engineering contribution and it's worth naming precisely. Firejail ships a large library of per-application profiles — text files describing what Firefox, Thunderbird, VLC, Evince, Transmission, Discord and hundreds of other programs should and shouldn't be allowed to touch. It supports profile inheritance, so common patterns are factored into shared includes. It can be wired in so that launching an application from your desktop menu transparently launches it jailed. The result is that a normal person gets meaningful confinement on their laptop without reading a single kernel man page, which is more than most security tools can claim.

The clearest way to state Firejail's job: it reduces the damage a program you installed on purpose can do if it misbehaves or gets compromised. Your PDF reader shouldn't be able to read your SSH keys. Your chat client shouldn't be rummaging through ~/Documents. Firejail makes that true with one command and a profile someone else already wrote, and that is genuinely valuable.

Firejail in practice

Here's roughly what it looks like, including the shape people reach for when they want to confine a script. Flags, defaults, and profile contents change between releases and distros, so treat this as illustrative and check `man firejail` and your own profile files before relying on any specific behaviour.

# The canonical use case: confine an app you installed on purpose.
# A maintained profile already describes what Firefox may touch.
firejail firefox

# Explicit profile selection, for when you want to know exactly
# which policy file is in force rather than relying on auto-matching.
firejail --profile=/etc/firejail/vlc.profile vlc ~/video.mkv

# The shape people reach for when they want to "just sandbox a script":
# no network, a private (throwaway) home directory, seccomp filtering on.
firejail --net=none --private --seccomp python3 script.py

# Useful while you're actually writing policy: see what the sandbox
# blocked, and what the process really tried to do.
firejail --debug --seccomp python3 script.py
firejail --tracelog --net=none --private python3 script.py

# Worth knowing what you're building on. Check your own install --
# packaging differs by distro and has changed over time.
ls -l "$(command -v firejail)"
ls /etc/firejail/ | head

That third command is a real, useful thing to run, and if the script is one you wrote and are debugging, it's a sensible precaution. The question is what happens when the script isn't yours.

The part that doesn't change: it's your kernel

Every isolation feature above operates on a process running on your host kernel. Namespaces change what the process can *name*. Capability drops change what it's *permitted* to do. Seccomp changes which syscalls it can *make*. None of them change the fact that the syscalls it does make land in the same kernel that runs your host, your other users, and everything else on the machine.

This is not a Firejail flaw — it's the ceiling of the entire shared-kernel model, and nsjail, Bubblewrap, and containers all sit under the same ceiling. But it has a concrete consequence: the attack surface available to the confined code is the Linux syscall interface, which is enormous, actively developed, and finds new local privilege-escalation bugs on a depressingly regular schedule. A kernel LPE is not a partial escape from a namespace sandbox. It's a full one. Your carefully chosen profile was, at the moment of the exploit, permitting exactly the syscall that was used.

A seccomp profile is a list of syscalls you thought of. The exploit will use the one you didn't.

A profile is a claim, not a proof

The profile library is Firejail's best feature and also the place where the mental model most often goes wrong. A profile is allow/deny policy: a human sat down, reasoned about what a specific program legitimately needs, and wrote it down. When that human was a Firejail maintainer working on a well-known application over several years, the result is usually good. When the program is one nobody has ever seen before, there is no profile, and the one you write is only as good as your ability to predict what the program will do.

Two failure directions, and they pull against each other. Too strict and the program breaks in confusing ways — which is exactly the pressure that makes people loosen a profile until things work, at which point the policy has been tuned for functionality rather than for security. Too loose and you have a sandbox-shaped object that permits the thing you were worried about. The dangerous middle case is the one where the profile *runs* the program successfully and you conclude it is therefore confined. It isn't; you've only confirmed the parts you thought of don't break it.

This is the specific hazard of applying Firejail to arbitrary code: an incomplete profile is indistinguishable from a complete one right up until it matters. Enumerating what a known application needs is a tractable problem. Enumerating what "whatever the model writes next" needs is not a problem you can finish, because the program doesn't exist yet when you write the policy.

The SUID question, stated plainly

One structural point worth stating factually and without drama. Firejail has historically shipped as a SUID-root binary, because setting up some of the isolation it performs — certain namespace and mount operations in particular — requires privilege that an ordinary user doesn't have. That's a legitimate engineering reason. It also means the tool you're using to build your isolation boundary is itself a privileged local binary that unprivileged users can invoke, and that category of program is held to an uncomfortably high standard: any bug in the privileged setup path is a local privilege-escalation bug rather than a sandbox-escape bug.

This has been discussed openly in the Linux security community for years, and it isn't a secret or an accusation. Packaging and configuration have evolved and vary — some distributions ship it differently, some restrict who may run it, some environments prefer alternatives that avoid SUID entirely by leaning on unprivileged user namespaces (with their own separate trade-offs, which the Bubblewrap comparison covers). The right move is not to take my word for it in either direction: check how your own distribution packages it, whether the binary is SUID on your system, and what your distro's security team says about it. I'm noting the shape of the trade, not grading it.

It also isn't built for fleet use

Set security aside entirely for a moment, because there's a purely operational mismatch that decides a lot of cases on its own. Firejail is a command-line tool for launching an application on a machine a person is sitting at. It is not a platform primitive, and it doesn't pretend to be one.

  • There's no snapshot. You cannot freeze a warm, fully-configured environment and stamp out a thousand identical copies of it, and you cannot fork a running one mid-execution to explore two branches.
  • There's no per-instance network fabric at scale. `--net=none` and per-sandbox veth options exist, but assigning and reclaiming isolated networking for tens of thousands of concurrent instances across a fleet is a different problem than jailing your browser.
  • There's no API. Your control interface is a subprocess and its exit code, so orchestrating at scale means screen-scraping a CLI that was designed for a human.
  • There's no scheduler, no placement, no lease, no notion of which host a workload should land on — all of which you need the moment "create ten thousand of these" becomes a real sentence someone says to you.
  • The lifecycle is a process lifecycle. Pausing, resuming, hibernating, and restoring an environment aren't concepts it has, because a desktop app doesn't need them.

None of this is a deficiency. Firejail is not competing for this job. But if the thing you're building is a code interpreter, a per-user playground, an ephemeral CI runner, or an agent execution backend, you'll discover you need every one of those missing pieces, and you'll end up building a small platform around a tool that wasn't shaped for it.

What Firecracker changes

Firecracker moves the boundary down to hardware. Each microVM gets its own guest kernel running behind KVM, and it reaches the outside world only through a deliberately tiny virtio device model — network, block, vsock, a serial console, an entropy source, over MMIO. No PCI bus, no BIOS, no bootloader, no graphics, no USB, no emulated SATA. Every device that isn't there is code that can't have a bug in it.

When untrusted code inside makes a syscall, it hits its own throwaway guest kernel. A local privilege escalation in that kernel makes the attacker root of a virtual machine that is about to be deleted. To reach your host they'd have to escape the hypervisor itself — a far smaller and far more heavily audited surface than the full Linux syscall interface. And Firecracker doesn't stop at the virtualization boundary: the jailer wraps the VMM process in a chroot, namespaces, cgroups, and dropped privileges, and a seccomp filter restricts the VMM to the small syscall set it actually needs. Note the inversion there — seccomp is applied to the *VMM*, a program whose behaviour is known and fixed, which is exactly the case where syscall filtering is realistic to get right.

The historical objection was cost: nobody wanted to boot a VM per request. Snapshot-restore removes it. Instead of cold-booting, you restore a pre-baked snapshot of an already-running guest. On PandaStack a create is p50 179ms and p99 around 203ms end to end, with the restore step itself around 49ms; a first-ever cold boot with no snapshot yet is roughly 3 seconds, once. Forking a warm machine copy-on-write lands in 400–750ms same-host, or 1.2–3.5s cross-host. Networking is pre-allocated rather than built on demand: each agent holds 16,384 pre-provisioned /30 subnets, so a sandbox gets its own network namespace and tap device without paying setup cost at create time.

The real cost of a microVM (this part is not free)

I'd rather you hear this from me than discover it on a Tuesday. Firecracker requires Linux with KVM — there is no macOS build and no Windows build, and on a Mac you need a Linux VM with nested virtualization just to host it. You supply a guest kernel image and a rootfs; there is no image registry and no `apt install` that produces them for you. Each guest carries real memory overhead that a namespace sandbox simply does not have, because there's an actual kernel and device model in there. The control interface is a REST API on a Unix socket, which is a gift if you're writing an orchestrator and an obstacle if you're a person who wanted to run one program.

So to be completely direct: Firecracker is the wrong tool for sandboxing Firefox on your laptop. If that's your goal, use Firejail, be happy, and stop reading. You do not want a hypervisor, a kernel build, and a tap device standing between you and your web browser.

Side by side

Nine dimensions where the two genuinely diverge. Firejail behaviour varies by version, distro packaging, and which profile is in force — verify anything load-bearing against Firejail's own documentation and your distribution's setup. The latency figures below are PandaStack's measured numbers on our platform, not generic claims about the VMM.

  • Isolation boundary — Firejail: namespaces, capability drops, filesystem views, and seccomp-bpf around a process on your host kernel. Firecracker: a hardware-virtualized microVM with its own separate guest kernel behind KVM.
  • Attack surface — Firejail: the entire Linux syscall interface, minus whatever the profile filters; a kernel LPE is a full escape, and the privileged setup path is itself in scope. Firecracker: the guest kernel (disposable) plus the small virtio and hypervisor surface, with the VMM further wrapped in jailer and seccomp.
  • Policy model — Firejail: allow/deny profiles that must be correct for your specific program; excellent maintained profiles exist for known applications, and none exist for code that didn't exist yet. Firecracker: no per-program policy to author — the code runs freely inside a guest that gets deleted, so correctness of a filter isn't load-bearing.
  • Startup cost — Firejail: essentially instant; it's a process launch plus setup, no OS to start. Firecracker: PandaStack creates a sandbox at p50 179ms / p99 ~203ms via snapshot-restore (~49ms for the restore itself), with a ~3s cold boot only on the very first spawn.
  • Density and footprint — Firejail: as cheap as the process itself, since there's no second kernel anywhere. Firecracker: real per-guest memory overhead for the kernel and device model, offset at scale by copy-on-write memory and a CoW rootfs so identical pages are shared across VMs.
  • Networking — Firejail: per-sandbox options like `--net=none` and veth pairs, aimed at one machine and a handful of jailed apps. Firecracker: a dedicated network namespace and tap device per VM; PandaStack pre-allocates 16,384 /30 subnets per agent so isolation is provisioned ahead of time rather than assembled per request.
  • Snapshot and restore — Firejail: not a concept; the unit is a process, and processes don't freeze and clone. Firecracker: first-class snapshot to a memory file plus state file, restore as the normal creation path, and copy-on-write forking of a warm machine (400–750ms same-host, 1.2–3.5s cross-host).
  • Host OS support — Firejail: Linux only, and behaviour depends on your distro's packaging, kernel configuration, and AppArmor availability. Firecracker: Linux with KVM only — no macOS or Windows build, so on a Mac you need a nested-virtualization Linux host first.
  • Target user — Firejail: a person on a Linux desktop or server confining applications they chose to install, with maintained profiles doing most of the work. Firecracker: a program creating and destroying isolated machines as a unit of work, for code nobody reviewed.

What the microVM version looks like

The same write-then-exec-then-read loop you'd script around Firejail, with a hypervisor underneath instead of a profile. Note what's absent: no policy file, no syscall list, no private-home flag to remember, no debugging session to work out which denied operation broke the program.

from pandastack import Sandbox

# Code from a stranger, a customer, or a model. It gets its OWN guest
# kernel -- so there is no profile to author and no syscall list to be
# right about. The blast radius is a VM that ends when the block ends.
model_written_code = """
import os, subprocess
subprocess.run(['pip', 'install', '--quiet', 'requests'])
import requests
print('uid:', os.getuid())
print('kernel:', os.uname().release)
print('status:', requests.get('https://example.com', timeout=10).status_code)
"""

with Sandbox.create(template="code-interpreter", ttl_seconds=300) as sbx:
    sbx.filesystem.write("/workspace/task.py", model_written_code)

    # It can install packages, open sockets, fork children, poke /proc --
    # all of which would each need a profile decision under Firejail.
    result = sbx.exec("python3 /workspace/task.py", timeout_seconds=30)

    print("exit:", result.exit_code)
    print(result.stdout)
    if result.exit_code != 0:
        print("stderr:", result.stderr)
# VM destroyed on block exit. Nothing it touched survives the run.

The thing worth noticing is not that the API is shorter. It's that the security argument no longer depends on you having predicted the program's behaviour. Under a profile-based sandbox, `pip install requests` is a policy negotiation: network access, writes into site-packages, a compiler if there's a native extension. Under a microVM it's a Tuesday, because the guest is allowed to do whatever it likes to a machine that is scheduled for deletion.

So which one do you actually want?

Here's the decision rule I'd give a friend, and it isn't about which tool is better — it's about who wrote the code and what you're afraid of.

Use Firejail when the threat is misbehaviour

  • The program is one you installed on purpose: your browser, mail client, PDF reader, media player, chat app. This is the case Firejail was built for and it does it well.
  • A maintained profile already exists for it, so the hard thinking about what it legitimately needs has been done by someone who studied the application.
  • What you're guarding against is a compromised or buggy application overreaching — reading files it has no business reading — rather than a program whose stated goal is to escape.
  • You want confinement on one Linux machine, with near-zero overhead, and "one command in front of the app I was already launching" is the ergonomics you need.
  • You're hardening a service you operate and control, as one layer among several, not betting your entire tenancy boundary on it.

Use a microVM when the threat is malice

  • The code was written by someone you've never met, or by a language model thirty seconds ago, and nobody read it before it ran.
  • It needs a real environment — install packages, open sockets, spawn processes, write files — which is precisely the set of things a profile would have to grant one by one until the profile no longer says much.
  • It's multi-tenant: several customers' code on shared hosts, where one kernel bug would unravel the boundary across all of them simultaneously.
  • You need to create and destroy environments programmatically at volume, with an API, a scheduler, and per-instance networking rather than a CLI and a human.
  • You want to freeze a warm, configured machine and restore or fork it — running five candidate fixes in parallel from an identical state, which has no process-sandbox analogue at all.

That's the whole thing, really. Firejail asks "what should this program be allowed to do?" and answers it with a policy file, which works beautifully when you know the program. A microVM asks "what happens if this program does anything at all?" and answers it with a disposable kernel, which is the only answer that holds when you don't. Neither question is the wrong question. Just make sure you're asking the one that matches the code you're about to run.

Frequently asked questions

Is Firejail secure enough to run untrusted code?

It depends heavily on what you mean by untrusted. Firejail is a shared-kernel sandbox — namespaces, capability drops, filesystem views, and seccomp-bpf around a process — so the confined code still makes syscalls into your host kernel, and a kernel local-privilege-escalation bug is a full escape rather than a partial one. For a known application with a maintained profile, where you're guarding against a compromised or buggy program overreaching, it's a genuine and worthwhile improvement. For arbitrary or AI-generated code that you cannot review in advance, the profile you'd have to write is a prediction about a program that doesn't exist yet. In that case a microVM like Firecracker, which gives each run its own disposable guest kernel, is the boundary that doesn't depend on your prediction being right. Verify Firejail's specific behaviour against its own documentation and your distribution's packaging.

What is the difference between Firejail and Firecracker?

Firejail confines a process on your existing host kernel using Linux namespaces, capability dropping, restricted filesystem views, and seccomp filtering, driven by a large library of ready-made per-application profiles. Firecracker is a Virtual Machine Monitor that boots a whole microVM with its own guest kernel behind KVM, exposing only a tiny virtio device model, with the VMM itself further constrained by a jailer and seccomp. The practical difference is what an attacker gets to attack: under Firejail it's the full Linux syscall interface of the kernel you depend on, and under Firecracker it's a throwaway kernel inside a VM that gets deleted. Firejail is built for a person confining apps on a machine; Firecracker is built for a program creating thousands of isolated machines.

Why is Firejail's SUID binary considered a security trade-off?

Firejail has historically shipped as a SUID-root binary because some of the isolation setup it performs — particular namespace and mount operations — needs privileges an ordinary user doesn't have. That's a legitimate reason, but it means the tool building your isolation boundary is itself a privileged local binary that unprivileged users can invoke, so any bug in its privileged setup path becomes a local privilege-escalation issue rather than merely a sandbox-escape one. This has been discussed openly in the Linux security community for years, and packaging has evolved: some distributions ship or restrict it differently, and some environments prefer designs that avoid SUID by relying on unprivileged user namespaces instead, which carry their own separate trade-offs. Check how your own distribution packages it and what your distro's security guidance says rather than assuming.

Can I use a Firejail profile to sandbox AI-generated code?

You can write one, but the exercise has a structural problem: a profile is allow/deny policy authored by a human who reasoned about what a specific program needs, and model-generated code doesn't exist when you write the policy. In practice you either make the profile strict and watch legitimate work break in confusing ways, or you loosen it until things run — which tunes the policy for functionality rather than security. Worse, a program that runs successfully under an incomplete profile looks exactly like a program that's properly confined. Since AI-generated code typically wants to install packages, reach the network, and spawn processes, granting all of that leaves a profile that isn't restricting much. A disposable microVM sidesteps the question by letting the code do anything it likes to a machine that's about to be destroyed.

Is Firecracker slower to start than Firejail?

Yes in absolute terms, though far less than people expect. A Firejail sandbox is essentially instant because it's a process launch with extra setup and there's no operating system to boot. Firecracker historically had to boot a guest, but snapshot-restore removes that step: on PandaStack a create is p50 179ms and p99 around 203ms end to end, with the restore itself around 49ms, and a roughly 3-second cold boot only on the very first spawn when no snapshot exists yet. Forking an already-warm machine lands in 400 to 750 milliseconds on the same host. So the isolation upgrade costs a couple hundred milliseconds rather than the seconds that used to make VMs impractical per request.

Should I use Firejail or a microVM for a multi-tenant code execution service?

For multi-tenant execution, a microVM is the appropriate boundary. The deciding factor is that a shared-kernel sandbox puts every tenant's code on the same kernel, so a single kernel vulnerability unravels isolation across all of them at once rather than affecting one tenant. There's also an operational mismatch: Firejail is a command-line tool for launching applications on a machine, with no API, no snapshot or restore, no scheduler, and no per-instance network fabric designed for tens of thousands of concurrent instances. Building a service on it means building a small platform around a tool that wasn't shaped for the job. Firecracker gives each tenant a separate guest kernel plus its own network namespace and tap device, which is why large serverless platforms use microVMs for exactly this pattern.

Run code in a microVM in one API call.

49ms p50 cold start. Fork, snapshot, and scale to zero.

Start free
Written by Ajay Kumar, Founder, PandaStack.