Skip to content

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-docker provides /usr/bin/docker wrapper.

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.