Skip to main content

gVisor Worker Image — Hardened Sandbox Spec

⚠️ STATUS: SPEC ONLY — NOT YET VALIDATED ON HARDWARE

This document is the specification for the AoA cloud worker image and its gVisor (runsc) sandbox profile. It records the intended hardened flag set and egress-firewall policy so that Task 2 (buildDockerRunArgs hardening) and Task 6 (resolveGvisorSandboxTarget defaults) have a single source of truth to encode against. Nothing in this document has been executed or verified on a live host. The live runsc validation spike and the egress-firewall proof are a PENDING Gate-B / deployment step that has not been run. Specifically:
  • Checkpoint A (runsc present without KVM/nested virt) — UNRUN
  • Checkpoint B (pinned claude + codex CLIs run under runsc) — UNRUN
  • Checkpoint C (one-shot run survives the full hardened flag set; memory/pids limits do not starve Node) — UNRUN
  • Checkpoint D (metadata + RFC1918 + control-plane unreachable while the provider API is reachable on the filtered bridge) — UNRUN
Do not treat any flag, version, or reachability claim below as confirmed. This is the design contract to validate, not a validation report. The go/no-go gate for the cloud pool remains open until checkpoints A–D pass on a real Hetzner worker and this banner is replaced with a dated validation record.

Purpose

A pooled multi-tenant run must execute untrusted agent code without giving it a route to the host, to peer tenants, to cloud metadata, or to the control plane. The self-hosted single-tenant path is unchanged and does not use any of this: the hardening is opt-in and default-OFF at every layer. Two independent layers provide the isolation:
  1. Process/kernel isolation — the runsc (gVisor) runtime plus a hardened docker run flag set (this doc, “Hardened flag set” below). Encoded as opt-in defaults by resolveGvisorSandboxTarget and emitted by buildDockerRunArgs in packages/adapter-utils/src/execution-target.ts.
  2. Egress isolation — a host firewall on the container bridge (this doc, “Egress firewall policy” below). This lives in the worker image, NOT in the app layer. The app layer does not and must not filter egress.
--add-host host.docker.internal:host-gateway — historically emitted unconditionally by buildDockerRunArgs — is a route to the control-plane host (an SSRF vector). Task 2 makes it conditional: it is emitted only when the callback bridge is actually running AND the target explicitly opts in via allowHostGateway. That removes ONE route; it is not egress filtering and does not by itself make a bridge-networked pool safe.

Base image contents

The golden worker image (aoa/agent-base:latest) must contain:
  • Node.js (LTS matching the server runtime).
  • claude CLI pinned to 2.1.x and codex CLI pinned to 0.145.x — the versions the codebase pins in server/src/services/cli-auth-topology.ts (cited from source; not hardware-verified here).
  • A non-root agent user (uid:gid = 1000:1000) whose $HOME is /home/agent. The CLI config directory must live on a writable tmpfs/bind, never on the read-only root filesystem.
  • runsc installed and registered with dockerd as a named runtime (see “runsc install” below).

runsc install (to be run + version-recorded during the spike — UNRUN)

runsc version: to be recorded from runsc --version during the spike — currently UNKNOWN / UNRUN.

Hardened flag set (the contract Task 2 encodes)

This is the exact flag set that a pooled gVisor run is intended to launch with. Task 2’s buildDockerRunArgs emits each of these behind the opt-in runtime/isolation/allowHostGateway profile; Task 6’s resolveGvisorSandboxTarget supplies these as the gVisor defaults.
Flag-by-flag rationale: --network none is the safe default. It is sufficient for runs that do not need the provider API (e.g. a fully offline task). Runs that need the provider API must switch to a filtered bridge (below) — and only then.

Checkpoint-C write test (UNRUN)


Egress firewall policy (M7 — HARD worker-image deliverable, PENDING)

--network none alone does not solve egress: a pooled run that needs the provider API forces the operator onto bridge, and plain bridge filters nothing — the container can reach 169.254.169.254 (cloud metadata), all of RFC1918, and the control-plane host. Making --add-host conditional (Task 2) removes ONE route; it is NOT egress filtering. Therefore the worker image MUST apply a host firewall on the container bridge (nftables/iptables on the DOCKER-USER chain, or an explicit egress proxy), applied at boot, that:
  • DENY RFC1918 (10/8, 172.16/12, 192.168/16), link-local 169.254.0.0/16 (incl. 169.254.169.254 metadata), and the control-plane CIDR(s).
  • ALLOW only the provider API hosts + package registries from the allowlist (default-deny otherwise, enforced via the egress proxy’s allowlist).

Allowlist (provider API + package registries)

The egress proxy allowlist should include (at minimum) the provider API hosts for the pinned CLIs plus the package registries the image needs at run time. The concrete host list is finalized during the spike; the policy is default-deny with an explicit allow for:
  • Anthropic API host(s) used by the claude CLI.
  • OpenAI API host(s) used by the codex CLI.
  • npm registry (and any other registry the base image resolves against).

Checkpoint-D proof (UNRUN)

Re-run the checkpoint-C container on the filtered bridge and assert:
  • claude -p "say hi" succeeds (provider egress ALLOWED), AND
  • curl -sS --max-time 3 http://169.254.169.254/ times out / is refused (metadata DENIED).
GO/NO-GO checkpoint D: metadata + RFC1918 + control-plane are unreachable while the provider API is reachable. Until this firewall ships and this proof passes, a cloud pool on bridge is unshippable — this spec makes no egress-protection claim for bridge without it.

Residual (state it plainly)

  • --network none is the safe default.
  • bridge is permitted for a pooled run ONLY with the worker-image egress firewall applied and checkpoint D passing.
  • The app layer does NOT filter egress — the worker image does.
  • Making --add-host conditional (Task 2) closes the host-gateway SSRF route; it is not a substitute for the egress firewall.

Fallback

If checkpoint B fails for an adapter (a pinned CLI dies on an unimplemented gVisor syscall), route that adapter to E2B on the pool and record the specific syscall gap here. This does not block the other adapters.