Post

Your ops agent asked for a run_script tool. Fair: doing percentiles "in its head" is how you get confidently wrong postmortem math. Then the harness ran it via subprocess.run() inside the agent's own pod. The pod with the LLM key, MCP tokens, a SA token, metadata reach. Fix 🧵

The script is untrusted input. The model needn't be malicious: it reads logs, PR comments and alert annotations, and any of those can carry an injection that ends in "now run this snippet". Deny it 4 things: credentials, network, the host kernel, unbounded time and resources.

Layer 1: an EMPTY namespace. Anyone who can create pods in a namespace can mount any Secret and use any ServiceAccount in it. So agent-sandbox holds nothing: no Secrets, no privileged SAs, no other workloads. Label it pod-security enforce=restricted.

A ResourceQuota that doubles as a rate limit: pods: 5 (max concurrent runs) count/jobs.batch: 30 With ttlSecondsAfterFinished: 300, a looping agent burns 30 executions, then gets 403 from the API server until old Jobs expire. Backpressure from the platform, not a prompt.

Layer 2: gVisor RuntimeClass. runsc intercepts syscalls in a user-space kernel; the script never touches the node's kernel. GKE: node pool --sandbox type=gvisor. EKS/self-managed: register runsc in containerd. Pin it to the tainted pool or pods stick in FailedCreatePodSandBox.

Prove you're really sandboxed: run dmesg in a test pod. Under gVisor it prints gVisor's own boot messages instead of the node's kernel log. Can't run gVisor? Kata Containers behind the same RuntimeClass gives a microVM boundary; the rest of the design is unchanged.

Layer 3: the Job template, non-obvious bits: - backoffLimit: 0 (never retry model-written code; the model reads the traceback) - automountServiceAccountToken: false - enableServiceLinks: false - runAsUser 65534, read-only rootfs, drop ALL caps - emptyDir /tmp 64Mi

Two timeouts, on purpose. Inner: timeout -k 2 60 python3 -c <script>. Exits 124 first, pod ends normally, partial output survives. Outer: activeDeadlineSeconds. When it fires the Job controller deletes the pod and its logs vanish. The backstop; also covers image pull.

The script travels as a -c argument, not a ConfigMap. No extra object to clean up, no extra RBAC verb, and the exact code that ran is preserved in the Job spec for the audit trail. Linux caps a single argument at ~128 KiB, so the executor rejects scripts over 64 KB.

Layer 4: default-deny NetworkPolicy. podSelector {}, policyTypes Ingress + Egress, zero egress rules. No metadata endpoint, no internal services, no DNS. Resist "just allow kube-dns": a resolver is a data channel. On a CNI that ignores NetworkPolicy it protects nothing.

Make the shape mandatory. A refactor that drops runtimeClassName silently downgrades every run to a plain container. ValidatingAdmissionPolicy (GA in 1.30) on the namespace: - runtimeClassName == 'gvisor' - no SA token automount - emptyDir volumes only PSA covers the rest.

The image is boring and pinned by digest: Python, jq, coreutils. No curl, no kubectl, no cloud CLIs. No network means no pip install at runtime, so bake numpy + pyyaml in. The tool description says NO network, NO creds, embed data inline. The model stops burning turns trying.

Executor ServiceAccount (held by the harness, never the model): a Role in agent-sandbox only. create+get jobs, list pods, get pods/log. No delete: the TTL controller cleans up, keeping finished Jobs inspectable 5 min and the quota rate-limit honest. Output capped at 16 KB.

Don't trust the YAML. Run hostile scripts through the tool in CI: - read SA token path = FileNotFoundError - urlopen 169.254.169.254 = timeout - gethostbyname = fails - while True = exit 124 - bytearray balloon = OOMKilled - 1 GB into /tmp = Evicted - no runtimeClassName = 403

0 reposts0 likes

Two honest limits. The /tmp fill is caught by kubelet eviction on an interval, so the write overshoots briefly. Fork bombs aren't in the pod spec: set podPidsLimit on the sandbox node pool. And stdout is STILL untrusted. A sandbox contains execution, not the text it returns.