Add README
ci/woodpecker/pr/lint Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful

This commit is contained in:
2026-10-23 22:29:01 +11:00
parent 7a6cb93fdb
commit 4ee52cb615
+83 -1
View File
@@ -1,3 +1,85 @@
# agent-sandbox
Rootless podman sandbox for Claude Code with allowlisted egress
Runs Claude Code in a rootless podman sandbox. The only thing visible from the
host is the directory you launch it from, and the only way out of the sandbox
is a per-session router that serves the hostnames on your allowlist.
## Usage
```sh
cd ~/src/some-repo
agent-sandbox [claude args...] # e.g. agent-sandbox --continue
agent-sandbox --exec bash # a shell in the sandbox instead of claude
```
- `$PWD` is mounted read-write at the same path. Files you create there are
owned by your host user. Nothing else from the host is mounted, and SELinux
labels on your files are never changed (`label=disable`).
- Claude's home is the `agent-sandbox-home` podman volume (login, config and
nested container images live here). All sessions share it.
- `podman` works inside the sandbox (nested rootless). The host podman socket is
never exposed.
- You can run several sessions at once, from different directories.
- Environment overrides: `AGENT_SANDBOX_IMAGE`, `AGENT_SANDBOX_ROUTER_IMAGE`,
`AGENT_SANDBOX_ALLOWLIST`, `AGENT_SANDBOX_HOME_VOLUME`.
## Allowlist
`~/.config/agent-sandbox/allowed-hosts` is copied from the packaged default on
first run. It is read at the start of each session.
```
api.anthropic.com exactly this host (ports 80 and 443)
.example.com example.com and every subdomain (ports 80 and 443)
db.example.com:5432 this exact host on TCP 5432 too (ssh, postgres, ...)
```
You don't need a proxy setting. Allowlisted names resolve to loopback
addresses inside the sandbox, and all other names return NXDOMAIN.
## How it works
Each session gets its own `--internal` podman network with no route out, and a
pod on that network that holds the sandbox's netns, DNS and hosts file.
- **router** (on the internal net and the shared egress net): dnsmasq answers
for allowlisted names only and never forwards a query. nginx routes 443 by
SNI and 80 by Host header to allowlisted names only. Each `host:port` entry
has its own router port, which forwards to that host and port only.
- **forwarder** (in the pod, read-only, no capabilities except
NET_BIND_SERVICE): socat listeners on the loopback IPs that forward to the
router.
- **claude** (in the pod, non-root keep-id user, no setuid binaries, no
NET_ADMIN, NET_RAW or SYS_ADMIN): has no capability over the netns. It can't
change addresses, routes, nftables, `/etc/hosts` or `/etc/resolv.conf`, and
can't see or signal the forwarders.
- Nested containers share claude's netns by default. They get the same DNS and
forwarders, and they can't change the netns either. A nested container on its
own network (`--network pasta|bridge`) has no egress.
## Threat model
- Confined: direct IP egress, other hostnames, DNS exfiltration (dnsmasq never
forwards), SNI or Host spoofing (the destination is always the name the
client sent, and it must be on the allowlist), the host filesystem beyond
`$PWD`, and the host podman socket.
- Not confined: anything you allowlist is still an exfiltration channel.
`git.unkin.net` and `api.anthropic.com` can carry your data out. `$PWD` is
writable, so anything there, including `.git/hooks`, runs later on the host
if you trust it.
- Router-side DNS resolves allowlisted names on the host's resolvers, so
internal names like `vault.service.consul` work.
- The home volume persists across sessions and is shared by all of them.
## Development
```sh
make build # local images (localhost/agent-sandbox{,-router}:dev)
make test lint pre-commit
make rpm # dist/agent-sandbox-*.rpm
make patch|minor|major
```
The in-repo launcher runs the local `:dev` images. The RPM's launcher pins the
images published by the release pipeline for its tag:
`artifactapi.k8s.syd1.au.unkin.net/docker-internal/agent-sandbox{,-router}`.