0007. Rootless Podman Replaces Docker-Outside-of-Docker¶
Status¶
Accepted
Context¶
The dev container previously used Docker-Outside-of-Docker (DooD): the host's
Docker socket (/var/run/docker.sock) was bind-mounted into the container,
and docker-cli was installed to interact with the host daemon.
DooD has a critical security flaw: the Docker socket grants root-equivalent access to the host. Any process inside the container that can write to the socket can start privileged containers, mount host filesystems, and escape all container security measures. This undermines the hardened posture (egress firewall, scoped sudo, non-root user) that the rest of the container enforces.
Requirements: - Build OCI images inside the container (Packer, manual builds). - Run containers inside the container (test-kitchen, CI tooling). - Maintain Docker CLI compatibility for scripts and muscle memory.
Decision¶
Replace the Docker socket mount with rootless Podman running natively inside the container.
Package changes (Alpine)¶
- Remove:
docker-cli - Add:
podman,podman-docker,fuse-overlayfs,shadow-uidmap,passt
Configuration¶
- Storage driver: fuse-overlayfs (overlay with mount_program), with
skip_mount_home = "true"to avoid mount propagation issues. - Networking: pasta (default Podman 5+ backend via passt package).
- Cgroups: Disabled in containers.conf (outer runtime manages cgroups).
- User namespaces: subuid/subgid mappings for vscode (100000:65536).
- Docker compatibility:
podman-dockerprovides/usr/bin/dockerwrapper.
Why --privileged is required¶
Rootless Podman inside a Docker container needs to perform mount namespace
operations (overlay mounts, mount propagation changes) that the kernel blocks
in non-privileged containers. Individual capabilities (SYS_ADMIN,
SYS_CHROOT) and security options (seccomp=unconfined, label=disable) are
insufficient — the container runtime enforces mount restrictions at a level
below capability checks.
--privileged is the standard approach for running Podman inside Docker
containers (see Podman upstream documentation).
Security comparison: --privileged vs DooD¶
| Aspect | DooD (old) | Privileged + Podman (new) |
|---|---|---|
| Host escape | Yes — Docker socket = root on host | No — no socket, no host access |
| Egress firewall | Bypassable via Docker socket | Still enforced (iptables on outer namespace) |
| Non-root user | Bypassable via Docker socket | Still enforced (vscode, scoped sudo) |
| VM isolation | Applies but moot with socket | Applies and meaningful |
| Blast radius | Entire host | Container namespace only |
--privileged grants elevated capabilities within the container but
provides no path to the host. The egress firewall, non-root user model, and
Colima VM isolation all remain effective.
Storage persistence¶
Named volume ${project}-podman-storage at ~/.local/share/containers.
Egress firewall interaction¶
The iptables egress firewall operates on the outer container's network namespace. Podman's nested containers use pasta for networking, which translates traffic back through the outer namespace. Egress rules still apply to nested container traffic. Container registry endpoints (Docker Hub) are added to the allowlist.
Consequences¶
Easier: - No host Docker socket dependency. Container is self-contained. - No implicit root-equivalent access to the host. - Works on Podman hosts, not just Docker hosts. - Docker CLI compatibility is transparent via podman-docker. - Egress firewall rules apply to nested containers automatically.
Harder:
- Container runs with --privileged (elevated capabilities within container).
- Container registry endpoints must be in the egress allowlist.
- Podman storage volume adds another named volume to manage.
- First podman pull inside the container requires network access (images
are not pre-cached like Terraform providers).
- Tools that use the Docker Engine API (not the CLI) will not work — this
is acceptable since the goal is CLI compatibility.