diff --git a/SECURITY.md b/SECURITY.md index 1a00fa0..11e28ba 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -10,40 +10,66 @@ are. The security design follows from that single fact: the agent runs *inside* a hardened Docker container, and **that container is the permission boundary** — -not the prompt, not the agent's judgment. The container is configured to -drop all Linux capabilities, forbid privilege escalation -(`--no-new-privileges`), run as a non-root user, and cap memory, CPU, and PID -counts. A malicious README can do whatever it likes to that disposable -container; the goal is that it can't reach your host, your network, or your -other runs. - -## Known tradeoffs (MVP, documented on purpose) - -These are real limitations of the current release. We would rather name them -than let you discover them. - -- **The API key enters the sandbox.** In the current design, the agent needs - model credentials inside the container — whichever provider you run on - (`ANTHROPIC_API_KEY`/`CLAUDE_CODE_OAUTH_TOKEN`, or `OPENAI_API_KEY` / +not the prompt, not the agent's judgment. A malicious README can do whatever it +likes to that disposable container; the goal is that it can't reach your host, +your secrets beyond what you deliberately inject, or your other runs. + +## Sandbox claims vs roadmap + +Every row marked **enforced today** was re-verified against the cited code on +this checkout. Rows marked **planned** or **known limitation** are intentional +honesty, not marketing. Parent epic for planned egress/credential hardening: +[#64](https://github.com/alphacrack/readme2demo/issues/64). + +| Claim (agent + verify containers) | Status | Where (verify on your checkout — line numbers drift) | +|---|---|---| +| All Linux capabilities dropped | **enforced today** | `src/readme2demo/sandbox.py` `start()` — `"--cap-drop", "ALL"` | +| No privilege escalation | **enforced today** | `sandbox.py` `start()` — `"--security-opt", "no-new-privileges"` | +| Memory / CPU / PID caps | **enforced today** | `sandbox.py` `Sandbox` defaults `memory="4g"`, `cpus="2"`, `pids_limit=512`; applied in `start()` | +| Non-root user in image | **enforced today** | `images/base/Dockerfile` — `useradd … demo` then `USER demo` | +| Target repo mounted read-only | **enforced today** | `agent.py` — mount `(…/repo, "/repo", "ro")`; agent copies to writable `/work` | +| Verify replays in a fresh container without model credentials | **enforced today** | `verify.py` — `Sandbox(...)` built with **no** `env=` argument | +| Docker socket only on explicit opt-in | **enforced today** | `agent.py` / `verify.py` — socket mounted only when `cfg.allow_docker_socket` | +| Network egress domain allowlist | **planned (v0.8)** — today: plain Docker network | `sandbox.py` default `network: str = "bridge"`; `start()` passes `"--network", self.network`. No allowlist in tree. See [#64](https://github.com/alphacrack/readme2demo/issues/64) | +| Host-side key-injecting proxy (credentials never enter the sandbox) | **planned (v0.8)** | [#64](https://github.com/alphacrack/readme2demo/issues/64) | +| Disk quotas on the work volume | **planned (v0.8)** | [#64](https://github.com/alphacrack/readme2demo/issues/64) | +| Red-team acceptance harness (hostile-README integration tests) | **planned (v0.8)** | [#64](https://github.com/alphacrack/readme2demo/issues/64) | +| API key visible on the docker argv (`-e KEY=VALUE`) | **known limitation** | `sandbox.py` `start()` env loop (`-e f"{k}={v}"`); agent passes `env=env`. Tracked in [#51](https://github.com/alphacrack/readme2demo/issues/51) | +| Render-stage container hardening (cap-drop / no-new-privileges / pids) | **known limitation** | `render.py` builds its own `docker run` with `--memory`/`--cpus`/`--network` only (no `--cap-drop ALL`, no `no-new-privileges`, no `--pids-limit`). Untrusted-execution surface separate from `Sandbox.start()`. See architecture/README.md | + +### Known tradeoffs (narrative) + +These match the table; keep them named so adopters are not surprised. + +- **The API key enters the sandbox (MVP).** The agent stage constructs + `Sandbox(..., env=env)` so the model credential is present inside the + container and also appears on the `docker run` argv — whichever provider you + run on (`ANTHROPIC_API_KEY`/`CLAUDE_CODE_OAUTH_TOKEN`, or `OPENAI_API_KEY` / `GEMINI_API_KEY` via the litellm-style `LLM_API_KEY` with the provider - presets). A malicious repo that fully - compromises the agent could read them. **Mitigation: use a dedicated, - low-limit API key for runs — never your primary key.** A host-side, - key-injecting egress proxy that keeps credentials out of the sandbox is on - the roadmap. -- **`--allow-docker-socket` pierces the isolation.** Some tools legitimately - manage their own containers, so this flag mounts the Docker socket into the - sandbox. Doing so is effectively root on the host's Docker daemon. **Only - pass it for repos you trust**, and the CLI treats it as an explicit, - opt-in security tradeoff. -- **Network egress is allowlisted, not eliminated.** Runs need to clone repos - and pull packages, so the sandbox has outbound network access. Data - exfiltration by a determined, compromised agent is not fully prevented in - this release. + presets). A fully compromised agent can read it. **Mitigation: use a + dedicated, low-limit key — never your primary key.** Host-side key injection + is planned in [#64](https://github.com/alphacrack/readme2demo/issues/64); + argv exposure is tracked in + [#51](https://github.com/alphacrack/readme2demo/issues/51). Related: + [#52](https://github.com/alphacrack/readme2demo/issues/52). +- **`--allow-docker-socket` pierces isolation.** When set, the Docker socket is + mounted read-write into the sandbox (agent and verify; render can also mount + it when enabled). That is effectively root on the host Docker daemon. **Only + pass it for repos you trust.** +- **Network egress is open bridge networking today, not allowlisted.** Runs need + to clone repos and pull packages, so the default is `--network bridge` with + no domain filter. Data exfiltration by a determined, compromised agent is not + fully prevented in this release. Domain allowlisting is planned for v0.8 + ([#64](https://github.com/alphacrack/readme2demo/issues/64)). +- **Render container is less hardened than agent/verify.** The render stage does + not go through `Sandbox.start()`; see the table row above. Do not run readme2demo against untrusted repos on a machine that holds secrets you can't afford to rotate. Prefer a throwaway VM or CI runner. +A shorter operator-facing summary lives in +[`docs/security.md`](docs/security.md) and links back here. + ## Supported versions readme2demo is pre-1.0 and moves fast. Security fixes land on `main` and in diff --git a/docs/security.md b/docs/security.md index edceb50..b1ef665 100644 --- a/docs/security.md +++ b/docs/security.md @@ -5,24 +5,42 @@ design concern, not an afterthought. ## The sandbox is the boundary -The agent runs *inside* a hardened Docker container: +The **agent and verify** stages run *inside* hardened Docker containers (`Sandbox.start()`): - `--cap-drop ALL` and `--security-opt no-new-privileges` -- non-root user +- non-root user (`USER demo` in the base image) - memory, CPU, and PID limits - the repo is mounted read-only; the agent works on a writable copy The verify stage then replays the distilled `commands.sh` in a **separate, -fresh container** with no state carried over from the agent's run. +fresh container** with no model credentials and no state carried over from the +agent's run. + +For a **claims-vs-roadmap table** (enforced today vs planned v0.8 vs known +limitations, each with code pointers), see the root +[`SECURITY.md`](https://github.com/alphacrack/readme2demo/blob/main/SECURITY.md). That table is the source of truth; this page +is the short summary. ## Known tradeoffs +- **Render container is not fully hardened like agent/verify.** `render.py` runs + its own `docker run` with memory/CPU/network limits only (no cap-drop / + no-new-privileges / pids-limit). See the root SECURITY.md table. + + - **API key in the sandbox** (MVP): the model credential currently enters the - sandbox. Use a dedicated, low-limit key. A host-side, key-injecting egress - proxy is planned so the credential never crosses the boundary. + sandbox (and the `docker run` argv). Use a dedicated, low-limit key. A + host-side, key-injecting egress proxy is planned so the credential never + crosses the boundary ([#64](https://github.com/alphacrack/readme2demo/issues/64); + argv exposure tracked in + [#51](https://github.com/alphacrack/readme2demo/issues/51)). - **`--allow-docker-socket`** is off by default. Enabling it mounts the host Docker socket into the sandbox so demos that manage containers work — but the socket pierces isolation. Only enable it for repositories you trust. +- **Network egress is open bridge networking today, not allowlisted.** The + sandbox defaults to Docker `--network bridge` so package installs and git + clones work. Domain allowlisting is planned for v0.8 ([#64](https://github.com/alphacrack/readme2demo/issues/64)). Until then, + treat outbound network from a compromised agent as possible. ## Reporting a vulnerability