A single bash script that runs a coding harness (pi by default) inside a rootless Ubuntu 24.04 container with only the current directory mounted at /project, an explicit egress allowlist and an explicit environment: nothing is passed in that was not named on the command line or in a config file. Network enforcement here is empirical rather than assumed (tests/spike_network.sh, docs/networking.md). The original design -- a sidecar proxy on an internal network -- does not work rootless on this host, so allow mode runs pasta with --map-host-loopback, installs an inet aisb ruleset inside the container that drops every egress path except the gate's, and relays through a squid gate bound to the host's loopback that resolves names itself and refuses anything unlisted. The entrypoint checks that the default-drop rule really exists before setpriv drops to uid 1000 with an empty capability set, and exits 77/78 rather than run unenforced. The Containerfile, the entrypoint, the squid config generator and the ruleset renderer all live in bin/ai-sandbox, and the image is tagged by content hash, so a policy change can never silently reuse a stale image. Tests are pure bash: 161 assertions start no container and touch no network, 256 run real ones, including proof that a raw socket, a stripped proxy env and DNS all stay blocked while an allowlisted host works over both http and CONNECT. LLM-Generated: true
79 lines
5.6 KiB
Markdown
79 lines
5.6 KiB
Markdown
# Threat model
|
|
|
|
Read this before relying on `ai-sandbox`. It is a **seatbelt, not a jail**: it
|
|
raises the cost of an agent's mistakes and of a moderately compromised prompt
|
|
context, on a machine you still log into as yourself.
|
|
|
|
## Who we assume is hostile
|
|
|
|
The modelled adversary is **the agent you are running**: an LLM harness with your
|
|
write access to one directory, capable of running arbitrary shell commands, and
|
|
vulnerable to prompt injection from the files it reads, the web pages it fetches,
|
|
and the package changelogs it installs. We assume it will try to escape when a
|
|
prompt injection tells it to.
|
|
|
|
We do **not** model a kernel-level adversary, a hostile container image from a
|
|
registry you don't control, or someone with access to your host user account.
|
|
|
|
## What stops working (defence in depth, most to least load-bearing)
|
|
|
|
| Attack | Stopped by | Evidence |
|
|
|---|---|---|
|
|
| Editing / deleting anything outside the project directory | only `$PWD` is mounted; the rest of the host is not in the mount namespace | `tests/e2e/12_sandbox.sh` |
|
|
| Reading `~/.ssh`, `~/.aws`, `~/.config/gcloud`, browser profiles | same as above; those paths do not exist in the container | `tests/e2e/12_sandbox.sh` |
|
|
| Exfiltrating your environment (API keys for *other* services) | nothing is passed through unless named with `--env` | `tests/unit/04_env.sh`, `tests/e2e/12_sandbox.sh` |
|
|
| Passing secrets through the process list to other users | values travel in a `0600` file, never in argv | `tests/e2e/12_sandbox.sh` |
|
|
| Egress to an arbitrary host (curl, python, a raw socket, a bespoke exfil tool) | netfilter `policy drop` inside the container's namespace | `tests/e2e/11_net.sh` |
|
|
| Egress through the proxy to a non-allowlisted domain, including HTTPS | squid's `http_access deny all` after the allowlist | `tests/e2e/11_net.sh` |
|
|
| Deleting or weakening the egress ruleset | the command runs with no capabilities; `CAP_NET_ADMIN` was dropped across `exec` | `tests/e2e/11_net.sh` |
|
|
| `unshare -n` to get a fresh namespace with its own routes | requires `CAP_SYS_ADMIN`, which the command does not have | `tests/e2e/11_net.sh` |
|
|
| Privilege escalation via a setuid binary in the image | `--security-opt no-new-privileges` + `--cap-drop=ALL` | container config, asserted in `tests/e2e/10_image.sh` |
|
|
| Reaching other host services on loopback | only the gate's port is routable through the mapped loopback address | `tests/e2e/11_net.sh` |
|
|
| Fork bombs, memory bombs, runaway builds | `--pids-limit`, `--memory`, `--cpus`, `--tmpfs` sizes | `tests/e2e/10_image.sh` |
|
|
| Writing to the host's package manager, `/etc`, global npm/pip | read-only-ish image, `HOME` inside the container, optional `--read-only` | `tests/e2e/12_sandbox.sh` |
|
|
| Credential persistence you did not ask for | `--ephemeral-home`; the default home volume is named and inspectable | `tests/e2e/12_sandbox.sh` |
|
|
| Silent wide-open starts | an empty allowlist in `allow` mode is a hard error | `tests/unit/01_args.sh` |
|
|
|
|
## What does **not** stop it
|
|
|
|
Be honest about these, and design your work accordingly.
|
|
|
|
1. **Anything you allowlist is reachable, in both directions.** Allow
|
|
`api.github.com` and a prompt-injected agent can push your code to a repository
|
|
it controls, using the token you deliberately handed it. The allowlist limits
|
|
*where*, not *what*. Prefer least-privilege, read-only, short-lived tokens.
|
|
2. **The project directory is genuinely writable.** A bad `rm -rf`, a force push, a
|
|
corrupted lockfile all still happen — just confined to that directory. Keep the
|
|
tree committed (`ai-sandbox doctor` warns you when it is dirty).
|
|
3. **Kernel and runtime escapes.** A container is not a VM. A container escape
|
|
vulnerability in runc/crun or the kernel gets an attacker to your user account.
|
|
For genuinely untrusted code, use a VM or a disposable machine.
|
|
4. **`--net full` and the escape hatches do what they say.** `--allow-unenforced`,
|
|
`--insecure-keep-root`, `--mount`, `AISB_EXTRA_ARGS`, `--net full` all weaken the
|
|
posture. They are named to be uncomfortable on purpose; nothing uses them
|
|
implicitly.
|
|
5. **Side channels.** Timing, resource consumption, and "the agent typed X into a
|
|
form on a site you allowed" are out of scope.
|
|
6. **A cached home volume can hold credentials.** If a harness logs in inside the
|
|
sandbox, the token lives in the `aisb-home` volume, and any later sandbox run
|
|
using that volume can read it. `--ephemeral-home` per run, or a dedicated
|
|
`AISB_HOME_VOLUME` per trust level.
|
|
7. **DNS is closed in gate mode, open in `--no-gate` mode.** With `--no-gate`, name
|
|
lookups leave the container; that is a modest data channel and a fingerprint of
|
|
what you are doing. Egress to non-allowlisted IPs stays blocked either way.
|
|
8. **The gate sees everything that flows through it** — including any credentials
|
|
in proxied requests. That is the point of an egress proxy; the squid config logs
|
|
to its own container log, and `gate logs` shows them. Do not put secrets in URLs.
|
|
9. **Supply chain inside the allowlist.** `registry.npmjs.org` allowlisted means any
|
|
package, including a typosquatted one, can be fetched. Allowlisting a domain is
|
|
not a security review of what you install from it.
|
|
10. **Rootless podman still means shared kernel and shared UID namespace.** Files
|
|
the container writes are owned by *you*. There is no uid boundary between
|
|
"agent's file" and "your file" beyond ownership metadata and your own discipline.
|
|
|
|
## Residual risk rating
|
|
|
|
*Accidental destruction and ambient credential leakage: largely eliminated.*
|
|
*Deliberate data exfiltration over an allowlisted path: mitigated, not prevented.*
|
|
*Container escape to the host: out of scope, accept it or use a VM.*
|