From a66cae3856868ab5f056082c9663cee8e2374ea7 Mon Sep 17 00:00:00 2001 From: Bret Mogilefsky Date: Mon, 21 Sep 2026 22:05:23 +0000 Subject: [PATCH 01/12] docs(adr): add ADR-0030 host-authoritative sandbox configuration Records the principle that acq must not rely on guest-generated or guest-tamperable state to configure the sandbox: because the in-sandbox agent has passwordless sudo, guest root-ownership is not a trust boundary against a prompt-injected agent. The only enforceable boundary is host<->guest. acq-trusted inputs (data AND code) must be either host-resident and mounted read-only into the guest, or held on the host and injected at exec time -- never read back from the guest and treated as authoritative. Introduces a per-sandbox host config directory mounted read-only, and trusted startup execution invoked from that read-only mount (three options considered; option 1 -- mount kit code read-only and invoke from the :ro path -- selected). Documents the staged migration of the existing /var/lib/acq markers (agent, workspace, ssh-auth-sock, kit-env, and the presence gates) off guest-writable paths. First consumer: patterns ADR 0003 (neutral model-provider discovery). Includes two Mermaid sequence diagrams (GitHub/Host/Guest boxes) contrasting the rejected guest-write path with the chosen host-materialized read-only path. Co-authored-by: OpenCode [claude_4_8_opus] --- .../0030-host-authoritative-sandbox-config.md | 308 ++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 docs/adr/0030-host-authoritative-sandbox-config.md diff --git a/docs/adr/0030-host-authoritative-sandbox-config.md b/docs/adr/0030-host-authoritative-sandbox-config.md new file mode 100644 index 00000000..5c7c90a6 --- /dev/null +++ b/docs/adr/0030-host-authoritative-sandbox-config.md @@ -0,0 +1,308 @@ +--- +title: "Host-Authoritative Sandbox Configuration (no trust in guest-generated state)" +status: accepted +date: 2026-09-21 +decision_makers: ["Bret Mogilefsky"] +category: architecture +nist_controls: ["AC-3", "AC-6", "CM-5", "CM-6", "SC-7", "SI-7", "SI-10", "SI-17"] +impact_level: moderate +ato_relevance: no +risk_treatment: mitigate +supersedes: [] +--- + +# ADR-0030: Host-Authoritative Sandbox Configuration (no trust in guest-generated state) + +## Context and Problem Statement + +acq deliberately gives the in-sandbox `agent` user **passwordless sudo** (see +`AGENTS.md`, and the msb agent-user provisioning in +`acq.backends/msb.sh`: `/etc/sudoers.d/90-acq-agent … NOPASSWD:ALL`). This is an +intentional convenience: the sandbox — not the guest OS user boundary — is the +security boundary (ADR-0005, ADR-0011). A consequence follows that has not been +stated as a principle until now: + +> **Inside the guest, root-ownership is not a trust boundary against the agent.** +> A prompt-injected agent can `sudo` to root and rewrite any file in the guest, +> including anything under a root-owned path such as `/var/lib/acq/`. + +acq currently violates the safe form of this in two ways: + +1. **acq reads guest-written state back and acts on it.** Four content-bearing + markers under the guest path `/var/lib/acq/` are written by acq (via + `msb exec -u 0`), then read back on a later session/restart and used to + *configure the sandbox*: + + | Marker | Written (host fn) | Read back → effect | + |---|---|---| + | `agent` | `acq_backend_provision` | selects which binary the re-attach launches | + | `workspace` | `acq_backend_provision` | sets the session working directory (`-w`) | + | `ssh-auth-sock` | ssh-agent bridge starter | sets `-e SSH_AUTH_SOCK=…` for git/ssh signing | + | `kit-env` | `_acq_msb_apply_kit_dir` | replays kit `environment[]` as `-e NAME=value` on every session | + + Each read is charset/token-hardened against *shell injection* (e.g. + `_acq_msb_safe_agent_token`, the `kit-env` awk name re-validation), so a + tampered marker cannot break out of an `sh -c`. But the hardening does not stop + a sudo-capable guest from setting a **valid-but-malicious value**: redirect + `SSH_AUTH_SOCK`, change the launched agent to another allowed token, change the + cwd, or inject arbitrary environment variables into the agent process. The + guest can influence its own host-driven reconfiguration. + +2. **acq re-runs guest-resident kit code across a restart.** On msb, kit + `files[]` (including startup scripts and config-merge programs such as the + provider kits' `merge-*-config.mjs`) are copied *into the guest* at provision + and then `chown`ed to the agent. acq's restart heal + (`acq_backend_ensure_kits_applied`) re-runs the `startup` phase by invoking + those **guest-resident** files. A sudo-capable agent can overwrite a startup + script or normalizer *between* provision and the next restart; the heal then + executes the tampered code. This is the general form of the + "provider-facts file is root-owned, therefore safe" error flagged in the + `agentic-coding-patterns` PR #423 review — root-ownership is not the barrier + it appears to be. + +A concrete instance motivating this ADR: the neutral model-provider discovery +design (patterns ADR 0003) originally had each provider kit **write** a facts +file to a root-owned guest path in its own `startup` command, and claimed that +path was "not agent-writable." Under passwordless sudo it is. A planted or +tampered facts file would let the guest steer model routing (and, absent the +host-side validation this ADR requires, an SSRF/credential-misuse surface). + +### Presence-only gates (a lower-severity variant of the same problem) + +Four other `/var/lib/acq/` files are `touch`/`test -f` idempotency gates +(`install-`, `agent-installed-`, `agent-user-ready`, `oci-ready`). +Their *content* is never consumed, but a guest can pre-`touch` them to +**suppress** a provisioning step (skip an install, skip the agent-user/sudoers +setup, skip OCI setup). That is a downgrade/DoS on setup rather than injection, +but it is the same root cause: acq relies on guest-resident state to make a +provisioning decision. + +## Decision Drivers + +- **The only enforceable boundary is host↔guest.** A value acq holds on the host + (in memory, or in a host file the guest cannot write) is beyond the reach of a + sudo-capable guest; a value living in the guest is not. +- **Least privilege / integrity** (AC-6, SI-7): the sandbox's configuration must + not be forgeable by the workload it sandboxes. +- **Preserve the sandbox-is-the-boundary model** (ADR-0005/0011): this ADR does + **not** remove passwordless sudo. It removes acq's *reliance on* guest-writable + state for configuration, which is orthogonal. +- **No code execution on the host** (the deciding factor that rejected patterns + ADR 0003's "Option 2"): acq may read *static data* from a pinned kit directory + on the host, but must not *execute* kit-shipped code on the host, next to real + credentials. +- **Backend parity and Windows path forms**: any new mount must work on both + msb (`--volume host:guest:ro`) and sbx (`:ro` positional), and respect the + host/guest path-form split (ADR-0029). +- **Fail-soft preservation**: the current marker writes are best-effort + (`|| true`); the migration must not turn a transient state-dir hiccup into a + hard provision abort where it was previously survivable. + +## The principle (normative) + +> **acq MUST NOT rely on guest-generated or guest-tamperable state to configure +> the sandbox.** Every input acq trusts to make a configuration or +> lifecycle decision — data *and* code — MUST be one of: +> +> - **(H1) Host-resident and mounted read-only into the guest** (the VMM/mount +> layer enforces read-only; guest sudo cannot override it), or +> - **(H2) Held on the host and supplied at exec time** from acq's own state +> (never read back from the guest). +> +> The guest may freely write its own genuinely-owned read-write state (the +> workspace, a per-sandbox cache, kit scratch) — acq simply must not read that +> state back and treat it as authoritative configuration. + +## Considered Options (trusted startup execution) + +The data case (H1/H2) is straightforward. The harder case is **running trusted +code in the guest at startup** — acq's restart heal must invoke a kit's +`startup`/normalizer logic without trusting a guest-resident, agent-tamperable +copy of it. Three options were considered: + +1. **Mount the kit's executable payload read-only and invoke it from the `:ro` + path.** acq stages the pinned kit's startup scripts/normalizers from the + host-side kit directory it already fetched into a per-sandbox host config + dir, mounts that dir `:ro` into the guest, and the heal executes + `sh /var/lib/acq/host//