How to Run AI-Generated Code Safely in Production
A model can write and ship an app before anyone reads it. Here is the practical playbook for letting that code touch real data without trusting a line of it.
To run AI-generated code safely in production, stop asking whether the code is good and start controlling the environment it executes in. A model wrote it, nobody reviewed it, and it is pointed at your real data. Treat it the way you would treat a patch from an anonymous contributor: untrusted by default. Three things have to be true before that code runs, and each is a concrete engineering decision. Miss any one and the other two won't save you.
1. Strong isolation: give it a kernel it can't reach
The first requirement is a hard boundary between the untrusted code and everything else: your host, your network, your other tenants. Most teams reach for a container here and stop, and that is where most teams go wrong. Containers share the host kernel. As gVisor's own documentation puts it, in that model "the workload is only one system call away from host compromise," so any kernel or runtime bug becomes an escape. This is not theoretical. CVE-2024-21626, a file-descriptor leak in runc 1.1.11 and earlier rated CVSS 8.6 High, let a container process operate in the host filesystem namespace and overwrite host binaries, a recent demonstration that container escapes remain possible.
The stronger primitives put a second, independent kernel in the way. Firecracker runs each workload as a microVM on hardware virtualization (KVM), exposing a deliberately minimal device model of VirtIO net and block, a serial console, and a partial keyboard controller, so there is almost no emulated surface to attack. Its design assumes the worst: "all vCPU threads are considered to be running malicious code as soon as they have been started," and a companion jailer process drops privileges and chroots Firecracker itself before the guest boots. gVisor takes a different path to the same goal. Its Sentry intercepts and reimplements syscalls in userspace and "never passes any system call directly to the host," restricting itself to a small allowlist of host syscalls. Either way, an attacker now has to break two independent kernels that share no code.
With microVM or userspace-kernel isolation, escaping requires "simultaneously exploiting the gVisor Sentry kernel and the host Linux kernel, which do not share any code." That is what separates a boundary from a suggestion.
2. Attributable logging: what ran, what it touched, as whom
Isolation contains the blast radius. It says nothing about what happened. The second requirement is a log of every action the code took: which app made the call, what data or resource it reached, and under which identity, captured at the boundary rather than self-reported by the app. This is the audit trail an incident responder and an auditor both need, and it is exactly what most setups lack.
OWASP lists Security Logging and Monitoring Failures in its Top 10 precisely because "breaches cannot be detected" without it. The standard is specific: log every access-control and validation failure "with sufficient user context to identify suspicious or malicious accounts." For AI-generated apps the identity dimension is where it gets serious. When a fleet of agents is generating and running software, "which app touched this customer record, and who approved it?" needs a real answer. Capture stdout, stderr, egress, and data access per execution, tied to a signed provenance record of what was approved to run.
3. Deny-by-default policy, enforced before the data
The third requirement is the one teams skip, and it is the one that actually decides safety. Isolation and logging are passive. They contain and record. Policy is active: it decides, at call time, whether an action is allowed before the code touches data. And it has to default to deny. Modal's guidance is blunt on the network case: isolation "should be the default configuration. Sandboxes start with no network access, and you explicitly enable only required connections." Generalize that past networking to every capability, including data reads, writes, secrets, and external calls. The model gets an explicit allowlist of what it may do, and everything absent from that list is refused. A missing authorization check in the generated code stops mattering, because the runtime never had permission to make the call in the first place.
Where this enforcement lives matters as much as what it says. Put it in a library the model is asked to call and the model can forget to call it, or a prompt injection can route around it. Put it in the boundary the code runs inside and it holds regardless of what the model wrote.
The honest catch
Here is the part the vendor blogs skip: assembling all three, correctly, for every workload is real engineering. Isolation you can buy, since microVM sandboxes are a solved product. Attributable per-call logging and runtime-enforced deny-by-default policy are bespoke plumbing you build and maintain per app. So most teams do requirement one, get a comforting sandbox, and quietly ship without two and three. What they have then is a good environment for agent iteration and a poor one for production data.
"Safe" is a bad thing to rebuild for each app. It works far better as the default state of the place your code runs, with all three requirements on by default and inherited by every workload.
Sources
- CVE-2024-21626, runc container escape via leaked file descriptor (CVSS 8.6 High), NIST National Vulnerability Database · nvd.nist.gov/vuln/detail/CVE-2024-21626
- "Introduction to gVisor security," gVisor documentation ("one system call away from host compromise"; dual-kernel exploitation) · gvisor.dev/docs/architecture_guide/intro
- Firecracker design documentation (KVM microVM, minimal device model, jailer, "malicious code as soon as they have been started") · github.com/firecracker-microvm/firecracker
- OWASP Top 10 (2021) A09: Security Logging and Monitoring Failures · owasp.org/Top10/2021/A09