releases.shpreview

Kubernetes Agent Sandbox guide for secure coding agent isolation

July 21, 2026Pulumi BlogView original ↗
From the original release noteView original ↗

When you use a coding agent, it can seem like there’s a trade-off between autonomy and permissions. If you approve every command, it’s safe but slow. Let it do whatever it likes and it works more autonomously, but as the nx supply-chain attack showed, that can go badly.

The fix is to give the agent a sandbox: a box it’s allowed to wreck, with limited permissions and scoped network access. The only files are the checkout you handed it, the only credentials are the task’s own, and trashing the machine just means a disposable pod gets garbage-collected early. Pulumi Neo works this way, and if you want to scale that pattern up inside your own organization, the Kubernetes project Agent Sandbox is a great path to building your own. This post is what it is and how to deploy it on GKE with Pulumi.

What is Agent Sandbox?

Agent Sandbox is a Kubernetes SIGs project that gives AI agents isolated, disposable environments as Kubernetes resources: a Sandbox custom resource, backed by gVisor or Kata Containers for kernel-level isolation.

You could build Agent Sandbox yourself. You’d need gVisor support, a userspace kernel that sits between the agent’s code and your host. With that in place, you could approximate a sandbox for every agent by stringing together a StatefulSet of size one, a headless Service, and a PersistentVolumeClaim, plus some lifecycle machinery to keep warm pools of nodes around. Agent Sandbox wraps all of that up as a CRD, so you can run a Kubernetes cluster where each sandbox is a disposable, kernel-isolated environment a coding agent runs in.

<span class="line"><span class="cl"><span class="nt">apiVersion</span><span class="p">:</span><span class="w"> </span><span class="l">agents.x-k8s.io/v1beta1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">kind</span><span class="p">:</span><span class="w"> </span><span class="l">Sandbox</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">demo-sandbox</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">podTemplate</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">spec</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">runtimeClassName</span><span class="p">:</span><span class="w"> </span><span class="l">gvisor</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">containers</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">agent</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu:24.04 </span><span class="w"> </span><span class="c"># swap in your coding-agent image</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"> </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"sleep"</span><span class="p">,</span><span class="w"> </span><span class="s2">"infinity"</span><span class="p">]</span><span class="w">
</span></span></span>

The `Sandbox` CRD, the whole idea in one manifest.

There are two common patterns for using it. In the first, every coding agent session in the organization maps to its own pod with a persistent volume, basically Claude Code running across a whole fleet of pods, each one an individual user’s session.

In the second, you’re building your own Neo or Devin: a productized agent harness that needs to spin up execution environments. The harness lives outside the sandboxes and hands each task a disposable box, and Agent Sandbox is what spins those boxes up and down, suspending and resuming them.

Why not just run agents in Docker?

So why do we need gVisor or Kata Containers at all? The agent already runs inside a container. Isn’t that the box?

Not really. A container isn’t much of a security barrier. Every container on a host shares one kernel, and the Linux kernel exposes a huge surface area, 450+ syscalls, to every one of them. CVE-2019-5736 is the canonical example: a malicious container tricks runc into overwriting the host’s own runc binary, and after that every docker run on the host runs attacker code as root. Put a prompt-injectable AI agent in that container, and the risk is obvious.

gVisor, the runtime underneath Agent Sandbox’s default path, puts a userspace kernel written in Go between your agent and the host, which limits the possible security surface area. A kernel bug the agent can reach becomes, mostly, a crash in a userspace process rather than a root shell on your node. That is what makes an Agent Sandbox secure.

What is GKE Sandbox?

GKE Sandbox is GKE’s gVisor feature: a RuntimeClass named gvisor that runs a pod under the userspace kernel described above. It predates Agent Sandbox by years and is a per-pod isolation primitive, not an agent tool. Agent Sandbox is the lifecycle layer that sits on top of it: the CRD’s runtime selection points down at gVisor (or Kata) for the actual kernel isolation. They compose. On GKE, Agent Sandbox with runtimeClassName: gvisor is literally using GKE Sandbox underneath.

