Skip to content

Security: Azathothas/errand-box

Security

SECURITY.md

Security

errand-box runs code an AI agent writes, on a host that also holds the agent's credentials. This page states what protects what, and what does not.

Reporting a vulnerability

Use GitHub's private vulnerability reporting on this repository ("Security", then "Report a vulnerability"). Do not open a public issue for it.

The trust boundary

side what runs there trusted?
host, root incusd, incus-user, lxcfs, s6 or systemd, errand-box setup, up, maint yes
host, the errand account errand, the bailey shim, the watchdog yes, but holds credentials (below)
guest everything in a session: kage, the agent's commands, Docker and podman inside, the guest's root no

A guest is an unprivileged system container. Its boundary is the shared host kernel, user namespaces with a uid range of its own, seccomp, and the cgroup limits Incus sets. It is not a virtual machine. A kernel vulnerability, used from inside a guest, gives control of the host.

Who holds what

credential where it lives can a guest read it?
chat bot token errand's config (~errand/.config/errand/config.json, mode 0600) no
model provider key errand's config; errand's broker adds it to requests no: under egress.mode = proxy the guest gets a per-session nonce, and a provider the broker cannot route gets no variable at all (docs/research/review.md)
GitHub token errand's config yes: errand puts it in the session's environment as GH_TOKEN. Use a token of an account that owns nothing of value.
the operator's creds.env wherever the operator keeps it; setup only reads it no

errand-box never prints a credential. setup names which keys it read, never their values. The shim passes the session's environment into the guest through the Incus API, not on a host command line.

What the operator supplies, and with which permissions

Give each credential before the step that needs it. Put it in creds.env (mode 0600) on the host, or in the repository's secrets for a workflow. Never in a chat, an issue, or a file in a repository.

for credential permissions it needs
errand: chat ERRAND_CHAT_TOKEN, a Discord bot token, and the channel id and user ids in errand-box.env the bot's privileged intent Message Content, set in the Discord developer portal (errand asks for Guilds, Guild Messages and Message Content); in the channel: view, send messages, create public threads, send messages in threads, manage threads (errand archives them), attach files, add reactions, read message history
errand: the model ERRAND_PROVIDER_CREDENTIAL, and ERRAND_PROVIDER_BASE_URL in errand-box.env a key for that endpoint only. With a base URL the key stays in errand's broker
sessions: GitHub (optional) ERRAND_GITHUB_TOKEN, and ERRAND_GITHUB_USER_NAME and ERRAND_GITHUB_USER_EMAIL in errand-box.env a fine-grained token of an account that owns nothing of value, limited to the repositories the agent may change: Contents read and write, Pull requests read and write, Metadata read. Every session can read it. errand-box does not configure errand's GitHub trigger, which reads notifications and needs a classic token
the scheduled workflows the workflow token, given by GitHub as each workflow declares. For freshness.yml to open pull requests, set "Allow GitHub Actions to create and approve pull requests" in the repository's Actions settings
CI on those pull requests (optional) a repository secret FRESHNESS_TOKEN a fine-grained token for this repository only: contents and pull requests, read and write. A pull request opened with the workflow token starts no workflow; one opened with this token does
a release none beyond the workflow token release.yml asks for contents: write

What a takeover of each component reaches

component taken over reaches
a guest's root its own instance; the two host directories errand granted it (the project and the session's state); errand's broker, with errand's allowlist. Not the host network, other guests, or the host's other files.
a guest that leaves its container through a setup mistake an unprivileged host uid of its own range; files of the errand account through the agent's mapped uid 1000.
a guest that leaves its container through a kernel vulnerability the host.
the errand account errand's credentials. Its restricted project forbids privileged containers, raw LXC settings, disk devices outside its home, and proxy devices (T-020), so the broker link is a shared unix socket. No device can reach a host socket the project does not intend (E-61 is closed). A guest cannot add devices, so only the errand account itself reaches this.
incusd, incus-user, lxcfs root.

The rules each change keeps

  1. The host never acts on a file a guest can write. The session's state directory is writable from the guest. So the shim checks the policy errand wrote there against the digest it recorded at trust, and every Incus client call names errand-box's own client directory instead of the HOME errand points into that state directory.
  2. No signal by name. Daemons stop through their supervisor, by pid.
  3. No unverified download runs. Every fetched archive is checked against pins.toml before use.
  4. A session gets no network device. Its only way out is errand's broker.
  5. errand's chat service is Discord outside a test. Only the variable ERRAND_DISCORD_MOCK_URL points errand at a local stand-in, and errand refuses it unless the URL and the stand-in's gateway are loopback IP addresses and the token begins with mock. (errand/src/chat/endpoint.rs). Nothing errand-box writes sets it.

Degraded on some hosts

errand-box doctor and setup report these, each as DEGRADED:

  • no AppArmor in the kernel: Incus loses its AppArmor profile; seccomp and per-guest uid ranges remain (edge case E-17);
  • no usable lxcfs: guests see the host's memory and CPU count (E-08);
  • a dir storage pool: the disk quota is not enforced (E-05);
  • Incus 6.0.0: new instances rewrite their rootfs, which is slow but not less safe (E-20).

There aren't any published security advisories