Firecracker vs AWS Nitro Enclaves: Two Different Threat Models
I build PandaStack, a Firecracker microVM platform, so I get asked about Nitro Enclaves a lot — usually by someone who has been told "just use enclaves" by a security reviewer and is now trying to work out whether that means their sandbox product was a mistake. It doesn't. These two things sit in the same neighbourhood of the virtualization stack and solve almost exactly opposite problems. Firecracker is a VMM you run to protect the host from the guest. Nitro Enclaves are a hardened compute environment designed to protect the guest's data from the operator — including from you, the account owner, and from anyone who has root on the parent instance. Once you see that inversion, most of the confusion resolves itself, and the rest of this post is about the parts that don't.
The shared lineage, stated honestly
They're relatives. Firecracker came out of AWS's work on running enormous numbers of untrusted tenants on shared fleets, and it was open-sourced under Apache-2.0. Nitro Enclaves come out of the same Nitro system and virtualization world, and are built on that hypervisor foundation. So when people notice a family resemblance — a minimal, purpose-built isolated environment with a deliberately tiny interface — they're not imagining it.
But shared lineage is not shared purpose, and I'm going to describe everything AWS-specific here qualitatively rather than quoting specs at you. Enclave sizing rules, supported instance families, region availability, SDK details, and the exact shape of the attestation document all change, and the failure mode of writing them down in a blog post is that someone architects against a stale sentence. Treat everything I say about Nitro Enclaves as the shape of the thing and verify the specifics against AWS's own documentation before you design around them.
What Firecracker actually protects
Firecracker is a Virtual Machine Monitor written in Rust that boots a real guest kernel under hardware virtualization (KVM), with a deliberately minimal device model — virtio-net, virtio-block, virtio-vsock, a serial console, and essentially nothing else. No BIOS, no PCI, no USB, no legacy emulation. It runs behind a jailer that chroots the process, applies seccomp and cgroups, and drops privileges as defense-in-depth. The threat model points outward from the guest: assume the code inside is hostile, and make sure the worst it can do is ruin its own microVM.
That means the guest gets to be a full, useful computer. It has a network stack, a writable filesystem, a shell, a package manager, and every syscall a real Linux kernel offers, because compatibility is a feature when you can't predict the workload. You, the operator, retain total visibility: you can read the guest's disk, attach to its console, snapshot its RAM to a file, and inspect that file at your leisure. That is not a bug in the model — it's the model. The host is the trusted party and the guest is the suspect.
This is also why Firecracker gets to be fast and dense. There's no cryptographic ceremony gating startup, so a snapshot of a fully-booted VM can simply be restored. On PandaStack, every create takes that path: a p50 of 179ms and a p99 around 203ms, of which the snapshot restore itself is roughly 49ms. The genuine cold boot happens once per template at around 3 seconds and is then baked away. Copy-on-write forks land in 400–750ms on the same host, 1.2–3.5s cross-host, and each agent pre-allocates 16,384 /30 subnets so every sandbox gets its own network namespace.
What Nitro Enclaves actually protect
A Nitro Enclave inverts the arrow. You carve an isolated compute environment off a parent EC2 instance — the parent gives up some of its CPU and memory to create it — and the enclave is deliberately built so that the parent cannot look inside. Not "shouldn't." Cannot, by design of the isolation boundary. The person with root on the parent instance, the person with the AWS account credentials, and the person who wrote the deployment pipeline are all outside the trust boundary. That's the entire point: it's confidential computing, and the adversary in the threat model includes the operator.
To make that claim survive contact with reality, an enclave gives up almost everything that makes a computer convenient:
- No external network. There's no NIC, no route to the internet, no VPC attachment. If your code inside wants to call an API, something outside has to do it and hand the bytes in.
- No persistent storage. There's no attached disk and nothing survives termination. State is memory, and memory goes away.
- No interactive access. No SSH, no shell, no attaching a debugger from the parent. If you could shell in, so could an attacker who compromised the parent — that's precisely the door being welded shut.
- Exactly one channel: a local vsock connection to the parent instance. Everything — requests, responses, logs, secrets in and results out — is a byte stream over that socket, and you design the protocol.
- Cryptographic attestation. The enclave can produce a signed attestation document containing measurements (PCRs) of the enclave image — effectively a fingerprint of exactly what code is running.
That last one is why anyone tolerates the first four. Attestation is the feature; the deprivation is the price. Because the enclave can prove which image it is, a key-management service can be configured to release a key only to an enclave whose measurements match an expected value. The key never lands on the parent instance, never lands in an environment variable, never lands anywhere an operator can read it. It goes into a measured piece of code that has no network to exfiltrate it over and no disk to accidentally write it to. That's a genuinely strong property, and it's not one a Firecracker microVM gives you, because Firecracker was never trying to.
An enclave's value isn't that it's isolated. Lots of things are isolated. Its value is that it can prove to a third party which code is running inside it, and that proof can be made a precondition for handing over a secret.
Side by side
Line them up on the dimensions that actually drive the decision. AWS-side entries are qualitative on purpose — check current AWS docs for specifics.
- Threat model — Firecracker microVM: the guest is untrusted; protect the host and the neighbours. Nitro Enclave: the operator is untrusted; protect the data inside from everyone outside, including the account owner.
- Who you're protecting from — Firecracker microVM: the code you're running, and the tenant who supplied it. Nitro Enclave: your own admins, your parent instance's root user, a compromised deployment pipeline, and yourself.
- Network — Firecracker microVM: a full stack; per-VM netns, TAP device, routing and egress policy entirely under your control. Nitro Enclave: none. No NIC, no internet, no VPC — anything external must be proxied by the parent.
- Persistent storage — Firecracker microVM: a real filesystem, copy-on-write rootfs, optional durable volumes. Nitro Enclave: none; memory only, and it dies with the enclave.
- Attestation — Firecracker microVM: not part of the model; the host can read guest memory, so there's nobody to prove anything to. Nitro Enclave: first-class — signed measurements of the enclave image, usable as a condition in a key policy.
- Boot and iteration speed — Firecracker microVM: snapshot-restore create at 179ms p50 on PandaStack, edit-and-rerun in seconds. Nitro Enclave: build an image, measure it, launch it, and re-do that cycle for every change — slower and more ceremonial (verify current behaviour with AWS).
- Snapshots and fork — Firecracker microVM: freeze a running VM's full RAM and device state, restore it mid-instruction, CoW-fork it (400–750ms same-host). Nitro Enclave: not the model — a snapshottable enclave whose memory you could read on disk would defeat its own purpose.
- Density — Firecracker microVM: very high; thousands per host is an operational question, and the tiny device model keeps overhead small. Nitro Enclave: low by design — you carve resources off a parent instance, so it's a small number of enclaves per host, sized deliberately.
- Debuggability — Firecracker microVM: full — console, shell, exec, file access, snapshot inspection. Nitro Enclave: minimal — structured logs over vsock, and a debug mode that (deliberately) invalidates the attestation measurements you'd rely on in production.
- Portability — Firecracker microVM: any Linux KVM host — AWS, another cloud, bare metal, on-prem, a laptop with nested virt. Nitro Enclave: an AWS Nitro-based EC2 feature; it goes where AWS goes.
- Typical workload — Firecracker microVM: multi-tenant code execution, AI agent tool calls, CI jobs, per-customer app hosting, anything arbitrary. Nitro Enclave: a small, well-defined step that touches raw secrets — key operations, PII tokenization, a clean-room join, a signing service.
- What it costs you — Firecracker microVM: you operate a fleet (hosts, kernels, networking, on-call). Nitro Enclave: you rebuild your app's I/O around a single socket and give up interactive debugging.
The decision framework
Almost every real question I get collapses into one of three buckets. Find yours and the answer stops being controversial.
"I run other people's code and need it not to hurt my host or other tenants"
This is microVMs. Firecracker, or something built on it. You need a real guest kernel so arbitrary code just works, a hardware boundary between tenants, per-VM network control so a crypto miner can't phone home to a Stratum pool, and a create path fast enough that you can afford a fresh VM per task instead of recycling one.
Nitro Enclaves are a genuinely bad fit here, and not because they're weak — because they're pointed the wrong way. Your untrusted code would need network access it can't have, a filesystem it doesn't get, and a package manager that can't reach a registry. You'd need to rebuild an image and re-measure it for every tenant's workload. And the property you'd be paying for — that you can't see inside — is actively the opposite of what you want, because when a tenant's job wedges you need to look at it. You would be buying a blindfold and calling it a security control.
"I process data so sensitive that my own operators must not see it"
This is an enclave, or an equivalent confidential-computing offering from whatever cloud you're on. The tell is that your requirement is written in the language of the people who work at your company, not the people who use your product: PII you're contractually forbidden from viewing, private keys you'd rather not be able to steal, PHI under a regulatory regime with meaningful teeth, or a clean room where two competitors' data meet and neither party trusts you as referee.
The load-bearing part is attestation, not isolation. If your compliance story is "we promise our admins don't look," a microVM gives you nothing an audit log doesn't. If your story is "the key is only released to a measured piece of code, and here is the measurement," that's an argument that survives an adversarial reviewer. Design the whole thing around what you can prove.
"Both" — and yes, they compose
This is the interesting case, and it's more common than people expect. You run a fleet of microVMs for the untrusted workload — customer code, agent tool calls, whatever — and one small enclave for the single step that touches the raw secret. The microVM does the messy work and produces a request. The enclave does the one operation that must be unobservable and returns a result. Neither tool is asked to do the other's job.
A concrete shape: an AI agent runs in a microVM, generates code, reads a database, does its thing — all fully visible to you, because it's untrusted and you want visibility. When it needs to decrypt a customer's private document, that decrypt happens in an enclave that holds the key, and the agent's microVM never sees the key material, only the plaintext it's entitled to. Same system, two boundaries, pointed in opposite directions.
The developer experience tax, honestly
Nobody warns you about this part enough. The workflow, at a shape level, is: build a container image, convert it into an enclave image, note the measurements that conversion produces, and launch it against a parent instance with resources carved off. Roughly:
# SHAPE ONLY — flags and tooling change; check current AWS docs.
# 1. Build your app the normal way. This part is familiar.
docker build -t kms-signer:1.0 .
# 2. Convert the container image into an enclave image. The output
# includes measurements (PCRs) — a fingerprint of exactly this image.
# Write these down. They are the thing your key policy trusts.
nitro-cli build-enclave \
--docker-uri kms-signer:1.0 \
--output-file kms-signer.eif
# -> PCR0/PCR1/PCR2 ... (record these)
# 3. Launch it, carving CPU + memory off the parent instance.
nitro-cli run-enclave \
--eif-path kms-signer.eif \
--cpu-count 2 \
--memory 2048
# 4. Your debugging options, in full:
nitro-cli describe-enclaves # is it alive?
nitro-cli console --enclave-id <id> # ONLY in debug mode, which
# zeroes the measurements — so the
# attestation you depend on in prod
# is exactly what you cannot have
# while debugging. Enjoy.
# There is no `ssh enclave`. There is no `exec`. There is no /var/log
# you can go read afterwards, because there is no afterwards.The second tax is the protocol. Every byte in or out goes over one vsock connection, which means you write and maintain a small RPC layer that would otherwise have been an HTTP client. The parent becomes a proxy for everything: fetching ciphertext, calling external services, emitting logs, exposing health.
# Shape of the parent <-> enclave channel. Python's socket module speaks
# AF_VSOCK on Linux. Addressing is (CID, port), not (host, port).
import json
import socket
ENCLAVE_CID = 16 # the enclave's context ID, from describe-enclaves
PORT = 5005
# ---- parent side: the enclave's only window on the world -------------
def ask_enclave(payload: dict) -> dict:
s = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
s.connect((ENCLAVE_CID, PORT))
s.sendall(json.dumps(payload).encode() + b"\n")
# Length-prefix or delimit properly in real code; a stream socket
# will happily hand you half a message and let you find out later.
resp = b""
while not resp.endswith(b"\n"):
chunk = s.recv(4096)
if not chunk:
break
resp += chunk
s.close()
return json.loads(resp)
# ---- enclave side: no network, no disk, no shell --------------------
def serve() -> None:
srv = socket.socket(socket.AF_VSOCK, socket.SOCK_STREAM)
srv.bind((socket.VMADDR_CID_ANY, PORT))
srv.listen()
while True:
conn, _ = srv.accept()
req = json.loads(conn.recv(65536))
# The real work: fetch an attestation document, present it to a
# key service whose policy names this image's measurements, get
# the plaintext key back, use it, and never let it leave here.
# attestation = get_attestation_doc(nonce=req["nonce"])
# key = kms_decrypt_via_parent(req["ciphertext"], attestation)
conn.sendall(json.dumps({"ok": True}).encode() + b"\n")
conn.close()
# Note what is absent: no requests.get(), no open("/var/log/app.log"),
# no os.environ["SECRET"] set by a deploy script an operator can read.Reproducible builds, because measurements are the product
Here's the constraint that catches teams out. Your key policy trusts a measurement. If the measurement changes, the key stops being released. So your build has to be reproducible enough that you can predict the measurement, or at least deterministically regenerate it and update the policy in lockstep with every deploy. A container image that pulls `latest` at build time, bakes in a timestamp, or resolves dependencies differently on Tuesday will produce a different fingerprint and your enclave will simply stop working with no useful error at the point of failure. Pin everything. Treat the measurement as a release artifact you review, not a number that falls out of CI.
Why "just put the whole app in the enclave" fails
It's the obvious idea and it collapses under its own weight. Your app needs a database, an object store, a payment provider, an identity provider — all network calls the enclave cannot make, so every one becomes a hand-written vsock proxy through the parent. It needs to log, and logs are now a protocol. It needs config, and config is now attested input. It needs to be debugged, and debugging invalidates the attestation. And the surface you were trying to shrink has grown: a large application in an enclave means a large measured image, which means every dependency bump changes the fingerprint you built your key policy on.
The pattern that works is the opposite. Make the enclave small, boring, and stable — one function, minimal dependencies, a fingerprint that changes twice a year. Everything else lives outside where it can have a network and a filesystem and a shell like a normal program. An enclave is a vault, not a datacenter. You put the crown jewels in it, not the office furniture.
Where PandaStack sits, without overclaiming
PandaStack is squarely in the first bucket, and I want to be direct about that rather than fuzz the line. It's an open-source Firecracker platform: every sandbox is its own microVM with its own guest kernel behind a KVM boundary, created via snapshot-restore at a p50 of 179ms, forkable copy-on-write, with a dedicated network namespace per sandbox. That's built to protect your host and your tenants from the code you're running.
It is not a confidential-computing product. It does not attest to anything, and as the operator of the host you can read a sandbox's memory and disk. If your requirement is "my own operators must not be able to see this data," PandaStack is not the answer and I'd rather tell you that than sell you a boundary pointed the wrong way. Use an enclave for that step. Use this for everything around it.
from pandastack import Sandbox
# The first-bucket tool: untrusted code, real guest kernel, hardware
# boundary, its own netns. Created by snapshot-restore (~179ms p50),
# not cold-booted, and destroyed when the block exits.
with Sandbox.create(template="code-interpreter", ttl_seconds=300) as sbx:
sbx.filesystem.write("/workspace/task.py", user_supplied_code)
result = sbx.exec("python3 /workspace/task.py", timeout_seconds=60)
print(result.exit_code, result.stdout, result.stderr)
# Explicit teardown if you're not using the context manager:
# sbx = Sandbox.create(template="code-interpreter", ttl_seconds=300)
# sbx.exec("pip install pandas && python3 /workspace/task.py")
# sbx.kill()
# Note what you CAN do here that you cannot do in an enclave: read the
# filesystem, exec a shell, tail logs, snapshot the running VM. That
# visibility is the feature — the guest is the suspect, not you.The bottom line
"Firecracker vs Nitro Enclaves" isn't a contest, it's a diagnostic question about who you don't trust. If the answer is "the code," you want a microVM: a real kernel, a hardware wall, full networking and storage, fast enough to throw one away per task, and full operator visibility because you're the one who has to debug it at 3am. If the answer is "me, and anyone who can get root on my instances," you want an enclave and the attestation that comes with it, and you should expect to pay for it in developer experience — no network, no disk, no shell, and a debugging story that is mostly stoicism.
Most sophisticated systems eventually want both, with a hard, narrow interface between them: a fleet of microVMs doing the untrusted work, and one small measured enclave holding the one secret that matters. Pick the boundary that points at your actual adversary. And whatever you decide about the AWS side of this, go read AWS's current documentation before you commit an architecture to it — I've described the shape, and the shape is stable, but the details are theirs to change.
Frequently asked questions
Can I run untrusted user code in a Nitro Enclave?
Technically you can put code in one, but it's the wrong tool and the fit is bad in every direction. An enclave has no external network, no persistent storage, and no interactive access — so untrusted code that wants to install a package, call an API, or write a file simply can't, and you'd have to proxy every one of those operations over vsock yourself. You'd also need to rebuild and re-measure the enclave image for each distinct workload, which destroys the iteration speed sandboxing depends on. Worst of all, the enclave's headline property — that you can't see inside it — is exactly backwards for running untrusted code, because when a tenant's job misbehaves you need visibility to investigate. For multi-tenant untrusted execution you want a microVM (Firecracker or a platform on top of it): a real guest kernel, full networking under your control, and a create path fast enough to use a fresh VM per task.
Do I need both Firecracker and Nitro Enclaves?
Only if you have both problems, but a surprising number of teams do, and the two compose cleanly. The pattern is a fleet of microVMs running the untrusted or messy work — customer code, AI agent tool calls, CI jobs — plus one small enclave that performs the single operation which must remain unobservable to your own operators, such as a decrypt, a signature, or a tokenization step. The microVM prepares a request and hands it over; the enclave does the sensitive operation and returns a result; the key material never leaves the measured environment. Keep the enclave small and stable so its measurements rarely change, and keep everything that needs a network, a filesystem, or a debugger outside it. If you only have the first problem, you only need microVMs, and adding an enclave buys you complexity without buying a property you're using.
What is enclave attestation and why does it matter?
Attestation is the enclave's ability to produce a cryptographically signed document containing measurements — PCR values — that fingerprint exactly which enclave image is running. That matters because it turns a promise into a proof. A key management service can be configured so that a particular key is released only to an enclave whose measurements match an expected value, which means the key never has to exist on the parent instance, in an environment variable, or anywhere an administrator could read it. This is the property that justifies giving up networking, storage, and interactive access, and it's the one thing a plain microVM fundamentally cannot offer, because the host operator can read guest memory by design. The practical catch is that your build must be reproducible enough to predict or deterministically regenerate those measurements — if the fingerprint drifts, the key stops being released. Verify current attestation mechanics against AWS's documentation.
Are Nitro Enclaves built on Firecracker?
They share a lineage rather than an identity. Both come out of AWS's Nitro system and its virtualization work, and Nitro Enclaves are built on that hypervisor foundation — which is why the two feel related when you look at how minimal and purpose-built each one is. But Firecracker is an Apache-2.0 open-source VMM you can download and run on any Linux KVM host, aimed at protecting the host from untrusted guests, while Nitro Enclaves are an AWS EC2 feature aimed at protecting data inside from the operator outside. The internals, the guarantees, and the feature sets are AWS's to define and change, so treat any claim about how they relate — including this one — as directional and check AWS's own documentation before designing around it.
How do you debug something running inside a Nitro Enclave?
Sparsely, and that's the honest answer. There's no SSH, no exec, no attaching a debugger from the parent instance, and no log file to read afterward because there's no persistent storage and no afterward. Your realistic options are structured logging emitted over the vsock channel to a listener on the parent, careful error typing so a failure tells you where it happened without leaking what it was processing, and a debug mode that lets you see console output but deliberately zeroes the attestation measurements — meaning the configuration you can observe is not the configuration you actually ship. The practical mitigation is to make the enclave do as little as possible: keep it to one small, well-tested function with minimal dependencies, exercise that logic thoroughly outside the enclave where you have a normal debugger, and only then measure and deploy it. Budget substantially more time for the first enclave deployment than the amount of code would suggest.
49ms p50 cold start. Fork, snapshot, and scale to zero.