The image ships pi with no provider and no preferences, so a sandboxed run
meant configuring the model twice. ai-sandbox now reads the host
~/.pi/agent/{models,settings}.json and rewrites them instead of copying them:
every provider apiKey becomes "$AI_API_TOKEN" (pi resolves an env reference
itself, so no value is written to disk), '!command' header values are dropped
rather than executed, settings that point into $HOME are dropped because that
path is not mounted, and auth.json / models-store.json / sessions / extensions
are never read.
The rewritten pair is seeded into /home/sandbox/.pi/agent at container start,
owned by uid 1000 and mode 600, and is treated as managed: a host change is
re-seeded, an edit made inside the sandbox is kept and reported, a deletion
takes the host version again.
The token follows the existing environment rules -- one variable, passed by
name through the 0600 env file, announced when auto-passed. A bridged run
whose token variable is unset or empty is refused instead of starting a pi
that cannot reach its model, and the bridge never widens the allowlist: it
warns with the exact --allow to add.
Docs and examples now use a self-hosted gateway (https://aipi.reeseapps.com)
instead of vendor API endpoints, since that is the only thing this tool is
used against. 391 assertions pass (make lint, ./tests/run.sh --e2e).
LLM-Generated: true
89 lines
6.4 KiB
Markdown
89 lines
6.4 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.
|
|
11. **The pi bridge reads a file that may hold a live key.** It rewrites `apiKey` to
|
|
an environment reference and never copies `auth.json`, but the *endpoint* in your
|
|
`models.json` becomes an endpoint the sandbox can be pointed at, and the token it
|
|
uses is your real one. So the bridged config is a convenience, not a narrower
|
|
credential: prefer a per-sandbox token from your gateway if it can mint one, and
|
|
note that a bridged run whose token variable is unset or empty is refused rather
|
|
than started. Sandboxed edits to `~/.pi/agent` persist in the home volume and are
|
|
kept over later host changes (see the README), which is convenience with a
|
|
consequence: a hostile run could rewrite the sandboxed pi config for later runs of
|
|
that volume. `--ephemeral-home` removes that.
|
|
|
|
## 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.*
|