Google engineers in Kubernetes SIG Apps maintain Agent Sandbox, Janet Kuo and Justin Santa Barbara (of kOps) among them, and it launched at KubeCon NA in November 2025. It’s also one layer of a broader cloud-native agent stack taking shape: agent-sandbox for isolation, kagenti (IBM) for identity, kagent (Solo.io, a CNCF Sandbox project) for agent logic, agent-substrate for density. As Solo.io’s Lin Sun puts it, “Sandboxing your agents is necessary, but not sufficient.”

The rung above gVisor: hardware microVMs

gVisor filters syscalls in a userspace kernel. The heavier option gives the workload its own guest kernel behind hardware virtualization (KVM), so an escape has to cross a CPU-enforced boundary. Firecracker, the VMM AWS built for Lambda (and the isolation layer under Bedrock AgentCore), keeps that boundary cheap: roughly 50,000 lines of Rust against QEMU’s ~1.4 million lines of C. Google’s kvmCTF pays $250,000 for a KVM escape, which is how the market rates that boundary. Agent Sandbox reaches this rung through Kata Containers via the same runtime selection, and the tradeoff isn’t linear: Kata can beat gVisor on I/O-heavy work, because its guest runs a real kernel servicing syscalls natively.

The one-second problem

Isolation is the hard part, but it isn’t the only one. If you’re in scenario 2, using Agent Sandbox pods as the backing instances for your own agent harness, startup time is a challenge too. People abandon chat sessions, and when they come back they expect the agent to pick up quickly, not wait on a machine to boot.

Booting a fresh Kubernetes pod costs about a second of overhead. That’s nothing for a rolling deployment, but enough that the maintainers say it “breaks the continuity” of an interaction. So Agent Sandbox avoids paying that cost twice: suspend a sandbox and its pod goes away while its workspace and identity persist, and warm pools keep pre-booted pods ready for new sandboxes to adopt. The managed GKE version goes further and restores from memory snapshots, the same snapshot-restore move E2B (~150ms) and Fly’s Sprites (~300ms) use to pull the effective cold start well under a second1.

Deploying it on GKE with Pulumi

If you already have a cluster and want to try Agent Sandbox, the kubectl apply from the project’s quickstart will probably get you going. But productionized use takes a bit more. Let’s walk through setting up a cluster with a sane access policy, named individuals, and a dedicated environment per user. The full program is at pulumi/examples/gcp-ts-agent-sandbox.

Move 1: the cluster. First we stand up a GKE cluster with a gVisor node pool. The one line that matters is the sandboxConfig, with sandboxType: "gvisor". As far as I’m aware, this can only be done on GKE:

<span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">gvisorPool</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">gcp</span><span class="p">.</span><span class="nx">container</span><span class="p">.</span><span class="nx">NodePool</span><span class="p">(</span><span class="s2">"gvisor-pool"</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">cluster</span>: <span class="kt">cluster.name</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">nodeConfig</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">machineType</span><span class="o">:</span> <span class="s2">"n2-standard-4"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">imageType</span><span class="o">:</span> <span class="s2">"COS_CONTAINERD"</span><span class="p">,</span> <span class="c1">// gVisor requires COS + containerd
</span></span></span><span class="line"><span class="cl"> <span class="nx">sandboxConfig</span><span class="o">:</span> <span class="p">{</span> <span class="nx">sandboxType</span><span class="o">:</span> <span class="s2">"gvisor"</span> <span class="p">},</span>
</span></span><span class="line"><span class="cl"> <span class="p">},</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span>

GKE installs the gvisor RuntimeClass, labels the nodes, and taints them so only pods that opt in land there.

That auto-install is the GKE convenience, not a requirement. Agent Sandbox itself is just CRDs and a controller, so on any other cluster you can install gVisor yourself (or Kata), register a RuntimeClass, and everything below works the same.

Move 2: the install, pinned. Next we install the controller and the CRDs, using the all-in-one sandbox-with-extensions.yaml manifest that shipped in last week’s v0.5.22:

