Running AI Agents on Kubernetes Without Handing Them the Cluster: Agent Sandbox + gVisor, Step by Step
> ## TL;DR > > - An AI agent is not a normal workload. It executes code that was generated seconds ago and never reviewed, it takes instructions from content it reads (web pages, tickets, documents — prompt injection is a design condition, not an edge case), and it holds real credentials: LLM API keys, tool tokens, sometimes cloud access. Your isolation has to assume the code inside is hostile. > - This is already a production question. The CNCF and SlashData State of Cloud Native Development Q3 2026 report (released 10 November 2026) found that 49% of developers integrating AI into their services already have agentic systems in production — and KubeCon + CloudNativeCon North America 2026 ran a dedicated AI Inference + Agentic track for the first time. > - The stack in this guide: a dedicated namespace with quotas → an agent identity with *zero* Kubernetes permissions → gVisor kernel isolation via RuntimeClass → the Kubernetes Agent Sandbox CRD (a SIG Apps project) for lifecycle, templates and warm pools → default-deny networking with named egress holes → short-lived, scoped credentials → audit on everything. > - One honesty note up front: Agent Sandbox is young — its APIs are at v1beta1 and the current release is v1.0.10. Treat the CRD as the lifecycle convenience it is. The load-bearing security layers are the ones underneath it: namespace, RBAC, RuntimeClass, NetworkPolicy. Those work with or without the CRD.
Every platform team has had some version of the same meeting this year. A product team wants to ship an agent: it reads tickets, writes code, calls internal tools, maybe opens pull requests. It needs to *run* somewhere, it needs credentials to be useful, and it will execute — by design — whatever the model decides to execute. The question lands on the platform team: where does this thing live?
The default answer, "just deploy it like a service," quietly assumes the code in the pod is trusted because a human wrote and reviewed it. For an agent, that assumption is false on day one. This guide builds the other answer: a sandbox stack on Kubernetes, layer by layer, with working manifests. The pattern is the same one you would use for any untrusted workload — CI runners executing strangers' code are the closest classical analogue — tuned for what makes agents different.
Why an agent is a different threat model
Four properties separate an agent runtime from an ordinary microservice:
- It runs unreviewed code. The whole point of a coding or tool-using agent is that it generates commands, scripts and tool calls at runtime. There is no code review gate between the model and the shell.
- Its instructions arrive over untrusted channels. An agent that reads a web page, a support ticket or a repository README is reading text an attacker may control. A crafted document can try to redirect the agent — read this file, call that tool, send this data somewhere. You cannot patch this at the model layer with confidence, so you bound what a redirected agent can reach.
- It is credentialed by design. An agent with no keys can do nothing. An agent with an LLM API key, a Git token and a database password is useful — and is a standing, machine-speed confused deputy holding all of them.
- It is long-lived and stateful. Agent sessions run for minutes to hours, accumulate files and context, and often persist a workspace. That is a different shape from both stateless services and batch jobs, and it is exactly the shape Kubernetes has historically been awkward at — one reason the Sandbox abstraction exists.
Put together, the design question changes. For a normal workload you ask, "what if this pod is compromised?" For an agent you ask, "this pod will eventually do something it should not — what can it reach when it does?" Every step below answers that question at a different layer. If you are newer to cluster hardening in general, our Kubernetes security basics guide covers the foundations this builds on.
The isolation stack at a glance
| Layer | Control in this guide | What it contains when the agent misbehaves |
|---|---|---|
| Kernel | gVisor (runsc) via RuntimeClass | A container escape reaches a user-space kernel, not the host kernel |
| Identity | Dedicated ServiceAccount, token automount off, zero RBAC | A stolen pod token buys nothing at the API server |
| Namespace | Dedicated namespace, Pod Security Standards restricted, quotas | Blast radius and resource abuse stay inside one fence |
| Lifecycle | Agent Sandbox CRD: templates, warm pools, claim TTLs | No forgotten, immortal agent pods; sessions expire by default |
| Network | Default-deny NetworkPolicy + named egress allows | Exfiltration and lateral movement have nowhere to go |
| Secrets | Scoped, short-lived, per-session credentials | A leaked credential is narrow and expires quickly |
| Visibility | API audit logs, pod logs shipped out, alerts | Misuse is visible after the fact — and during it |
No single layer is the security story. The story is that an attacker (or a confused model) has to defeat all of them in sequence, and each one is independently testable.
Step 1 — A namespace that assumes the worst
Agents get their own namespace. Not a shared tools namespace, not the namespace of the product they serve — their own, so that quotas, policies and network rules apply to agents and nothing else.
apiVersion: v1
kind: Namespace
metadata:
name: agents
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: agents-quota
namespace: agents
spec:
hard:
pods: "20"
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
requests.ephemeral-storage: 10Gi
limits.ephemeral-storage: 20Gi
count/persistentvolumeclaims: "20"
---
apiVersion: v1
kind: LimitRange
metadata:
name: agents-defaults
namespace: agents
spec:
limits:
- type: Container
default:
cpu: "1"
memory: 1Gi
ephemeral-storage: 2Gi
defaultRequest:
cpu: 250m
memory: 512MiThree decisions in this manifest are worth more than the rest:
- Pod Security Standards at
restrictedmeans the API server itself rejects pods that ask for privilege escalation, host namespaces, or root-by-default. You get a baseline even from manifests you did not write — including ones a controller generates. - The quota caps the fleet, not just the pod. An agent stuck in a reasoning loop is a cost and availability incident before it is a security one. Twenty pods and 8 CPU of limits is a starting point for a pilot, sized so a runaway can hurt the
agentsnamespace and nothing else. (Our resource requests and limits guide covers the sizing discipline in depth.) - Ephemeral storage is quota'd. Agents write files — checkouts, build artifacts, downloads. Unbounded ephemeral storage is how one session fills a node disk and evicts its neighbours.
One step further, on clusters that can afford it: taint a dedicated node pool for agent workloads and give the sandbox template the matching toleration. Kernel isolation (Step 3) reduces what a hostile pod can do to a node; physical separation removes the shared fate entirely. For a first deployment, the namespace fence plus gVisor is a defensible start; note the node pool as the scale-up move.
Step 2 — Identity: the agent gets zero Kubernetes permissions
This is the step teams most often get backwards, so be deliberate. There are two identities in this design, and only one of them ever touches the API server:
- The orchestrator — your service that receives a request, creates a
SandboxClaim, and hands back a session. It needs a small, explicit Role. - The agent runtime — the pod that runs generated code. It needs no Kubernetes permissions at all, and its ServiceAccount token should not even be mounted.
apiVersion: v1
kind: ServiceAccount
metadata:
name: agent-runtime
namespace: agents
automountServiceAccountToken: false
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: agent-orchestrator
namespace: agents
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: agent-orchestrator
namespace: agents
rules:
- apiGroups: ["extensions.agents.x-k8s.io"]
resources: ["sandboxclaims"]
verbs: ["create", "get", "list", "watch", "delete"]
- apiGroups: ["agents.x-k8s.io"]
resources: ["sandboxes"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: agent-orchestrator
namespace: agents
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: agent-orchestrator
subjects:
- kind: ServiceAccount
name: agent-orchestrator
namespace: agentsRead the asymmetry: the orchestrator can create claims and read sandboxes *in this namespace only* — no secrets, no pods/exec, no other namespaces. The agent runtime's ServiceAccount exists so the pod has a named identity, but with automountServiceAccountToken: false and no RoleBindings, code inside the sandbox that goes looking for the usual /var/run/secrets/kubernetes.io/serviceaccount/token finds nothing to steal. If your agent framework insists it needs to "call Kubernetes tools," that is the moment to stop and redesign: give the agent an application-level API with the three operations it actually needs, mediated by your orchestrator — never raw cluster credentials. Our Kubernetes RBAC guide covers the audit pattern for keeping Roles this small in practice.
Step 3 — Kernel isolation: gVisor via RuntimeClass
Namespaces and RBAC govern the API plane. They do nothing about the kernel plane: a container under the standard runc runtime shares the host's kernel, so a kernel exploit or an overly broad syscall path is a node compromise. An agent executing generated code is precisely the workload that should not share a kernel with your other tenants.
gVisor inserts a user-space kernel (the Sentry) between the container and the host. The container's syscalls are intercepted and serviced by gVisor, which makes a much smaller, carefully chosen set of calls to the host on the workload's behalf. A bug in the workload's code now has to escape a second, purpose-built boundary before it sees the real kernel.
On self-managed nodes with containerd, register the runsc runtime (the shim is containerd-shim-runsc-v1; the runtime type is io.containerd.runsc.v1):
# /etc/containerd/config.toml (excerpt) — then restart containerd
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runsc]
runtime_type = "io.containerd.runsc.v1"Then expose it to the scheduler as a RuntimeClass — the handler name must match the runtime name you registered in containerd:
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: gvisor
handler: runscOn GKE, you do not edit containerd configs: GKE Sandbox is the managed form of the same idea — a node pool where pods declaring runtimeClassName: gvisor run under gVisor. Either way, from the pod spec's point of view the contract is identical, which is why the template in Step 4 carries a single line: runtimeClassName: gvisor.
Verify with a throwaway pod before you build anything on top — apply a test pod with the runtime class, confirm it reaches Running, and check that kubectl get pod shows the runtimeClassName you set. A pod stuck in ContainerCreating with runtime errors is a node-side installation problem, and it is much cheaper to meet it now than after the templates land.
gVisor or Kata? They are the two realistic choices, and the decision is mostly about your nodes:
| gVisor | Kata Containers | |
|---|---|---|
| Isolation model | User-space kernel intercepts syscalls | Each pod runs in a lightweight VM with its own guest kernel |
| Node requirement | Runs anywhere containerd runs | Needs KVM — bare metal or nested virtualisation on cloud nodes |
| Overhead profile | Syscall- and I/O-heavy workloads pay a tax | VM memory footprint and cold-start cost per pod |
| Compatibility | Some syscalls are unimplemented; exotic workloads can notice | Near-native Linux inside the guest |
| Pick it when | General agent/code-execution sandboxing on ordinary nodes | You already run KVM-capable nodes and want VM-grade separation |
Agent runtimes — shells, interpreters, file tools, HTTP calls — are a good gVisor fit. If your agents will run heavy builds or unusual low-level tooling, benchmark both against your real workload before standardising; the RuntimeClass abstraction means the choice stays a one-line change per template.
Step 4 — Agent Sandbox: lifecycle as a CRD
With the fence (Step 1), the identities (Step 2) and the kernel boundary (Step 3) in place, you still need to manage the sandboxes themselves: create one per session, keep it warm enough to start fast, give it a stable identity and workspace, and — most importantly — make it *die* on schedule. That lifecycle is what the Kubernetes Agent Sandbox project provides.
Agent Sandbox is a SIG Apps project that adds a Sandbox custom resource: a single, stateful pod with a stable hostname, optional persistent storage, and lifecycle management (creation, scheduled deletion, pause and resume) — the "lightweight, single-container VM" shape that agent sessions actually have, instead of forcing them into Deployments or one-member StatefulSets.
Be precise about what it is not: the project describes itself as a sandbox *orchestrator*. It deliberately delegates low-level isolation to secure runtimes like gVisor or Kata, by creating pods configured with the RuntimeClass you choose. The CRD gives you lifecycle and consistency; the security still comes from Steps 1–3 and 5–6. Designs that treat "we installed the Sandbox CRD" as "we sandboxed the agent" have it exactly backwards.
Install it pinned — never from latest — and verify the CRDs and controller afterwards:
export VERSION="v1.0.10"
kubectl apply -f https://github.com/kubernetes-sigs/agent-sandbox/releases/download/${VERSION}/sandbox-with-extensions.yaml
kubectl get crd sandboxes.agents.x-k8s.io
kubectl get deploy agent-sandbox-controller -n agent-sandbox-system(v1.0.10 is the current release as of this writing, November 2026; the project asks for Kubernetes 1.25 or later, with 1.29+ recommended so CRD CEL validation rules are fully enforced.) The extensions manifest adds the three resources that make the project practical: SandboxTemplate, SandboxWarmPool and SandboxClaim.
A minimal Sandbox shows the shape — one pod template, nothing more:
apiVersion: agents.x-k8s.io/v1beta1
kind: Sandbox
metadata:
name: agent-session-demo
namespace: agents
spec:
podTemplate:
spec:
runtimeClassName: gvisor
restartPolicy: Never
containers:
- name: agent
# Stand-in: use your own agent runtime image and entrypoint
image: python:3.12-slim
command: ["sleep", "infinity"]In production you would rarely create bare Sandbox objects by hand. You define one hardened SandboxTemplate — owned by the platform team, reviewed like code — and let everything else instantiate from it. Here is the centrepiece of this guide; every field earns its place:
apiVersion: extensions.agents.x-k8s.io/v1beta1
kind: SandboxTemplate
metadata:
name: agent-sandbox-template
namespace: agents
spec:
# The controller manages a NetworkPolicy for sandboxes from this template.
networkPolicyManagement: Managed
networkPolicy:
egress:
# DNS only — name resolution is not general network access
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# TLS out — tightened further in Step 5
- ports:
- protocol: TCP
port: 443
volumeClaimTemplates:
- metadata:
name: workspace
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 5Gi
podTemplate:
spec:
serviceAccountName: agent-runtime
automountServiceAccountToken: false
runtimeClassName: gvisor
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: agent
# Stand-in: your agent runtime image belongs here
image: python:3.12-slim
command: ["sleep", "infinity"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
ephemeral-storage: 2Gi
env:
- name: LLM_API_KEY
valueFrom:
secretKeyRef:
name: llm-credentials
key: api-key
# Fail closed: no key, no sandbox
optional: false
volumeMounts:
- name: workspace
mountPath: /workspace
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir:
sizeLimit: 512MiDetails that matter:
- The template is the security policy. Runtime class, non-root, dropped capabilities, read-only root filesystem, token automount off, per-container limits — all fixed here, once, by the platform team. Application teams get sandboxes; they do not get to renegotiate the isolation line by line. That separation is the entire point of the template/claim split.
- Managed network policy means the controller maintains a NetworkPolicy for sandboxes created from this template, with the DNS-and-443 egress shape above as the starting point. Step 5 hardens networking at the namespace level as well — belt and braces, deliberately.
- The workspace volume is per-sandbox. Each sandbox gets its own PVC from the template, so one session's files are never another session's files, and storage dies with the sandbox.
optional: falseon the secret. If the LLM credentials Secret is missing, the pod refuses to start. Fail closed, find out in staging — not a runtime that silently runs unauthenticated, and not a fallback to some shared default key.
Claims and warm pools are how sessions actually start. A SandboxWarmPool keeps a small number of template-conformant sandboxes pre-created; a SandboxClaim checks one out:
apiVersion: extensions.agents.x-k8s.io/v1beta1
kind: SandboxWarmPool
metadata:
name: agent-warmpool
namespace: agents
spec:
replicas: 2
sandboxTemplateRef:
name: agent-sandbox-template
---
apiVersion: extensions.agents.x-k8s.io/v1beta1
kind: SandboxClaim
metadata:
name: session-0001
namespace: agents
spec:
warmPoolRef:
name: agent-warmpool
lifecycle:
# Reap the claim (and its sandbox) 10 minutes after the session finishes
ttlSecondsAfterFinished: 600
shutdownPolicy: DeleteTwo operational rules for pools. First, a pool is a trust boundary: never serve two teams — and certainly never two tenants — from the same warm pool, because pool members are interchangeable by design. One pool per template per trust level. Second, a claimed sandbox is spent: it serves exactly one session, and when the claim ends the sandbox is deleted, not wiped and returned. The pool tops itself back up to spec.replicas with fresh sandboxes — that refill behaviour is what makes warm starts safe to offer. And the claim's lifecycle fields give every session a hard expiry even if your orchestrator crashes mid-cleanup: expiry is enforced by the claim controller, not by your code remembering to tidy up.
Step 5 — Network: default-deny, then named holes
Network policy is where "sandbox" stops being a metaphor. The template's managed policy is a start; the namespace gets its own explicit posture so the rules survive template edits and apply to anything else that ever lands in agents:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: agents
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: agent-egress
namespace: agents
spec:
podSelector: {}
policyTypes:
- Egress
egress:
# DNS to the cluster resolver — nothing else in kube-system
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
# TLS to the outside world — deliberately nothing on port 80,
# which keeps the cloud metadata endpoint (169.254.169.254)
# unreachable even before the rules below are added
- ports:
- protocol: TCP
port: 443Three points of craft here. First, ingress stays closed: there is no rule allowing anything *into* the sandboxes, because agents should be reached through your orchestrator (or the project's sandbox router), never directly. Second, note what is absent — port 80 egress. The cloud instance metadata service lives on a link-local address over plain HTTP, and it is the single highest-value target inside any pod. Default-deny plus a 443-only allow means the metadata endpoint is unreachable by construction. Third, be honest about the limit of the 443 rule: NetworkPolicy works on IPs and ports, not names, so "443 anywhere" still reaches anything that listens on 443, including services you did not intend. It is the pragmatic minimum, not the finished state.
The finished state is a named destination list. If your CNI supports FQDN policy (Cilium is the common case), express the LLM egress as the exact host it should be:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: agent-egress-llm
namespace: agents
spec:
endpointSelector: {}
egress:
- toFQDNs:
- matchName: api.example-llm-provider.com
toPorts:
- ports:
- port: "443"
protocol: TCP
- toEndpoints:
- matchLabels:
k8s:io.kubernetes.pod.namespace: kube-system
k8s-app: kube-dns
toPorts:
- ports:
- port: "53"
protocol: UDPSwap in your provider's real API hostname. The alternative shape — used when the CNI cannot do FQDNs — is an egress proxy: deny all direct egress, allow exactly one proxy service, and let the proxy enforce the allowlist (and hold the real provider credentials, which Step 6 picks up). Either pattern beats "443 to the world" by a wide margin. Our Kubernetes network policies guide walks through policy composition and the usual debugging traps if you are building these for the first time.
One more warning that experience earns the hard way: NetworkPolicy only constrains traffic if your CNI enforces it. Confirm enforcement on a test pod (try to reach a disallowed destination and watch it fail) before you claim the control exists. An unenforced policy is a comment, not a boundary.
Step 6 — Secrets and credentials: short-lived, scoped, or not at all
Every layer so far assumed the agent might go wrong. Credentials decide how much that costs. The rules, in descending order of preference:
- The sandbox holds nothing it does not need for this session. No kubeconfigs, no cloud CLI credential files, no shared "team" tokens baked into the image. If a credential is in the sandbox and the session is manipulated, the credential is gone — design so that "gone" is a small event.
- Scope what it must hold. The LLM key in the template is a dedicated key for the agent platform — not the organisation's primary provider account — with a spend limit set at the provider. A leaked agent key should be a billing alert, not an incident. Keep provider keys and rotation in a namespace-scoped Secret, managed like any other workload secret (our ConfigMaps and Secrets guide covers the handling patterns).
- Prefer per-session, execution-scoped credentials. The strongest pattern: the sandbox holds no standing secret at all. Your orchestrator (or an egress proxy) holds the real credential, and the agent receives a token minted for one session — or even one tool execution — that is revoked when the run ends. The Agent Sandbox project's own examples include exactly this pattern: execution-scoped credentials delivered to a reused sandbox, with egress pinned to a credential proxy and the token revoked when the process exits. Even if you do not adopt that example wholesale, steal its shape.
- Cloud access goes through an identity, never a file. If the agent legitimately needs cloud APIs, attach a per-namespace workload identity whose role is three verbs on one resource — the same least-privilege discipline as on-prem RBAC, applied to the cloud side (see our AWS IAM least privilege guide). Long-lived access keys inside a sandbox are a finding, full stop.
The rare exception: something in the sandbox genuinely must authenticate *to the Kubernetes API* — for example, a tool gateway you run in-pod. Do not fall back to the mounted default token. Issue a projected, audience-bound token with a one-hour life instead:
# Pod-spec fragment — add to the template's containers/volumeMounts and volumes
volumeMounts:
- name: tool-token
mountPath: /var/run/secrets/tokens
readOnly: true
# ...and under spec.volumes:
volumes:
- name: tool-token
projected:
sources:
- serviceAccountToken:
audience: tool-gateway
expirationSeconds: 3600
path: tokenThe audience means the token is only acceptable to your tool gateway; the expiry means a stolen copy is a one-hour problem; the projection means it is minted per pod, not stored in a Secret that outlives the session.
Step 7 — Audit and observability: assume you will need the tape
When an agent does something expensive or strange — and the interesting incidents are usually "expensive" before they are "malicious" — the first questions are forensic: which session, created by whom, ran what, talked to whom, for how long? Wire the answers up before the first real session:
- API audit logs. Sandbox and claim creation/deletion are the control-plane events that matter. Your audit trail should answer "who or what created this
SandboxClaim, from which identity, at what time" without archaeology. An orchestrator identity (Step 2) makes this attribution clean: every sandbox traces to one claim, every claim to one orchestrator request. - Pod logs, shipped out of the sandbox. Collect sandbox stdout/stderr into your central logging with the sandbox and claim names as labels. Logs that live only inside the sandbox are destroyed with it — by your own TTL, at the worst possible moment. Agent frameworks should also emit structured tool-call logs at the application layer (which tool, what arguments, what came back); ship those through the orchestrator, not from inside the sandbox, so a manipulated agent cannot quietly stop reporting itself.
- Lifecycle and resource signals. Alert on: sandboxes living far longer than the session norm, claim TTLs being hit repeatedly (sessions dying mid-work), ResourceQuota saturation in the
agentsnamespace (the signature of a runaway loop), and — where your CNI reports them — NetworkPolicy drop spikes from sandbox pods, which are often the first visible sign of an agent trying to reach something it should not. - Events and controller logs. The agent-sandbox controller's events on claims and pools (adoption failures, refill problems) are your early warning that the warm pool is empty and sessions are cold-starting — an availability problem your users will report as "the agent product is slow."
Our Kubernetes observability guide covers the logging/metrics plumbing; the agent-specific work is the labelling discipline above, so sandbox telemetry joins up with session telemetry from your application.
What I would not run yet
A guide that only sells the stack is not trustworthy, so the reservations, plainly:
- Do not let the CRD's youth become your risk. Agent Sandbox's APIs are
v1beta1and the project is releasing fast (v1.0.10 at this writing). Pin the version, read the release notes before upgrading — recent releases have changed router NetworkPolicy behaviour in ways that need action — and keep your GitOps manifests as the source of truth so a rollback is a revert. The pattern in this guide does not depend on the CRD; if the project stalled tomorrow, plain Pods plus Steps 1–3 and 5–7 would still stand. - Do not give an agent write access to anything production-shaped in v1 of your rollout. Read-only tools first. The sandbox limits blast radius; it does not make a bad write un-happen. Irreversible actions (sends, deletes, merges, payments) belong behind human approval at the application layer — no kernel feature gives you that.
- Do not share warm pools across trust boundaries, and do not return used sandboxes to a pool. Ever. (Step 4 — it bears repeating because it is the mistake the abstraction makes easiest.)
- Do not skip the egress work because "443 is fine." The metadata endpoint, internal services on 443, and simple data exfiltration to an attacker-controlled host all fit through a careless 443 rule. The proxy or FQDN allowlist is not polish; it is the control.
- Do not run agent workloads on your general-purpose nodes "temporarily" without at least the RuntimeClass layer. Temporary has a way of becoming the architecture.
The final hardening checklist
Namespace and resources
- [ ] Dedicated
agentsnamespace; Pod Security Standards enforced atrestricted - [ ] ResourceQuota caps pods, CPU, memory and ephemeral storage for the whole namespace
- [ ] LimitRange defaults set so no container runs unbounded
- [ ] (Scale-up) Agent workloads tainted onto a dedicated node pool
Identity
- [ ] Agent runtime ServiceAccount with
automountServiceAccountToken: falseand no RoleBindings - [ ] Orchestrator identity scoped to claims/sandboxes in the agents namespace only
- [ ] No kubeconfigs, cloud key files or shared tokens inside the sandbox image
Kernel and pod hardening
- [ ] gVisor (or Kata) registered on nodes; RuntimeClass created and verified with a test pod
- [ ] Template sets
runtimeClassName,runAsNonRoot, seccompRuntimeDefault - [ ] Containers:
allowPrivilegeEscalation: false,readOnlyRootFilesystem: true, all capabilities dropped - [ ] Per-container requests and limits, including ephemeral storage
Lifecycle
- [ ] Agent Sandbox installed pinned (v1.0.10 at this writing), CRDs and controller verified
- [ ] One hardened SandboxTemplate per workload type, owned and reviewed by the platform team
- [ ] Warm pools are per-trust-boundary; claimed sandboxes are never reused
- [ ] Claims carry lifecycle expiry (
ttlSecondsAfterFinished+shutdownPolicy: Delete)
Network
- [ ] Default-deny NetworkPolicy (ingress and egress) on the namespace
- [ ] Egress allows DNS and named destinations only; no port-80 egress (metadata stays unreachable)
- [ ] FQDN allowlist or egress proxy in place — not bare "443 to the world"
- [ ] Policy enforcement verified with a live test, not assumed from the YAML
Secrets and audit
- [ ] LLM credentials are dedicated, spend-capped keys in a namespace-scoped Secret; sandbox fails closed without them
- [ ] Per-session/execution-scoped tokens where the design allows; projected audience-bound tokens for any in-pod API need
- [ ] API audit covers sandbox/claim create and delete; pod and tool-call logs ship outside the sandbox
- [ ] Alerts on sandbox lifetime anomalies, quota saturation and policy drops
FAQ
Is a regular container not sandboxed enough? A regular container under runc shares the host kernel — namespaces and cgroups isolate resources and visibility, but every syscall lands on the same kernel your other tenants use. For trusted, reviewed code that trade-off is normal. For code generated at runtime from untrusted instructions, the extra kernel boundary (gVisor or Kata) is the layer that turns a container escape into a much harder problem.
Does Agent Sandbox replace gVisor? No — they solve different problems and compose. Agent Sandbox is an orchestrator: it manages the lifecycle of singleton, stateful sandbox pods (templates, warm pools, claims, expiry). gVisor and Kata are runtimes: they provide the actual isolation boundary, and the sandbox's pod template selects one via runtimeClassName. Using the CRD without a secure runtime gives you tidy lifecycle management around an ordinary container.
Can I build this without the Agent Sandbox CRD? Yes. Plain Pods (or one-member StatefulSets) with the same namespace, RBAC, RuntimeClass, NetworkPolicy and secret patterns give you the same security posture. What you give up is the ergonomic lifecycle: templates as a single reviewed source, warm pools for fast session starts, claims with enforced expiry. Plenty of teams start with plain Pods and adopt the CRD when session volume makes the lifecycle worth a controller.
gVisor or Kata Containers — which should I choose? Start with gVisor unless you know otherwise: it runs on ordinary nodes and fits interpreter/shell/HTTP-shaped agent workloads well. Choose Kata when your nodes support KVM (or nested virtualisation) and you want VM-grade separation, or when your workloads trip over gVisor's unimplemented syscalls. Because the choice lives in one runtimeClassName line per template, you can run both and assign per workload type.
Is Agent Sandbox production-ready? Honestly: it is a young, fast-moving SIG project — APIs at v1beta1, releases landing roughly monthly, and occasional behaviour changes that need operator action (recent router NetworkPolicy changes are a good example). The safe way to adopt it is the way this guide is structured: let the CRD own lifecycle convenience, let the boring, stable primitives underneath own the security, pin your version, and test upgrades like you would any controller. Teams that need a fully settled API today can run the same architecture on plain Pods and adopt the CRD later without redesigning anything.
---
*Sources: kubernetes-sigs/agent-sandbox — project README, releases (v1.0.10) and official examples (Sandbox, SandboxTemplate, SandboxClaim, SandboxWarmPool manifests), checked 12 Nov 2026; gVisor documentation, containerd configuration (runsc runtime type io.containerd.runsc.v1); CNCF and SlashData, "State of Cloud Native Development Q3 2026" — CNCF announcement, 10 Nov 2026 (49% of developers integrating AI reporting agentic systems in production); Kubernetes documentation on RuntimeClass and Pod Security Standards. Project status, versions and statistics checked against these sources on 12 Nov 2026.*
