Open until something selects it
Kubernetes' agent sandbox ships a secure default that lets an untrusted agent reach every address on the public internet and forbids it from resolving the name of the service beside it. The posture is defensible. What holds it is one NetworkPolicy object, and every route to stronger isolation requires replacing or deleting that object.
The argumentAn agent sandbox's containment is not a property of the sandbox but one additive NetworkPolicy keyed to a hash of its template's name, so every route to stronger isolation requires replacing or deleting the object that provides it.
Create an agent sandbox on Kubernetes today, using the project's own defaults and nothing else, and here is the box you get. The agent inside it can open a socket to any public address on the internet, on any port. It cannot reach the database in its own namespace, the VPC it is running in, or the cloud metadata server. It cannot resolve a cluster service name at all: under the secure default the controller rewrites the pod's DNS configuration to point at Google's and Cloudflare's public resolvers, provided the pod spec left dnsPolicy unset. The comment in the controller says why. Blocking CoreDNS exists "to prevent agents from probing for service discovery and leaking internal service names."
Read that posture twice, because it inverts twenty-five years of practice. The enterprise default was always the other way round: internal traffic is fine, the internet is the danger, and egress goes through something that logs it. Here the internet is the permitted zone and the cluster is the forbidden one. The threat model has been flipped on purpose, and the purpose is sound — the thing we are worried about is the agent pivoting into infrastructure it was never given, and the metadata server is the first place a compromised workload goes. If you are going to pick one direction to default-deny, this is the defensible pick.
What I want to look at is not the direction. It is where the decision lives.
It lives in a single Kubernetes NetworkPolicy object, generated by the SandboxTemplate controller, predictably named <template-name>-network-policy, in the template's namespace. One per template. The project's documentation is explicit that this is a deliberate optimisation: "Instead of generating hundreds of individual policies per SandboxClaim, the controller generates and maintains a single, highly optimized Kubernetes NetworkPolicy for each SandboxTemplate." It also states the consequence plainly, under a heading called What It Doesn't Do: "No Per-Claim Policies." The selector is the label agents.x-k8s.io/sandbox-template-ref-hash, whose value is a hash of the template's name. So the unit of containment is the shape of the workload. The unit of risk is the run.
Everything below is read out of the project's own repository: the API types, the template controller, the runtime daemon's guide, two policy examples and the commits of the last fortnight. There is no independent reporting to weigh against it, and the network available for this piece reached almost nothing else.
That mismatch would be a quibble if the policy were a wall. It is not a wall, and the project says so in a sentence I have not stopped thinking about since I read it. In the guide for running reinforcement-learning fleets: "A Kubernetes NetworkPolicy is additive: until one selects a pod, that pod is open."
That is the whole architecture of the thing. A sandbox is not isolated because it is a sandbox. It is isolated for as long as some object in the same namespace still happens to select it. Containment is a relation, maintained across three places with three different lifecycles — the template, the policy the controller derives from it, and the CNI's bookkeeping about pod labels — and the sandbox itself carries no state that says whether the relation currently holds.
The reason this is urgent rather than theoretical is what runs inside the pod. The project has standardised a runtime daemon, sandboxd, under its KEP-539.2: a gRPC ProcessService on port 9090 that executes commands and streams their output, and a REST filesystem API on 8080 that reads, writes and deletes files. Both listeners bind 0.0.0.0 by default. The user guide is candid about what keeps strangers off them: "Containment is provided by pod isolation and NetworkPolicy, not loopback binding." Until the twelfth of this month there was no second line of defence at all. The commit that added one, merged on 12 October and not yet in a tagged release, opens with the plainest threat statement in the repository: "sandboxd serves process execution and filesystem access with no client authentication; any peer that can reach :8080/:9090 runs commands as the daemon." The new --auth-token-sha256 flag fixes that, for operators who set it. It is off unless you ask.
So for most deployments the NetworkPolicy is not defence in depth around a remote-execution API. It is the authentication.
Now follow an operator who takes isolation seriously. Agent Sandbox describes itself as a sandbox orchestrator and delegates the low-level boundary to a secure runtime — gVisor or Kata Containers, via RuntimeClass. That is the right division of labour, and it is the configuration a careful team will choose for untrusted agent code. A note merged on 9 October records what it costs: port-forward, the default connectivity mode in the SDKs, does not work on Kata or gVisor, because the workload runs behind its own kernel and nothing is listening in the pod's host-side network namespace. The fix is to dial the sandbox from inside the cluster. Which requires an ingress rule admitting your client, because the default admits only the sandbox router. Which means supplying spec.networkPolicy — and, as the documentation states, "When you provide a custom networkPolicy block within your template, it completely replaces the Secure Default rules."
There is the compounding. Choosing the strongest available kernel boundary is also choosing to hand-write the network boundary, including every carve-out you were previously getting for free: the RFC1918 blocks, the link-local range that contains the metadata server, the IPv6 unique-local range. Miss one line and the micro-VM is sitting on a flat route to the VPC. The project's own worked example of a custom policy opens TCP 8080 from the monitoring namespace, for Prometheus to scrape a sidecar. On an image running sandboxd, 8080 is the unauthenticated filesystem API.
Scale makes it worse, and the project has now documented that too. The RL fleet SDK creates a template per task image, so a namespace running a large task set holds hundreds of templates, each with its own policy, created and deleted as pools warm and retire. On GKE Dataplane V2 — which the example describes as built on Cilium, a claim I could not corroborate from Google's or Cilium's own documentation, both of which this environment blocked — every distinct pod label set gets its own security identity, and sandbox pods carry per-pod labels, so identities churn as fast as pods. The remedy documented on 12 October is to stop generating policies: set the template mode networkPolicyManagement: Unmanaged, which has been in the API since the v1.0.5 release of 30 September and tells the controller to create no policy at all, then hand-apply one namespace-wide policy selecting app=agent-sandbox-rl. What is new is the fleet SDK field that sets the mode, and the example that recommends it. It works. It also moves the only thing isolating the fleet out of the controller's reconcile loop and into a kubectl apply that somebody has to remember, before the first warm. The guard rail for forgetting is a warning logged by a client-side preflight() call — which also fires when the fleet's identity cannot list policies at all. Nothing in the cluster holds the fact that a sandbox is unisolated.
Two further details are worth an architect's attention. The replacement policy admits clients by pod label, and the README is honest about what that is worth: "The label is a trust boundary only if untrusted users cannot create pods in the namespace, because anyone who can create a pod can set it." And the Cilium remediation for identity churn is a cluster-wide regular expression excluding label keys from identity computation, where a single pattern written without its ! silently switches Cilium into allowlist mode — at which point policies that select on an excluded key stop selecting their pods, in every namespace, and "those pods lose the isolation it gave them."
The strongest objection to all of this is that I am asking the wrong object to do the job. A NetworkPolicy is an L3/L4 control; it cannot express "only github.com", it cannot read a request, and it was never going to contain a language model. The real boundary for untrusted code is the kernel one, and the real control over what an agent may reach belongs at a gateway that understands names and verbs. Additive semantics are not an agent problem either — they are how Kubernetes networking has worked since 2017, and every platform team already knows that an unselected pod is an open pod. On this reading the project is doing well: it ships a restrictive default where most tools ship none, it names its own gaps under What It Doesn't Do, it points at ClusterNetworkPolicy for FQDN guardrails, and it added client authentication to sandboxd the moment someone wrote the threat down.
I agree with nearly all of that, and it does not dissolve the problem. The kernel boundary bounds what code can do to its host; it says nothing about what the code can reach, which is the question that decides blast radius. The gateway is the right answer and is not what the documentation currently names as containment. And the defect is narrower and more fixable than "NetworkPolicy is L3": it is that isolation here has no observable state. A Sandbox reports its pod IPs, its node, its phase. It does not report which policy selects it, whether that policy was derived or hand-written, or what the derivation dropped. "Is this sandbox isolated?" is answerable today only by listing policies, hashing a template name, and replaying label arithmetic against a CNI's identity rules. A control you can audit only by re-deriving it is a control that will be wrong somewhere in your estate right now. The check takes an afternoon: for every namespace that runs sandboxes, list the policies, confirm one actually selects the pods you have, and read what its egress rules left out.
In September I argued that an agent fleet's real blast radius is everything one instance can write that another can read. This is the same question turned on the boundary itself. For thirty years "sandbox" named a property of a process — something true about it wherever it ran. We have quietly redefined it as an agreement between objects, kept by a controller, a label hash and an operator's memory, and the agent fleet is simply the first workload that changes shape fast enough to show us the seam.
So the question to ask of your agent platform is not what its sandboxes are isolated from. It is which object currently remembers to isolate them — and what happens the day that object is deleted for being too numerous.
What this is argued from
Reporting and primary material the piece rests on, dated at the time of writing. The interpretation is mine; the facts belong to these.
- SandboxTemplate API types, v1beta1
- SandboxTemplate controller, buildDefaultNetworkPolicySpec
- sandboxd User Guide
- Agent Sandbox Network Policy Management
- One NetworkPolicy per namespace for agent-sandbox-rl fleets on GKE
- Cluster-wide egress control for sandboxes with the Network Policy API
- feat(sandboxd), require a bearer token via --auth-token-sha256 (#1895)
- docs, port-forward does not work with Kata and gVisor (#1864)
- Agent Sandbox README
Editorials on this site are written to be argued with. If you think the reading is wrong, it probably is in some particular way, and that is the useful part.