all posts

Firecracker vs Podman: which runs untrusted code?

Ajay Kumar··9 min read

"Firecracker vs Podman" is a fairer fight than "Firecracker vs Docker," because Podman closed a lot of the gap that made rootful Docker scary. Podman is a daemonless, drop-in-Docker-compatible OCI container engine that runs containers rootless by default, wrapped in user namespaces. That's a real, meaningful improvement in the default security posture. But it's still a container engine — the container still runs on the host's one shared kernel. And that single fact is the whole reason this comparison exists. For trusted first-party workloads, Podman is excellent. For untrusted, multi-tenant, or AI-generated code, you want a boundary the kernel can't be the weak link in.

What Podman actually is (and does well)

Podman is a container engine that speaks the OCI standard and mirrors the Docker CLI almost verb-for-verb — `podman run`, `podman build`, `podman ps` — so much so that `alias docker=podman` is a well-worn move. Where it differs from classic Docker is architecture, and the differences are genuine wins:

  • Daemonless — there's no long-lived root daemon owning every container. Podman forks the container as a child of your own process (via conmon), so there's no single privileged socket that, if compromised, hands an attacker the whole host.
  • Rootless by default — a normal, unprivileged user can run containers without ever being root on the host. This removes an entire category of "the daemon runs as root, so a bug in it is root" risk.
  • User namespaces — rootless Podman maps container UID 0 to your unprivileged host UID via subuid/subgid ranges. "Root" inside the container is a nobody outside it, so a process that thinks it's root has no real host privilege.
  • Pods — Podman groups containers into Kubernetes-style pods sharing a network namespace, which is lovely for local dev that mirrors production.
  • systemd-native — Quadlet/`podman generate systemd` makes containers first-class services, which is a much cleaner ops story than babysitting a daemon.

None of this is marketing fluff. Rootless + user namespaces genuinely reduces the blast radius compared to rootful Docker: if a process breaks out of the container's namespaces, it lands as your unprivileged user, not as root. That is a real safety improvement and a good reason to prefer Podman for a lot of workloads. (Exact defaults and mappings vary by version and distro — verify against the Podman docs for your setup.)

Rootless is a great seatbelt. It is still not a separate car. It makes a crash far more survivable — but you and the kernel are still riding in the same vehicle.

The catch: it's still one shared kernel

Here's the load-bearing fact, and it isn't a knock on Podman's engineering — it's inherent to the container model. Every Podman container, rootless or not, makes its syscalls against the host's Linux kernel. User namespaces change who the process appears to be; seccomp narrows which syscalls it may make; namespaces change what it can see. But the kernel servicing the surviving syscalls is the same kernel your host and every neighbor container are running on. The entire reachable Linux syscall surface — hundreds of calls plus the drivers, filesystems, and networking paths behind them — is the attack surface.

Rootless helps a lot, but it doesn't change the shape of the threat. A kernel local-privilege-escalation bug reachable through an allowed syscall is still a kernel LPE. And user namespaces are a double-edged sword here: they're powerful precisely because they expose kernel code paths (mount, network, and namespace machinery) to unprivileged users — code that has, historically, contained its own share of exploitable bugs. So a container escape or a kernel LPE still threatens the host. Rootless raises the bar for turning that escape into full root; it does not remove the kernel from the trust boundary.

The math that matters: with Podman, one kernel privilege-escalation bug reachable through an allowed syscall can mean a container escape — and the kernel you'd be escaping through is the same one your other tenants share. Rootless makes that harder to weaponize into root. It doesn't move the boundary off the shared kernel.

Firecracker's boundary: a kernel per guest

Firecracker draws the boundary a level lower. Instead of carving up the host kernel with namespaces, it boots a whole new guest kernel inside a hardware-virtualized microVM (KVM). The workload makes its syscalls against its own private guest kernel and never touches the host kernel at all. The only path from guest to host is a tiny virtio device model — a couple of emulated devices plus the KVM ioctl interface — running behind a jailer that chroots, drops privileges, and applies its own seccomp for defense in depth.

So the same class of bug lands very differently. A kernel LPE now compromises the guest kernel — which is that one VM's kernel, isolated from the host and from its neighbors. To reach the host, an attacker has to find and exploit a bug in the VMM's device emulation or in KVM itself: a far smaller, far more heavily audited surface than the full Linux syscall ABI a container leaves exposed. This is exactly why AWS Lambda runs untrusted, multi-tenant code on Firecracker microVMs rather than on bare containers — rootless or otherwise.

