container: Use rootless Podman instead of Docker
Access to the Docker socket is equivalent to host root: whoever can talk to /var/run/docker.sock can start a privileged container with the host filesystem mounted and edit any file on it. Membership of the docker group defeats the containment this tool exists to provide, because an agent escapes the project directory by asking the daemon, not by exploiting a bug. Rootless Podman keeps the workflow and drops the escape: containers run in a user namespace owned by the invoking user, and container root maps to a subuid from /etc/subuid instead of to host root. Rootless containers are created with --userns=keep-id so that the host UID/GID map to the same values inside the container and bind-mounted files keep their ownership in both directions; without it the container UID 1000 would own nothing in the project. Sudo inside the container keeps working, but only reaches container root. Docker stays available through AGENT_CONTAINER_RUNTIME=docker for hosts that cannot run rootless Podman. Existing Docker containers and images are deliberately left alone: they are not reused, and the first Podman run builds its own image.
This commit is contained in:
parent
aff70f009e
commit
ed1c9a7b92
2 changed files with 104 additions and 25 deletions
65
README.md
65
README.md
|
|
@ -1,9 +1,10 @@
|
|||
# agent-container
|
||||
|
||||
Run OpenCode, Claude Code, and Pi (pi.dev) inside a shared Arch Linux Docker container that closely mirrors a local development environment, while limiting access to sensitive files on the host.
|
||||
Run OpenCode, Claude Code, and Pi (pi.dev) inside a shared Arch Linux Podman container that closely mirrors a local development environment, while limiting access to sensitive files on the host.
|
||||
|
||||
## Features
|
||||
|
||||
- **Rootless**: Containers run as your own user inside a user namespace via rootless Podman. No privileged daemon, no `docker` group, nothing that turns a compromised agent into host root (see "Container runtime" below)
|
||||
- Tools are not baked into the image: OpenCode, Claude Code, and Pi are
|
||||
bootstrapped into the persistent container home on first use (official
|
||||
installers, no sudo), so `pi update`, `claude update`, and
|
||||
|
|
@ -17,9 +18,66 @@ Run OpenCode, Claude Code, and Pi (pi.dev) inside a shared Arch Linux Docker con
|
|||
- Mounts only the current project directory (same absolute path inside container)
|
||||
- **Security boundary**: No access to SSH keys, passwords, or full `$HOME` (intentionally prevents remote code pushes)
|
||||
|
||||
## Container runtime
|
||||
|
||||
Containers run with rootless Podman. Nothing about that needs a privileged
|
||||
daemon or a privileged group: the container lives inside a user namespace owned
|
||||
by your own user, so it has exactly your file permissions and no more.
|
||||
|
||||
This matters because Docker's `docker` group is equivalent to unrestricted root
|
||||
on the host. Write access to `/var/run/docker.sock` lets anyone start a
|
||||
privileged container with `-v /:/host` and edit any file, including `/etc/shadow`
|
||||
and your SSH keys. An agent with that access is one bad tool call away from
|
||||
owning the machine. Rootless Podman removes that path entirely: container root
|
||||
maps to a subordinate UID from `/etc/subuid`, not to host root.
|
||||
|
||||
How the details are handled:
|
||||
|
||||
- Containers are created with `--userns=keep-id`, so your host UID/GID map to
|
||||
the same UID/GID inside. Files in the mounted project and in the container home
|
||||
keep their normal ownership in both directions.
|
||||
- `sudo` inside the container still works (the image has passwordless sudo), but
|
||||
only reaches container root, which is a subordinate UID on the host. Files that
|
||||
container root creates on the bind mounts show up on the host as owned by that
|
||||
subordinate UID (typically `100000:100000`), not by `root`.
|
||||
- Container storage (images, writable layers) lives in
|
||||
`~/.local/share/containers/storage`, not in `/var/lib/docker`.
|
||||
- Docker is still possible if you really want it:
|
||||
`AGENT_CONTAINER_RUNTIME=docker agent-container pi`.
|
||||
|
||||
### Migrating from Docker
|
||||
|
||||
Existing `ac-*` containers and the `agent-container:latest` image in Docker are
|
||||
not reused; Podman builds and manages its own copy. Remove the old ones once you
|
||||
no longer need them:
|
||||
|
||||
```sh
|
||||
docker ps -a --filter 'name=^ac-' --format '{{.Names}}' | xargs -r docker rm -f
|
||||
docker rmi agent-container:latest
|
||||
```
|
||||
|
||||
Only after that, and after moving any other user-level services off Docker (see
|
||||
below), leave the `docker` group:
|
||||
|
||||
```sh
|
||||
sudo gpasswd -d "$USER" docker
|
||||
```
|
||||
|
||||
The change takes effect on your next login (`newgrp docker` or a fresh session).
|
||||
System services that run Docker as root keep working; only your own access to the
|
||||
socket goes away.
|
||||
|
||||
## Install
|
||||
|
||||
On a fresh machine, two steps are all it takes. The agent tools themselves are not part of the image and need no installation step: on first use, agent-container.py automatically runs each tool's official installer inside the container (no sudo), just like it builds the Docker image automatically if it is missing.
|
||||
You need rootless Podman on the host. On Arch Linux the packages `podman` and `passt` (or `slirp4netns`) are enough; your user also needs subuid/subgid entries, which `usermod --add-subuids 100000-165535 --add-subgids 100000-165535 <user>` sets up if the package did not:
|
||||
|
||||
```sh
|
||||
sudo pacman -S podman passt
|
||||
grep "^$USER:" /etc/subuid /etc/subgid
|
||||
podman info >/dev/null && echo "rootless podman works"
|
||||
```
|
||||
|
||||
On a fresh machine, two more steps are all it takes. The agent tools themselves are not part of the image and need no installation step: on first use, agent-container.py automatically runs each tool's official installer inside the container (no sudo), just like it builds the Podman image automatically if it is missing.
|
||||
|
||||
```sh
|
||||
cd ~/Projects/
|
||||
|
|
@ -73,6 +131,7 @@ This means inline overrides work as expected: `OPENCODE_CONFIG=foo opencode`
|
|||
For persistent environment variables, add them to `container-home/.bashrc` (or `.bash_profile` / `.profile`). Tools run through `bash -l` inside the container, so standard shell config files are sourced automatically. Default shell config files from `/etc/skel` are seeded into the container home on first run.
|
||||
|
||||
- `XDG_DATA_HOME`: Override default data directory (default: `~/.local/share`)
|
||||
- `AGENT_CONTAINER_RUNTIME`: Container runtime to use (default: `podman`, set to `docker` for the old behaviour)
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
@ -120,7 +179,7 @@ To remove all containers, the image, and the persistent home directory:
|
|||
agent-container purge
|
||||
```
|
||||
|
||||
Note that this also deletes the shared agent state that lives in the container home (`.claude`, `.pi`, ...). The symlinks in your `$HOME` will dangle afterwards.
|
||||
Note that this also deletes the shared agent state that lives in the container home (`.claude`, `.pi`, ...). The symlinks in your `$HOME` will dangle afterwards. Podman's own storage directory is left alone; other Podman images and containers are not touched.
|
||||
|
||||
## Sharing host config files via hard links
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue