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 (buildDockerRunArgshardening) and Task 6 (resolveGvisorSandboxTargetdefaults) have a single source of truth to encode against. Nothing in this document has been executed or verified on a live host. The liverunscvalidation spike and the egress-firewall proof are a PENDING Gate-B / deployment step that has not been run. Specifically: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.
- Checkpoint A (runsc present without KVM/nested virt) — UNRUN
- Checkpoint B (pinned
claude+codexCLIs run underrunsc) — 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
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:- Process/kernel isolation — the
runsc(gVisor) runtime plus a hardeneddocker runflag set (this doc, “Hardened flag set” below). Encoded as opt-in defaults byresolveGvisorSandboxTargetand emitted bybuildDockerRunArgsinpackages/adapter-utils/src/execution-target.ts. - 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).
claudeCLI pinned to2.1.xandcodexCLI pinned to0.145.x— the versions the codebase pins inserver/src/services/cli-auth-topology.ts(cited from source; not hardware-verified here).- A non-root
agentuser (uid:gid = 1000:1000) whose$HOMEis/home/agent. The CLI config directory must live on a writable tmpfs/bind, never on the read-only root filesystem. runscinstalled and registered withdockerdas a named runtime (see “runsc install” below).
runsc install (to be run + version-recorded during the spike — UNRUN)
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’sbuildDockerRunArgs emits each of these behind the opt-in
runtime/isolation/allowHostGateway profile; Task 6’s
resolveGvisorSandboxTarget supplies these as the gVisor defaults.
--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-local169.254.0.0/16(incl.169.254.169.254metadata), 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
claudeCLI. - OpenAI API host(s) used by the
codexCLI. - npm registry (and any other registry the base image resolves against).
Checkpoint-D proof (UNRUN)
Re-run the checkpoint-C container on the filteredbridge and assert:
claude -p "say hi"succeeds (provider egress ALLOWED), ANDcurl -sS --max-time 3 http://169.254.169.254/times out / is refused (metadata DENIED).
bridge is unshippable — this spec makes no
egress-protection claim for bridge without it.
Residual (state it plainly)
--network noneis the safe default.bridgeis 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-hostconditional (Task 2) closes the host-gateway SSRF route; it is not a substitute for the egress firewall.