A Zero-Trust Architecture for Executing Untrusted Code
Zero-trust reshaped how we think about network security by killing one bad assumption: that being inside the perimeter means you're trustworthy. The principles — never trust, always verify; assume breach; least privilege; explicit per-request authorization; microsegmentation; short-lived credentials; full auditability — are now table stakes for how we treat users and services. They apply just as cleanly to a problem that's exploded in the last couple of years and gets far less rigorous treatment: running code you didn't write. LLM-generated code, user plugins, tenant scripts, agent tool calls. Zero-trust means you don't trust the code — and you really don't trust the code an LLM wrote at 2am to make a test pass.
Most 'run untrusted code' setups quietly violate the zero-trust principles they'd never violate for a network. They run the code in a shared, long-lived worker (no assume-breach), with ambient host credentials (no least-privilege), open egress (no microsegmentation), reusing state across executions (no explicit per-request authorization), and no per-execution audit trail. This post takes the zero-trust principles one at a time and maps each to a concrete control for executing untrusted code — the architecture that falls out is per-execution, hardware-isolated, credential-starved, and disposable. I'm Ajay; I build PandaStack, a Firecracker microVM platform, so the controls here are the ones I've had to make routine rather than aspirational.
Assume breach → per-execution hardware isolation
The keystone principle. Zero-trust doesn't ask 'is this code malicious?' — it assumes the execution environment will be compromised and designs for the blast radius when, not if, the code turns hostile. Applied to code execution, that rules out the shared, long-lived worker immediately: if one execution can compromise the environment, and the environment is shared and persistent, then one hostile execution compromises everything that runs there before and after.
The control is a fresh, hardware-isolated boundary per execution. A Firecracker microVM per run gives the untrusted code its own guest kernel under KVM hardware virtualization — so 'the environment is compromised' means one disposable VM is compromised, not your host and not the next execution. This is precisely why a shared-kernel container fails the assume-breach test for untrusted code: a kernel local-privilege-escalation or a container escape crosses the shared boundary, so 'assume the container is breached' has to mean 'assume the host is breached.' A separate guest kernel makes assume-breach survivable — the breach is contained to a machine you were going to destroy anyway.
Least privilege → no ambient creds, scoped and short-lived
Zero-trust grants the minimum access needed and nothing by default. The default failure mode for code execution is the opposite: the worker runs with ambient host credentials — a cloud role, a mounted secret, environment variables full of API keys — and every execution inherits all of it. Untrusted code in that process reads the credentials straight out of its environment and now holds your raw keys, not just their effects.
- No ambient host credentials in the execution environment. The guest starts with nothing — no cloud role, no mounted secrets, no inherited env keys. There's nothing for hostile code to read because you didn't put anything there.
- Inject only what this execution needs, scoped and short-lived. If a run legitimately needs to touch storage or a database, mint a per-run credential scoped to exactly that resource, valid for minutes, expiring around the execution's timeout — as a file a specific process reads, not a shared environment.
- Drop the metadata endpoint. Block the guest's route to 169.254.169.254 (and cloud equivalents). This one rule kills the most common escalation path: an SSRF that pulls the host's broader role. The box the code runs on physically can't reach the metadata service.
- Split powers by direction and resource. Read-only where reads suffice, write-only to exactly the destination the task writes. A hostile execution then can't exceed the shape of its actual task.
Microsegmentation → per-execution netns + egress allowlist
Zero-trust networks deny by default and segment finely, so a compromised segment can't move laterally. The equivalent for code execution is per-execution network isolation with a deny-by-default egress allowlist. Each run gets its own network namespace and can reach only the specific endpoints its task requires — the object store, one database, the specific API — and nothing else. No arbitrary internet, no internal services, no other execution's resources.
This is the control that turns 'the code read some data' into 'the code can't send the data anywhere.' A hostile execution that legitimately reads its own task's input still can't POST it to an attacker's server, because the packet has nowhere to go. On a platform where each agent pre-allocates 16,384 /30 subnets, per-execution egress control is the default rather than a bottleneck — every guest already has its own network world to fence, so 'reach only what this task needs' is a local rule, not a special case carved out of a shared network.
Explicit per-request authorization → no reused warm state
Zero-trust authorizes every request explicitly rather than trusting a session to carry standing permission. For code execution, that means every run is a fresh, individually-authorized boundary — not a warm worker carrying whatever the last execution left behind. The warm-worker pattern is the code-execution version of an over-trusted session: state, credentials, and cached secrets from run N are sitting there when run N+1 lands, so run N+1 inherits authority it was never explicitly granted. A fresh boundary per execution means each run starts from a known state and holds exactly the (scoped, short-lived) authority you minted for it and nothing carried over.
Ephemerality as a control → teardown destroys state
Zero-trust prefers short-lived everything so a compromise has a short shelf life. Applied to execution, ephemerality is itself a security control: nothing persists between runs, so nothing can be inherited across them or exfiltrated over time. When an execution ends, you destroy the machine — the injected credential, any cached token, the code, and every byte it touched go with it. There's no worker to scrub, no /tmp to sweep, no cached session for the next run to inherit. Teardown isn't cleanup; it's the mechanism that keeps the leak surface between one execution and the next at zero. A `ttl_seconds` backstop ensures even a run you forget about reaps itself.
Auditability → attribute every execution
Zero-trust logs and attributes everything so anomalies trace to a source. For code execution, tag every run with the identifiers that matter — tenant id, job id, agent/session id — so a runaway execution, a denied egress attempt, or an anomalous read pattern traces back to exactly one execution and one caller. When something goes wrong (and assume-breach says it will), you want to answer 'which execution, whose request, what did it try to reach' in seconds, not reconstruct it from a shared worker's tangled logs.
The principles → controls mapping
- Assume breach — Control: a fresh Firecracker microVM per execution; a compromise is contained to one disposable guest with its own kernel.
- Least privilege — Control: no ambient host credentials; inject only per-run, scoped, short-lived creds; block the metadata endpoint; split read/write.
- Microsegmentation — Control: per-execution network namespace with a deny-by-default egress allowlist; reach only the task's endpoints.
- Explicit per-request authorization — Control: a fresh boundary per run; no warm worker carrying prior state, credentials, or cached secrets.
- Ephemerality / short-lived — Control: teardown destroys the machine and everything in it; a ttl backstop reaps forgotten runs.
- Auditability — Control: tag every execution with tenant/job/session ids; log egress attempts; attribute anomalies to one run.
What one zero-trust execution looks like
Here's several of these controls in a single execution: a hardware-isolated guest (assume breach), created with egress locked and no ambient creds (microsegmentation + least privilege), holding only a scoped per-run credential (least privilege), tagged for attribution (auditability), reaped by a ttl and destroyed at the end (ephemerality).
from pandastack import Sandbox
def execute_untrusted(code: str, run_id: str, caller_id: str,
scoped_creds: dict | None = None) -> dict:
"""One zero-trust execution: fresh hardware-isolated boundary, no ambient
creds, deny-by-default egress, scoped short-lived creds if needed,
attributed, and destroyed when done."""
with Sandbox.create(
template="code-interpreter",
ttl_seconds=120, # ephemerality backstop: reaps itself
metadata={"run_id": run_id, "caller": caller_id}, # auditability
# Create with egress allowlisted to ONLY this task's endpoints (or
# none) -- microsegmentation. Metadata endpoint blocked.
) as sbx:
# No ambient host creds live in this guest -- least privilege by
# default. If the task genuinely needs access, inject a SCOPED,
# SHORT-LIVED credential as a file, not a shared env.
if scoped_creds:
sbx.filesystem.write("/run/creds.json", _to_json(scoped_creds))
# The untrusted code -- an LLM's, a tenant's, a plugin's. It runs
# behind a hardware boundary, not next to your secrets.
sbx.filesystem.write("/run/task.py", code)
# timeout_seconds is the circuit breaker; it kills THIS vm only.
result = sbx.exec("python3 /run/task.py", timeout_seconds=90)
return {"run": run_id, "caller": caller_id,
"exit": result.exit_code, "out": result.stdout,
"err": result.stderr}
# VM gone here: the scoped cred, the code, any cached token, and every
# byte the run touched are destroyed with the machine. Nothing is
# inherited by the next execution -- explicit per-request authorization.And it's affordable, which is what keeps zero-trust from being aspirational. A hardware boundary per execution only works as a default if creating one is cheap: PandaStack creates by restoring a baked snapshot, so the restore step is around 49ms, an end-to-end create is p50 179ms and p99 about 203ms, and a true cold boot happens only on the first spawn of a template (around 3s). Fork a warmed parent per execution in 400–750ms same-host (1.2–3.5s cross-host) when you want each run to start from a known-good state. Zero-trust for code that stays fast enough to be the default.
Putting it together
You already apply zero-trust to your network and your users; apply it to your code execution, because 'code you didn't write' is exactly the untrusted actor the model was built for. The principles translate directly: assume breach becomes a hardware-isolated microVM per execution; least privilege becomes no ambient credentials plus scoped, short-lived injection and a blocked metadata endpoint; microsegmentation becomes a per-execution network namespace with a deny-by-default egress allowlist; explicit authorization becomes a fresh boundary per run instead of a warm worker; ephemerality becomes teardown that destroys everything; auditability becomes per-execution attribution. Build that and a compromised execution — the LLM's rm-rf, the tenant's exfiltration attempt, the plugin's crypto miner — gets a disposable machine with no credentials, no egress, and a short fuse. Which is the whole point of never trusting the code in the first place.
Frequently asked questions
What does 'zero-trust' mean for running untrusted code specifically?
It means applying the same principles you'd apply to an untrusted network actor — never trust, always verify; assume breach; least privilege; explicit per-request authorization; microsegmentation; short-lived credentials; full auditability — to the code itself. Concretely: don't trust the execution environment to stay uncompromised (assume breach), don't give the code ambient standing access (least privilege), don't let it reach anything its task doesn't require (microsegmentation), don't let one run inherit another's state (explicit authorization), don't let anything persist between runs (ephemerality), and attribute every execution (auditability). The architecture that falls out is a fresh, hardware-isolated, credential-starved, disposable boundary per execution.
Why does 'assume breach' rule out shared-kernel containers for untrusted code?
Because assume-breach means designing for the case where the execution environment is compromised. If that environment is a container sharing the host kernel, then a kernel local-privilege-escalation or a container escape triggered by the untrusted code crosses the shared boundary — so 'assume the container is breached' logically has to mean 'assume the host is breached,' and almost nobody is actually designing for that. A Firecracker microVM per execution gives the code its own guest kernel under hardware virtualization, so assuming the guest is breached means assuming one disposable VM is breached, not the host and not the next execution. Hardware isolation per run is what makes assume-breach a survivable stance rather than a hollow claim.
How does least privilege work if the code needs credentials to do its job?
You give it the minimum, scoped and short-lived, and nothing by default. The execution environment starts with no ambient host credentials — no cloud role, no mounted secrets, no inherited env keys — so there's nothing for hostile code to read. When a run genuinely needs access, you mint a per-run credential scoped to exactly the resource it needs (read-only where reads suffice, write-only to one destination), valid for minutes and expiring around the execution's timeout, and inject it as a file a specific process reads rather than into a shared environment. You also block the guest's route to the cloud metadata endpoint, which kills the SSRF-to-host-role escalation path. The code gets exactly its task's authority and no standing power.
What role does destroying the environment play in a zero-trust execution model?
Ephemerality is itself a control, mapping to zero-trust's preference for short-lived everything. When an execution ends you destroy the whole machine, so the injected credential, any cached token, the code, and every byte it touched are gone — there's no warm worker to scrub and no cached session for the next run to inherit. That keeps the leak surface between one execution and the next at zero and enforces explicit-per-request-authorization, because each run necessarily starts from a fresh boundary holding only what you just granted it. A ttl backstop ensures even a forgotten or wedged run reaps itself, so a compromise has a short shelf life by construction rather than by hoping cleanup ran.
Doesn't a fresh isolated environment per execution make things too slow or expensive?
Only if creating the boundary is slow, which is the problem managed microVM platforms solve. PandaStack creates by restoring a baked snapshot of an already-booted machine rather than cold-booting, so the restore step is around 49ms and an end-to-end create is p50 179ms (p99 about 203ms); a true cold boot happens only on the first spawn of a template (around 3s). You can also fork a warmed parent per execution in 400–750ms same-host so each run starts from a known-good state. Because create is that cheap, a hardware-isolated boundary per execution can be the default rather than a heavyweight exception — which is exactly what zero-trust for code execution requires to be practical.
49ms p50 cold start. Fork, snapshot, and scale to zero.