Chapter 26Lesson 05~165 minutes

Checkpoint Lab — Rootless Docker, RootlessKit, User Namespaces, Networking, Storage, Limitations, and Threat Reduction

Compare one synthetic workload under rootful and rootless contexts, preserve socket/data-root/UID/network/resource evidence, and state exactly which threat boundary improved and which did not.

Checkpoint labRootful vs rootlessEvidence packetThreat modelChapter 26

Learning objectives

  • Run a controlled rootful-versus-rootless comparison using the same immutable image identity.
  • Capture socket/data-root/UID/network/resource/storage evidence and verify predictions independently.
  • State the improved threat boundary and the risks that remain shared or external.
  • Produce an auditable evidence packet and bridge the result to UID/GID ownership work in Chapter 27.
Checkpoint rule. The same image digest under two daemons is not the same execution boundary. Compare daemon authority, namespace mapping, data root, networking, and resource governance explicitly.

1. Checkpoint objective and prerequisites

Run the same synthetic workload under a known rootful context and a known rootless context, then compare control-plane authority and observable behavior. If the host has no authorized rootful context, record only the rootless side and use the documented rootful expected-state table; do not create a privileged daemon merely for the exercise.

Required evidence: Engine/CLI version, context endpoint, SecurityOptions, data root, image RepoDigest, container ID/PID, UID/GID maps, network/port evidence, cgroup mode, resource evidence, storage driver, bind-mount ownership note, and a limitations/threat-model statement.

2. Create a clean evidence directory and capture assumptions

E="$HOME/da-ch26-checkpoint"
mkdir -p "$E"
date -Is | tee "$E/time.txt"
uname -a | tee "$E/uname.txt"
id | tee "$E/host-id.txt"
docker version | tee "$E/docker-version.txt"
docker context ls | tee "$E/contexts.txt"
grep "^$(whoami):" /etc/subuid 2>/dev/null | tee "$E/subuid.txt" || true
grep "^$(whoami):" /etc/subgid 2>/dev/null | tee "$E/subgid.txt" || true
stat -fc %T /sys/fs/cgroup 2>/dev/null | tee "$E/cgroup-fs.txt" || true

3. Predict before execution

Write predictions into $E/predictions.txt before running containers. At minimum:

  1. The rootless context will use a user-owned socket/data root and report rootless in SecurityOptions; the rootful context will not.
  2. Container UID 0 under rootless will map through a user namespace rather than to host UID 0.
  3. A loopback published high port such as 18081 will be reachable through the rootless port-forwarding path even though the container endpoint IP may not be directly reachable from the real host namespace.
  4. Resource limits will only be reliable in rootless mode if cgroup v2/systemd delegation is available.

4. Record both daemon identities before running the workload

ROOTLESS_CTX=rootless
ROOTFUL_CTX=default

docker \
  --context "$ROOTLESS_CTX" info \
  --format 'Security={{json .SecurityOptions}} Root={{.DockerRootDir}} Driver={{.Driver}} Cgroup={{.CgroupVersion}}'   | tee "$E/rootless-info.txt"

docker \
  --context "$ROOTFUL_CTX" info \
  --format 'Security={{json .SecurityOptions}} Root={{.DockerRootDir}} Driver={{.Driver}} Cgroup={{.CgroupVersion}}'   | tee "$E/rootful-info.txt"

If default is not your authorized rootful daemon, stop and replace it with the correct existing context or omit that half of the comparison.

5. Pull once per daemon and preserve immutable image evidence

IMG=busybox:1.37.0
for C in "$ROOTLESS_CTX" "$ROOTFUL_CTX"; do
  docker --context "$C" pull "$IMG"
  docker --context "$C" image inspect "$IMG" --format '{{json .RepoDigests}}' | tee "$E/$C-repodigests.txt"
done

Separate pulls are intentional because each daemon has its own image store. Matching RepoDigests prove content identity; they do not imply identical daemon authority or networking/storage state.

6. Run the same identity workload in each context

for C in "$ROOTLESS_CTX" "$ROOTFUL_CTX"; do
  docker --context "$C" run --rm "$IMG" sh -c 'id; cat /proc/self/uid_map; cat /proc/self/gid_map'     | tee "$E/$C-mapping.txt"
done

Interpret the maps, not just id. Inside both containers the process may print UID 0; the outer mapping and daemon privilege model are the security-relevant difference.