Side by side

  • Isolation boundary — Podman: namespaces + cgroups + seccomp + user namespaces around a process on the shared host kernel. Firecracker: a hardware-virtualized microVM; the guest talks to its own kernel and only a tiny virtio + KVM surface faces the host.
  • Kernel — Podman: none of its own; every container uses the host kernel. Firecracker: a full, real guest kernel per microVM.
  • Privilege model — Podman: rootless by default, container root maps to an unprivileged host UID (a genuine improvement over rootful Docker). Firecracker: the guest can be root inside its own VM and it still means nothing to the host.
  • Untrusted / AI-generated code — Podman: better than rootful Docker, but a kernel LPE or escape still threatens the host. Firecracker: designed for it — a kernel bug stays inside one VM.
  • Startup / density — Podman: near-zero overhead beyond the process, very high density, extremely fast start (verify against your workload). Firecracker: a few MB per guest for kernel + device model; snapshot-restore makes per-create cost sub-second in practice.
  • Best fit — Podman: trusted first-party services, local dev, rootless CI, Docker-compatible packaging. Firecracker: multi-tenant, untrusted, or model-generated workloads where you want the kernel bug contained.
  • Ecosystem — Podman: drop-in Docker CLI, pods, systemd integration, huge familiarity. Firecracker: needs a VMM path (firecracker-containerd, Kata, or a platform), but a real guest kernel gives near-total Linux compatibility.

Rootless Podman, concretely

The rootless story rests on subuid/subgid ranges — the host tells the kernel which UID/GID ranges your user is allowed to map inside a user namespace. It's a real boundary, and it's why "root" inside a rootless container is harmless outside it:

# Rootless Podman maps container UID 0 -> your unprivileged host UID.
$ cat /etc/subuid
alice:100000:65536          # alice may map 65536 UIDs starting at 100000

$ id -u                     # you are a normal user on the host
1000

# Run a container as an unprivileged user, no daemon, no host root:
$ podman run --rm -it alpine id
uid=0(root) gid=0(root)      # "root" ... but only inside the userns

# Prove it from the host side while the container runs:
$ podman top <ctr> huser hpid   # the container's root is host uid 100000, not 0

# Good hardening, all still on the SHARED host kernel:
$ podman run --security-opt no-new-privileges \
             --cap-drop=ALL \
             --read-only alpine my-workload
# seccomp + dropped caps + userns shrink the surface. They do not remove
# the host kernel from the trust boundary. One kernel LPE still escapes.

This is good hygiene and you should do it. It just doesn't change the fundamental question: the code inside is talking to your kernel. If you wrote that code, fine. If a model wrote it, or a stranger uploaded it, "harden the container" is mitigation, not isolation.

When Podman is genuinely the right call

Most containers in the world are trusted first-party code, and Podman is a superb way to run them. Your own services, your build steps, your internal tooling, a local dev stack that mirrors production, rootless CI runners for repos you control — in all of these the shared-kernel risk is a risk you already own, and Podman gives you a daemonless, rootless, Docker-compatible runtime with near-zero overhead and excellent density. Reaching for a microVM here would be paying for a boundary you don't need against a threat you don't have. If you're choosing between rootful Docker and rootless Podman for trusted workloads, Podman is the better default. Full stop.

The line to watch for is trust. Podman's boundary is exactly as strong as the assumption that the code inside isn't actively hostile to the kernel. Hold that assumption and Podman is excellent; drop it and the shared kernel becomes the thing standing between an attacker and the whole host — with rootless as a valuable but not impregnable extra layer.

When you need a VM boundary

The moment the code is untrusted, multi-tenant, or generated at runtime, the calculus flips. AI agents executing model-written commands, per-user code playgrounds, code interpreters, CI runners for arbitrary repos, per-customer databases — in all of these you cannot assume the code is friendly to the kernel, because you didn't write it and often no human even read it before it ran. That's the case for a per-guest kernel: you want a bug in the workload's kernel to stay in the workload's kernel.

This is the line PandaStack is built on. Every sandbox, database, and app is its own Firecracker microVM with its own guest kernel under the jailer — created via snapshot-restore so the VM boundary costs you almost nothing at create time. The ergonomics stay container-grade even though the boundary is VM-grade — a create lands around 49ms restoring a snapshot (~179ms p50, ~203ms p99 end-to-end; only the very first cold boot is around 3s):