<span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">base</span> <span class="o">=</span> <span class="sb">`https://github.com/kubernetes-sigs/agent-sandbox/releases/download/v0.5.2`</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">agentSandbox</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">k8s</span><span class="p">.</span><span class="nx">yaml</span><span class="p">.</span><span class="nx">v2</span><span class="p">.</span><span class="nx">ConfigGroup</span><span class="p">(</span><span class="s2">"agent-sandbox"</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">files</span><span class="o">:</span> <span class="p">[</span><span class="sb">`</span><span class="si">${</span><span class="nx">base</span><span class="si">}</span><span class="sb">/sandbox-with-extensions.yaml`</span><span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">});</span>
</span></span>

Pinning matters here: releases land every week or two, and the API group already graduated once (v1alpha to v1beta1).

Move 3: sandboxes are a loop, not a manifest. This is where Pulumi really shows its advantages. In practice you don’t create sandboxes by hand, because they’re per-user or per-task. So the demo reads a list and maps over it:

<span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">developers</span> <span class="o">=</span> <span class="nx">config</span><span class="p">.</span><span class="nx">requireObject</span><span class="p"><</span><span class="nt">Developer</span><span class="err">[]</span><span class="p">>(</span><span class="s2">"developers"</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// Who may open which box, derived from that same list.
</span></span></span><span class="line"><span class="cl"><span class="kr">const</span> <span class="nx">acl</span> <span class="o">=</span> <span class="nx">sandboxAcl</span><span class="p">(</span><span class="nx">developers</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kr">export</span> <span class="kr">const</span> <span class="nx">sandboxes</span> <span class="o">=</span> <span class="nx">developers</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">dev</span> <span class="o">=></span> <span class="k">new</span> <span class="nx">AgentSandbox</span><span class="p">(</span><span class="sb">`sbx-</span><span class="si">${</span><span class="nx">dev</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">owner</span>: <span class="kt">dev.name</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">credentials</span><span class="p">,</span> <span class="c1">// this owner's Claude Code creds, per box, not per cluster
</span></span></span><span class="line"><span class="cl"> <span class="nx">prompt</span>: <span class="kt">BOOT_PROMPT</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="c1">// ...
</span></span></span><span class="line"><span class="cl"><span class="p">},</span> <span class="p">{</span> <span class="nx">dependsOn</span><span class="o">:</span> <span class="p">[</span><span class="nx">agentSandbox</span><span class="p">,</span> <span class="nx">operator</span><span class="p">,</span> <span class="nx">acl</span><span class="p">]</span> <span class="p">}));</span>
</span></span>

That developers array is read at runtime, so it could just as easily be a GitHub team or whoever currently has a session open, none of which kubectl apply -k can loop over. (The developers list and the per-box credentials are stack config; the example’s README has the exact pulumi config set commands.)

Nothing in the sandbox is Claude-specific, either. The agent is just what the image installs: swap in Codex CLI, or point one of the Claude Code orchestration frameworks at it, and the isolation story doesn’t change.

Move 4: the egress policy. Create a Sandbox directly, as we do here, and no NetworkPolicy applies3: kernel isolation, but wide-open egress. (The SandboxTemplate extension manages a default policy for template-created sandboxes; a raw Sandbox gets nothing.) We set a policy that lets the agent reach api.anthropic.com and npm but not any private IPs:

<span class="line"><span class="cl"><span class="nx">egress</span><span class="o">:</span> <span class="p">[{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">to</span><span class="o">:</span> <span class="p">[{</span> <span class="nx">ipBlock</span><span class="o">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl"> <span class="nx">cidr</span><span class="o">:</span> <span class="s2">"0.0.0.0/0"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">except</span><span class="o">:</span> <span class="p">[</span><span class="s2">"10.0.0.0/8"</span><span class="p">,</span> <span class="s2">"172.16.0.0/12"</span><span class="p">,</span> <span class="s2">"192.168.0.0/16"</span><span class="p">,</span> <span class="s2">"169.254.169.254/32"</span><span class="p">],</span>
</span></span><span class="line"><span class="cl"> <span class="p">}}],</span>
</span></span><span class="line"><span class="cl"><span class="p">},</span> <span class="cm">/* + a DNS rule to kube-system:53 */</span><span class="p">]</span>
</span></span>

We also exclude 169.254.169.254 to keep the agent off the node metadata server.

Move 5: private access with Tailscale. The sandbox runs an IDE (code-server) on port 13337. Instead of a public load balancer with a password, we put each sandbox on a tailnet with the Tailscale Kubernetes operator. Three annotations on the Service do it:

<span class="line"><span class="cl"><span class="s2">"tailscale.com/expose"</span><span class="o">:</span> <span class="s2">"true"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="s2">"tailscale.com/hostname"</span><span class="o">:</span> <span class="nx">name</span><span class="p">,</span> <span class="c1">// sbx-adam
</span></span></span><span class="line"><span class="cl"><span class="s2">"tailscale.com/tags"</span><span class="o">:</span> <span class="sb">`tag:</span><span class="si">${</span><span class="nx">name</span><span class="si">}</span><span class="sb">`</span><span class="p">,</span> <span class="c1">// tag:sbx-adam
</span></span></span>

Move 6: lock down each sandbox. Finally, we generate the tailnet access policy from the same developer list that creates the boxes, so each person can reach only their own box:

<span class="line"><span class="cl"><span class="nx">acls</span>: <span class="kt">developers.map</span><span class="p">(</span><span class="nx">d</span> <span class="o">=></span> <span class="p">({</span>
</span></span><span class="line"><span class="cl"> <span class="nx">action</span><span class="o">:</span> <span class="s2">"accept"</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"> <span class="nx">src</span><span class="o">:</span> <span class="p">[</span><span class="nx">d</span><span class="p">.</span><span class="nx">email</span><span class="p">],</span> <span class="c1">// this person
</span></span></span><span class="line"><span class="cl"> <span class="nx">dst</span><span class="o">:</span> <span class="p">[</span><span class="sb">`tag:sbx-</span><span class="si">${</span><span class="nx">d</span><span class="p">.</span><span class="nx">name</span><span class="si">}</span><span class="sb">:13337`</span><span class="p">],</span> <span class="c1">// this box, this port. Nothing else.
</span></span></span><span class="line"><span class="cl"><span class="p">})),</span>
</span></span>

Because the boxes and the grants come from one array, a developer’s sandbox and their permission to open it can’t drift apart.

Move 7: run it. With all of that declared, pulumi up builds the whole environment in one command: cluster, gVisor pool, controller, per-developer sandboxes, egress policy, and private access. Get the stack’s URL and open it:

<span class="line"><span class="cl">$ pulumi stack output sandboxUrls
</span></span><span class="line"><span class="cl"><span class="o">[</span><span class="s2">"http://sbx-adam:13337"</span><span class="o">]</span>
</span></span>

Open it on any device signed into your tailnet and you’re in a full VS Code (code-server), running inside the sandbox pod, on the gVisor node pool, behind the egress policy set above.

Each box also boots with a task. The prompt in the demo asks the agent to figure out where it is: read /proc/version, decide whether it’s in a container or a VM, and write up the evidence in a REPORT.md. An agent doing forensics on its own jail is a decent smoke test that the isolation is real.

Here’s the finished result:

Recorded a little demo: Here's how every developer gets their own kernel-isolated dev box on Kubernetes: VS Code + Claude Code in a pod behind Tailscale with gVisor underneath for security isolation. write up coming

— Adam Gordon Bell (@adamgordonbell) July 20, 2026

Wrapping up

Agent Sandbox makes a kernel-isolated, disposable agent environment a first-class Kubernetes object, and Pulumi is how you stand one up as one program instead of a runbook.

The full program, everything in this post, deploy to teardown, is at pulumi/examples/gcp-ts-agent-sandbox. Clone it, point it at a GCP project, and you have your own dedicated agent cluster in about twelve minutes. If you’re new to Pulumi, get started here.

[GitHub repository: pulumi/examples

github.com/pulumi/examples

](https://github.com/pulumi/examples)


  1. Warm pools plus snapshot restore are how the managed version keeps this fast at scale: Google’s GKE Agent Sandbox launch cites 300 sandboxes per second at sub-second latency. See Bringing you Agent Sandbox on GKE and Agent Substrate. ↩︎

  2. The all-in-one manifest is new in v0.5.2 (July 17): earlier releases split the install into manifest.yaml plus extensions.yaml, and v0.5.2 renamed the core file — one more reason to pin the version you deploy. ↩︎

  3. Verified against the v0.5.2 release manifests: sandbox-with-extensions.yaml installs no NetworkPolicy objects, and networkPolicyManagement defaults to Managed only for SandboxTemplate-created sandboxes. A directly-created Sandbox gets neither, so restricting egress is left to you. See the v0.5.2 release. ↩︎

Fetched July 21, 2026