7. Run a bounded service and capture rootless network/resource evidence

docker \
  --context "$ROOTLESS_CTX" run -d \
  --name da-ch26-cp \
  --label academy.chapter=26 -p 127.0.0.1:18081:8080   "$IMG" sh -c 'while true; do printf "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nOK" | nc -l -p 8080; done'

docker \
  --context "$ROOTLESS_CTX" inspect da-ch26-cp \
  --format 'ID={{.Id}} PID={{.State.Pid}} Networks={{json .NetworkSettings.Networks}} Ports={{json .NetworkSettings.Ports}}'   | tee "$E/rootless-container.txt"
docker --context "$ROOTLESS_CTX" stats --no-stream da-ch26-cp | tee "$E/rootless-stats.txt"
curl -fsS http://127.0.0.1:18081 | tee "$E/rootless-http.txt"

8. Optional resource-control proof when cgroup v2/systemd delegation is present

Only run this if the preflight confirms cgroup v2 and the rootless daemon accepts delegated controls:

docker \
  --context "$ROOTLESS_CTX" run \
  --rm \
  --memory 64m \
  --pids-limit 64 "$IMG"   sh -c 'echo resource-controls-active; cat /proc/self/cgroup'   | tee "$E/rootless-resource-proof.txt"

If it fails because controllers are not delegated, preserve that failure as platform evidence; do not switch to privileged mode.

9. Threat-boundary conclusion

Create $E/conclusion.txt answering four questions:

  1. Improved: Which host-root authority did the rootless daemon/runtime no longer possess?
  2. Unchanged: Which shared-kernel/application/credential risks still exist?
  3. Different behavior: Which networking/storage/cgroup behaviors differed from rootful execution?
  4. Decision: Is rootless appropriate for this workload, or is a rootful feature/VM/sandbox boundary required?

10. Verification checklist and exact cleanup

  • Rootless SecurityOptions captured.
  • Rootless and rootful data roots compared.
  • RepoDigests recorded separately per daemon.
  • UID/GID maps captured and interpreted.
  • Published port verified without exposing all host interfaces.
  • cgroup/storage/network limitations recorded rather than hidden.
  • No daemon config, firewall, LSM, or privileged-mode workaround used.
docker --context rootless rm -f da-ch26-cp 2>/dev/null || true
# Keep $HOME/da-ch26-checkpoint as the evidence packet, or archive it deliberately:
tar -C "$HOME" -czf "$HOME/da-ch26-checkpoint.tgz" da-ch26-checkpoint

Review the evidence packet before sharing it: context endpoints, host usernames, paths, and system metadata can be sensitive in real environments.

11. What Chapter 26 adds—and the bridge to Chapter 27

Chapter 26 adds a daemon-level least-authority option: the Docker daemon and runtime themselves can execute without host-root privilege, with explicit user-namespace, networking, storage, and cgroup tradeoffs. Chapter 27 narrows the lens to UID/GID mapping, userns-remap, non-root images, filesystem ownership, and least-privilege identity—concepts that remain important whether the daemon is rootful or rootless.

Knowledge check

Why pull the same image separately in rootful and rootless contexts?

What two predictions must be verified independently in the checkpoint?

If the rootless memory-limit proof fails because cgroup controllers are not delegated, what should you do?

What threat does rootless mode reduce most directly?

What threat remains after switching to rootless?

Next lesson

Next: User Namespace Remapping, UID/GID Mapping, Non-Root Images, Filesystem Ownership, and Least Privilege: Concepts, Architecture, and Mental Model

Continue with the next lesson in the course sequence and carry forward the evidence-first Docker operating model.

Official references and version notes

Version/platform baseline, verified 2026-09-22.

Docker Engine 29.8.1 is the current Engine release. Engine 29.5 changed packaged rootless networking so gvisor-tap-vsock is the preferred/default path when slirp4netns is not separately installed, and rootless host networking now reaches the real host network namespace. Engine 29.8 updates RootlessKit to 3.1.0 and adds the optional pesto port driver when paired with pasta (IPv4 only). Rootless resource limits require cgroup v2 plus systemd delegation; rootless storage support is host/kernel dependent. Docker Desktop uses an Engine inside a managed Linux VM and must not be described as ordinary Rootless Docker. Every lab therefore records actual docker version, context, security options, cgroup/storage/network state rather than assuming upstream defaults.

Keep the academy open

Support free, practical DevOps education.

Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.