# Podman:  podman run --rm alpine python3 -c 'print(sum(range(100)))'
#          -> runs against the SHARED host kernel.
#
# PandaStack: the same one-liner, but inside a Firecracker microVM with its
#             own guest kernel. The untrusted code's syscalls hit the GUEST
#             kernel, never the host's.

from pandastack import Sandbox

# One Firecracker microVM, created via snapshot-restore (~49ms restore;
# ~179ms p50 / ~203ms p99 end-to-end). Root inside the guest is still
# nothing to the host — because the host kernel isn't in the loop at all.
sbx = Sandbox.create(template="base", ttl_seconds=300)

result = sbx.exec("python3 -c 'print(sum(range(100)))'")
print(result.stdout, result.exit_code)

sbx.destroy()   # the whole microVM goes away — nothing leaks to the next task

Under the hood the same snapshot-and-fork moat applies: a same-host copy-on-write fork lands in roughly 400–750ms and a cross-host fork in about 1.2–3.5s, and each agent pre-allocates 16,384 /30 subnets so per-sandbox networking is set up in single-digit milliseconds. You get the Podman-shaped developer experience — one call, run a command, tear it down — with a hardware boundary underneath instead of a shared kernel.

It's not Podman versus Firecracker as enemies. Podman for trusted code where the shared kernel is a risk you own and rootless is a fine extra layer; Firecracker for untrusted or multi-tenant code where you want the kernel bug to stay inside one VM. Plenty of real stacks run both — Podman for the platform's own services, a microVM runtime for whatever arbitrary code the platform is asked to execute.

The honest bottom line

Podman is a genuinely better container engine than rootful Docker: daemonless, rootless by default, user namespaces on, drop-in Docker-compatible, with pods and clean systemd integration. For trusted, first-party code it's a great default and its rootless posture is a real security improvement. But it's still a container engine, and every container rides the host's one shared kernel — so a kernel LPE or a container escape still threatens the host. Rootless is a great seatbelt; it is still not a separate car. Firecracker replaces the shared-kernel boundary with a per-guest kernel behind hardware virtualization, trading a few MB and a guest boot (made sub-second by snapshot-restore) for the property that a kernel bug is contained to one VM. If the code is yours, Podman. If the code is arbitrary, untrusted, or model-generated, put a VM boundary under it — and verify Podman's exact rootless defaults against their docs for your version.

Frequently asked questions

Is Podman more secure than Docker?

For its default posture, yes. Podman is daemonless (no long-lived root daemon owning every container) and runs containers rootless by default with user namespaces, so container root maps to an unprivileged host UID. That's a real improvement over rootful Docker. But both are container engines that share the host kernel, so neither turns an untrusted workload into VM-grade isolation — a kernel privilege-escalation bug still threatens the host. Verify exact defaults against the Podman docs for your version.

Can rootless Podman safely run untrusted code?

Rootless Podman reduces the blast radius — if a process breaks out of the container's namespaces it lands as your unprivileged user, not host root — but the container still makes syscalls against the shared host kernel. A kernel local-privilege-escalation bug reachable through an allowed syscall, or a container escape, can still compromise the host. For untrusted, multi-tenant, or AI-generated code, use a hardware-isolated microVM (Firecracker) so a kernel bug stays inside one VM.

What do user namespaces actually protect against?

User namespaces remap UIDs/GIDs so that root inside the container corresponds to an unprivileged user on the host (via subuid/subgid ranges). This means a process that thinks it's root has no real host privilege, which blocks a large class of privilege-escalation paths. The trade-off is that user namespaces themselves expose kernel code (mount, network, namespace machinery) to unprivileged users, which has historically had its own bugs. They raise the bar; they don't remove the shared kernel from the trust boundary.

Is Firecracker slower than Podman to start a workload?

A cold Firecracker boot includes booting a guest kernel, so raw startup is slower than a container's start, and containers have higher raw density because there's no per-guest kernel memory. In practice, platforms snapshot a booted VM and restore per create — PandaStack restores at around 49ms (~179ms p50, ~203ms p99 end-to-end), with only the first cold boot around 3 seconds — which makes the per-create cost sub-second. Verify the container-side numbers against your own workload.

When should I choose Podman over Firecracker?

Choose Podman when the code is trusted first-party code: your own services, build steps, internal tooling, local dev, rootless CI for repos you control. You already own the shared-kernel risk, and Podman gives you a daemonless, rootless, Docker-compatible runtime with near-zero overhead and high density. Reach for Firecracker (or a platform built on it, like PandaStack) when the code is untrusted, multi-tenant, or generated at runtime, and you need a kernel bug to be contained to a single VM.

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.