Don't Trust the Sandbox Claim — Verify It Against Your Own CNI
"It's sandboxed" is not a security sign-off. If you're the one responsible for approving multi-tenant agent code execution on your own infrastructure, the question isn't whether a vendor says network access is denied by default — it's exactly which mechanism enforces that, at which layer, what happens when one layer is misconfigured, and whether you can check each one yourself on the cluster you actually operate.
"It's sandboxed" isn't sufficient for a sign-off
Every code-execution sandbox claims isolation. The claim that actually matters to a security review is more specific: what kernel or platform primitive is doing the enforcing, at what layer, and what happens if one layer is misconfigured. boxxkite's network-dark-by-default posture doesn't rest on a single switch — it stacks three independent mechanisms that fail in different ways for different reasons, and being self-hostable means each one is something you can inspect and exercise directly, on your own cluster, instead of taking on faith.
This post walks all three, in the order an attacker would encounter them: the per-exec network namespace, the pod-level NetworkPolicy, and the sidecar's own shared-secret auth. For each, the useful thing to carry away isn't "it's secure" — it's the specific command that tells you whether that layer is actually live on your CNI, and the specific way each one can be silently defeated.
Layer one: a fresh network namespace per exec, by default
In boxxkite's Kubernetes runtime mode, each exec call is designed to run inside a fresh, empty Linux network namespace by default — meaning the executed process starts with no network interfaces configured at all, not even a loopback route to the rest of the pod's networking. There's no interface for it to send traffic through in the first place, independent of any firewall rule deciding whether that traffic would be allowed. This isn't a policy that gets consulted; it's the physical absence of a device.
Concretely, the sidecar builds the exec command in sidecar/sidecar_execution.py (build_k8s_exec_command). The generated command enters the sandbox's mount and PID namespaces via nsenter -m -p and drops to the unprivileged sandbox user — but it does notjoin the sandbox's network namespace. Instead the whole thing is wrapped in unshare -n, which creates a brand-new empty network namespace for the process:
# build_k8s_exec_command(sandbox_pid, command)
nsenter_cmd = [
"nsenter",
"-t", str(sandbox_pid),
"-m", "-p", # mount + PID namespaces only -- NOT --net
"--setuid", str(SANDBOX_UID), # drop to the unprivileged sandbox user
"--setgid", str(SANDBOX_GID),
"--", "sh", "-c", command,
]
# Default path (SANDBOX_EXEC_NETWORK_ISOLATION_ENABLED = "true"):
# unshare -n runs FIRST, wrapping nsenter, so the process lands in a
# fresh, empty network namespace -- no interfaces, not even loopback.
["unshare", "-n", *nsenter_cmd]One detail in that code is easy to miss and worth calling out, because it's the kind of thing that's only obvious in hindsight: the unshare -n has to run before nsenterenters the sandbox's mount namespace, not after. If the order were reversed, the process would already be inside the sandbox's filesystem view, where the sidecar's own unsharebinary isn't visible — the isolation step would fail to even start. The namespace-creation ordering is load-bearing, not incidental.
This whole layer is gated by a single config flag, SANDBOX_EXEC_NETWORK_ISOLATION_ENABLED, which defaults to true. That default matters for the review: if it's ever flipped off for a specific workflow — the git tools, for instance, need it off to reach a remote — this layer disappears for everyexec'd command in that pod, not just the one that needed it, and the NetworkPolicy below becomes the sole backstop. That's exactly why the second layer has to be independently restrictive, not merely redundant belt-and-suspenders.
The one scoped exception, and why it doesn't widen the pod
There is exactly one path that deliberately opts a process out of the fresh namespace: /process/start with an expose_portset, used for preview URLs (a dev server the sidecar needs to proxy to). That single background process stays in the pod's own shared network namespace so the sidecar can reach it on 127.0.0.1. It's worth being precise about the blast radius of that exception, because "one process skips the namespace" sounds alarming until you see what it does and doesn't touch:
The skip_network_isolation override affects only that one explicitly-requested process. Every other /exec and /process/start in the same pod still gets its own fresh, empty namespace. And critically, opting out of layer one does nottouch layer two: the exposed process is still governed by the pod's NetworkPolicy, and nothing outside the cluster can reach its port directly — only the sidecar's already-authenticated /previewsurface can. The exception is scoped to a named process, and it drops down exactly one layer (to pod-level isolation, the same boundary every container in the pod already has), not to "no isolation."
Two independent layers, not one
Layer two: a default-deny egress NetworkPolicy
Independent of the namespace, boxxkite's deployment manifests ship a default-deny NetworkPolicy (see deploy/network-policy.yaml) applied at the pod level. It sets both Ingress and Egresspolicy types and then allowlists only what's genuinely needed: ingress to the sidecar's :8080 from the API gateway and worker, and egress to DNS. This is the same kind of Kubernetes-native primitive a platform team already uses to restrict traffic between any two workloads — not a boxxkite-specific mechanism layered on unfamiliar infrastructure, just the standard tool, applied.
The metadata endpoint at 169.254.169.254— the classic target for exfiltrating a node's IAM credentials — is blocked here purely by omission: default-deny egress with nothing in the allowlist that names it. That's the correct design, but it means the protection is only as real as the rest of the allowlist staying narrow. Two failure modes are worth internalizing before you trust the file:
The selector has to match real pods. The policy's podSelector targets app: sandbox — the label that SandboxManager and WarmPoolManager actually set on live pods. An earlier revision of the manifest selected component: execution, a label nothing sets, so it silently matched zero pods and provided no protection at all — a NetworkPolicy that applies to nothing looks identical, in kubectl get netpol, to one that's working. Confirm the selector matches your live pods' labels, not just that the object exists.
The storage-egress rule is a placeholder, not a default.Real cloud object storage (S3, Azure Blob) doesn't publish small, stable CIDRs, so the manifest ships that egress rule as an explicit CHANGEME you must fill in for your backend. The one thing you must not do is fall back to ipBlock: 0.0.0.0/0:443— a previous version of the file did, and that's equivalent to no egress restriction at all: it also permits PyPI, npm, GitHub, and the metadata endpoint over any HTTPS path. If your CNI supports FQDN-based egress (Cilium's toFQDNs), that's a materially tighter fit for named storage or git hosts than any IP block.
The additive-policy trap
This one catches people who think they've tightened a policy when they've actually loosened it. NetworkPolicy objects with overlapping podSelectors are additive — their allow-rules are unioned, never intersected. Two consequences follow directly, and both are real footguns the manifests are structured to avoid:
First, the fully-open "permissive" egress mode lives in its own file, deploy/network-policy-permissive-optin.yaml, specifically so it can't be applied as a silent side effect. It used to be a second ----separated document inside network-policy.yaml, which meant the natural command — kubectl apply -f deploy/network-policy.yaml— quietly applied both, and the union of "deny-all" and "egress: [{}]" is wide-open egress, with no error from kubectl. Apply at most one egress policy per pod selector.
Second, the same math is why the per-session secrets-broker policy (BOXXKITE_SECRETS_NETWORK_POLICY_ENABLED) only provides real scoping when the base policy stays at its storage-only default. That mechanism provisions one dynamically-scoped NetworkPolicy per session, granting egress to exactly that session's granted secrets' resolved destination IPs and tearing it down at session end — but if the base file's egress were widened to a broad CIDR, the union would swallow the per-session narrowing whole. Tight-per-session OR wide-base is still wide.
Layer three: sidecar auth that doesn't depend on the network at all
The sidecar's HTTP API has no authentication of its own historically — it relied entirely on network isolation to stay unreachable. Because that assumption doesn't hold on every CNI, boxxkite adds a third layer that is independent of network topology entirely: every route except /health requires a per-pod shared-secret header (see src/boxxkite/sidecar_auth.py and SECURITY.md). The secret is generated per pod at creation time — never a static, repo-wide value — stored in a per-pod Kubernetes Secret and injected via secretKeyRef, not as a literal env value.
The reason this layer is genuinely independent, rather than more of the same: it holds even in the exact scenario where the other two fail. If the CNI silently doesn't enforce NetworkPolicy, or an over-broad egress rule also happens to expose the sidecar's ingress port (both containers in a pod share one set of NetworkPolicy rules), a caller that reaches the sidecar still can't drive it without the pod's secret.
| Layer | Mechanism | Gated by | How you verify it |
|---|---|---|---|
| Per-exec netns | Fresh empty network namespace via unshare -n | SANDBOX_EXEC_NETWORK_ISOLATION_ENABLED (default on) | Run a network call via an agent exec — it fails instantly, no interface |
| Pod NetworkPolicy | Default-deny ingress + egress, DNS-only allowlist | CNI enforcement + a correct podSelector | curl from the sidecar container (pod netns) times out |
| Sidecar auth | Per-pod shared-secret header on every route but /health | Always on; secret injected via secretKeyRef | A request without the header is rejected regardless of topology |
Why the distinction matters even if one layer were misconfigured
Defense-in-depth means neither layer is a single point of failure for the other. If a network namespace were somehow set up incorrectly and left with an interface present, the pod's NetworkPolicy would still deny the egress. If the NetworkPolicy were misapplied or the CNI didn't enforce it correctly, the exec call would still have no interface to route traffic through. And if both network layers failed at once, the sidecar's shared-secret requirement would still stand between an attacker and the control surface. A review that only checks one of these has checked a fraction of the boundary.
The verification step — and why it matters where you run it
The specific, well-known check worth running yourself: attempt to reach the cloud metadata endpoint most cloud providers expose at 169.254.169.254. On a correctly isolated pod, this should simply fail to connect — not time out ambiguously or partially succeed. But there's a subtlety that's easy to get wrong and that changes what a "pass" even means: where you run the check decides which layer you're actually testing.
Run it as an agent-visible exec — through bash_toolinside a session — and you're testing layer one. It fails because the process is in an empty namespace with no interface at all; the failure is immediate and total, and it tells you nothing about whether your NetworkPolicy works, because traffic never got far enough to be policed.
# Run from inside a boxxkite session (e.g. via bash_tool) -- this tests LAYER ONE.
# On a correctly isolated pod this should fail fast with no route/connection,
# not hang indefinitely -- an ambiguous hang is itself worth investigating.
curl --max-time 3 -sS http://169.254.169.254/latest/meta-data/ ; echo "exit: $?"To test layer two in isolation, run the check from the sidecarcontainer instead. The sidecar lives in the pod's own network namespace — it has real interfaces — so if the request reaches the wire and is stopped, that's your CNI actually enforcing the NetworkPolicy. This is the check that tells you whether the "blocked by omission" of the metadata endpoint is real on your cluster:
# Run against the pod's OWN network namespace -- this tests LAYER TWO (the CNI).
# A timeout / connection-refused means the NetworkPolicy is enforced.
# ANY HTTP status back means your CNI is NOT enforcing it and IMDS is reachable.
kubectl exec <sandbox-pod> -c sidecar -- \
curl -m 3 -s -o /dev/null -w '%{http_code}\n' http://169.254.169.254/Both checks are things you can only meaningfully run if you operate the cluster the pod is running on. A hosted vendor telling you their sandbox passes these tests is a claim; running them against your own deployment, from both vantage points, is a verification — and only the second vantage point actually exercises your CNI.
What being self-hostable buys a security team
The difference isn't that self-hosted infrastructure is inherently safer than a hosted product — it's that a security sign-off on self-hosted infrastructure can be based on tests you ran yourself, against manifests you can read, rather than a vendor's attestation you have no way to independently check. You can read the exact unshare -n ordering, confirm the podSelector matches your live labels, and watch the metadata endpoint fail from both the namespace and the pod. None of that is a promise; all of it is an observation.