Back to blog
August 26, 202611 min readEverstack Team

How to Safely Run AI-Generated Code

A practical threat model for executing code an LLM wrote: why containers alone are the wrong boundary, what microVMs and gVisor actually buy you, and the network, filesystem and resource controls that matter more than the isolation primitive.

SecuritySandboxesAI AgentsEngineering

An agent writes Python. Something runs it. That sentence contains a security decision most teams make by accident.

The code is untrusted. Not because the model is malicious, but because the model's output is a function of its input, and its input includes text from your users. Prompt injection is not exotic anymore. If a user can influence what an agent writes, and the agent's code runs with any privilege at all, you have handed a stranger a shell with extra steps.

This post is a practical threat model: what you are defending against, which isolation primitive fits, and the controls that matter more than the primitive choice.

What you are actually defending against

Four failure modes, roughly in order of likelihood.

Accidental destruction. The model writes rm -rf on a path that resolves somewhere unexpected, or a loop that fills the disk. No attacker required. This is the common case and the cheapest to defend.

Resource exhaustion. An infinite loop, a fork bomb, a runaway memory allocation. One tenant's bad generation takes down the host and everyone on it.

Data exfiltration. The generated code reads a credential from the environment, or a file from a mounted volume, and POSTs it somewhere. This is the one prompt injection targets, and the one most sandboxes are weakest against, because the isolation is usually about the kernel and not about the network.

Container or VM escape. The rarest, the most severe, and the one everyone thinks about first. Worth defending, but if you defend only this you have prioritised backwards.

Choosing an isolation primitive

Containers share the host kernel. The isolation is namespaces and cgroups, which is a boundary designed to separate cooperating workloads, not to contain hostile ones. Container escapes are a recurring class of vulnerability, and "we run it in Docker" is not the answer to "how do you contain untrusted code".

That does not make containers useless. It makes them insufficient on their own for code a model wrote in response to user input.

gVisor puts a user-space kernel between the workload and the host, intercepting syscalls. It meaningfully shrinks the attack surface without full virtualisation, at some syscall-heavy performance cost. A reasonable middle position.

MicroVMs (Firecracker and similar) give each workload its own kernel with a deliberately minimal device model. This is where most of the agent sandbox category has landed, and for good reason: a real hardware-assisted boundary with startup measured in tens of milliseconds. E2B uses Firecracker. So does Everstack, alongside Docker and Kubernetes backends.

Full VMs are stronger and slower to start. Rarely the right trade for per-request execution.

Default to microVMs for model-generated code. Use containers only when you control the code, or when a microVM is genuinely not available and you have compensating controls.

The controls that matter more than the primitive

Here is the part most guides skip. A microVM with unrestricted network egress is a worse security posture than a container with none, because exfiltration does not require an escape.

Network egress is the control that matters most

Default deny. Then allow specifically.

Most agent code needs either nothing, or a package registry, or one API. Work out which and allow only that:

default:            DENY all egress
package install:    ALLOW pypi.org, files.pythonhosted.org (or a mirror)
specific API:       ALLOW the exact host, on 443 only
metadata service:   DENY 169.254.169.254 explicitly
peer sandboxes:     DENY

Two of those deserve emphasis.

The cloud metadata endpoint at 169.254.169.254 hands out instance credentials to anything that can reach it. It has been the pivot in a long list of real breaches. Block it explicitly rather than assuming your network policy covers it.

Peer sandboxes. If every sandbox lands on one flat network, code in sandbox A can reach an agent or service in sandbox B. If those sandboxes belong to different tenants, that is a cross-tenant compromise that never touched the host kernel. This is an easy thing to get wrong, because the isolation looks correct from the host's perspective. Per-sandbox network policy that drops inter-guest traffic is not optional in a multi-tenant system.

Filesystem

Mount nothing you would not hand over. In practice:

  • A writable scratch directory, sized and discarded with the sandbox.
  • Read-only for anything the code needs to read.
  • No host paths, no shared volumes between tenants, no credential files.

If the workload needs data, pass it in as input rather than mounting a volume the code can enumerate.

Secrets

The generated code should never see a credential it does not need. Two rules:

Do not put provider API keys in the sandbox environment. If the code needs to call a model, route it through a gateway that holds the key and enforces per-tenant limits, so the sandbox holds a scoped token rather than a provider key. That way a leaked token is revocable and rate-limited rather than a bill.

Scope any token you do inject to the single tenant and the single operation, with a short expiry.

Resource limits

Set all four, and set them low:

CPU:        capped, with a hard ceiling not just a share
Memory:     hard limit, OOM-kill rather than swap
Disk:       quota on the writable layer
Wall clock: a timeout that actually fires

The wall clock one is the most commonly missed. A sandbox with no timeout is a resource leak waiting for its first infinite loop. Kill it and return an error; a hung request is worse than a failed one.

Lifecycle

One sandbox per unit of work, destroyed afterwards. Reusing a sandbox across tenants means state from one leaks into another, and it converts a per-request blast radius into a per-pool one.

Do not build this yourself

Everything above is achievable with Firecracker, iptables and cgroups directly. It is also a lot of surface to get right, and the failure mode is silent: a misconfigured egress rule does not throw an error, it just quietly permits exfiltration until someone notices.

The reasonable options:

  • E2B: Apache-2.0 across two repositories, Firecracker, excellent developer experience. The commercial product is hosted, and self-hosting is documented separately in a root-level self-host.md deployed with Terraform, GCP GA and AWS beta. Concurrency is tiered at 20 on Hobby and 100 on Pro, purchasable to 1,100.
  • Daytona: fast and well designed, but note that its open source repository states it has received no updates since June 2026, when core development moved to a private codebase, and that the LICENSE file was deleted on 23 June 2026, so GitHub now reports no licence at all. The last open release, v0.190.0, is AGPL-3.0. Its isolation is contested by its own materials: the docs say default sandboxes are Linux containers, the README claims a dedicated kernel. Their hosted product is unaffected; the self-hosted build is an unmaintained snapshot, including for security fixes. For a security boundary, both of those matter.
  • Modal: general serverless compute with first-class GPUs, hosted only. Isolation is gVisor, and the network controls are good: block_network drops all egress, an outbound CIDR allowlist narrows it, and sandboxes cannot reach other workspace resources. Sandboxes are priced above standard Modal compute, so budget off the Sandbox rate.
  • Everstack: Apache-2.0, Docker, Firecracker and Kubernetes backends, self-hosted by default, with per-sandbox network policy and a gateway that holds provider credentials so the sandbox never does.

A checklist

Whatever you use, verify these rather than assuming:

  • Isolation is a microVM or gVisor, not a bare container, for model-generated code.
  • Egress is default-deny with an explicit allowlist.
  • 169.254.169.254 is blocked.
  • Sandboxes cannot reach each other.
  • No provider API keys in the sandbox environment.
  • CPU, memory, disk and wall-clock limits are all set and all fire.
  • Sandboxes are destroyed after one unit of work, never reused across tenants.
  • You have tested the above by trying to break out, not by reading the config.

That last one is the difference between a security posture and a security intention. Write a test that tries to reach the metadata endpoint, a peer sandbox and an arbitrary host, and assert that all three fail. Run it in CI. Configuration drifts, and a test is the only thing that notices.