Chapter 27Lesson 05~170 minutes

Checkpoint Lab — User Namespace Remapping, UID/GID Mapping, Non-Root Images, Filesystem Ownership, and Least Privilege

Prove a least-privilege write design for one mounted directory, then document exactly how its numeric ownership changes under ordinary rootful, userns-remap, and rootless Docker.

Checkpoint labOwnership matrixLeast privilegeEvidence packetChapter 27

Learning objectives

  • Build a non-root container that can write only one intended mounted directory and prove the result with numeric ownership evidence.
  • Compare the same design under no remapping, userns-remap mapping rules, and rootless mapping rules without requiring destructive daemon changes.
  • Capture image, runtime, mount, group, UID/GID-map, and retention evidence in one auditable packet.
  • State the final least-privilege contract and bridge it to secret/configuration handling in Chapter 28.
Checkpoint rule. Measured numeric evidence beats usernames and assumptions. Mark theoretical mapping rows honestly when a daemon mode is not actually present on the host.

1. Checkpoint scenario and safety boundary

You will build one non-root image, create one disposable mounted directory, prove that the application can write only that directory, and assemble an ownership matrix for three daemon models: ordinary rootful, userns-remap, and rootless. The mandatory execution path does not require enabling userns-remap or rootless; those mappings can be documented from current official rules unless the corresponding authorized context already exists.

2. Preflight, assumptions, and evidence packet

set -eu
LAB="$HOME/da-ch27-checkpoint"
E="$LAB/evidence"
SRC="$LAB/src"
DATA="$LAB/data"
mkdir -p "$E" "$SRC" "$DATA"

docker version > "$E/docker-version.txt"
docker compose version > "$E/compose-version.txt" 2>&1 || true
docker buildx version > "$E/buildx-version.txt" 2>&1 || true
docker context inspect "$(docker context show)" > "$E/context.json"
docker info --format 'Security={{json .SecurityOptions}} Root={{.DockerRootDir}}' > "$E/docker-info.txt"
id > "$E/host-id.txt"
grep "^$(whoami):" /etc/subuid 2>/dev/null > "$E/subuid.txt" || true
grep "^$(whoami):" /etc/subgid 2>/dev/null > "$E/subgid.txt" || true

3. Write predictions before changing state

Record at least these predictions in $E/predictions.txt:

  1. The image’s default runtime process will have UID/GID 10001:10001 and will not be host/container root.
  2. The process will write its prepared image path but will fail on a mounted directory whose numeric ownership does not authorize it.
  3. After precise ownership/group preparation of the disposable directory, only that path becomes writable; unrelated image/root paths remain protected.
  4. Under userns-remap/rootless, the host-visible owner for a container UID can differ according to the documented map even though the process still sees UID 10001 inside.

4. Build the checkpoint image and record immutable identity

cat > "$SRC/Dockerfile" <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.22.1
RUN addgroup -g 10001 app && adduser -D -H -u 10001 -G app app     && mkdir -p /app /data     && chown -R 10001:10001 /app /data
COPY --chown=10001:10001 app.txt /app/app.txt
USER 10001:10001
WORKDIR /app
CMD ["sh", "-c", "id; cat /app/app.txt; sleep 3600"]
EOF
printf 'least-privilege-checkpoint
' > "$SRC/app.txt"

docker build --progress=plain -t da-ch27-checkpoint:lab "$SRC" | tee "$E/build.log"
docker image inspect da-ch27-checkpoint:lab --format 'ID={{.Id}} RepoDigests={{json .RepoDigests}} User={{json .Config.User}}' | tee "$E/image.txt"

5. Prove default identity and writable image path

docker run -d --name da-ch27-cp --label academy.chapter=27 da-ch27-checkpoint:lab
docker exec da-ch27-cp sh -c 'id; touch /data/image-write-ok; stat -c "%u:%g %a %n" /data/image-write-ok; cat /proc/self/uid_map; cat /proc/self/gid_map'   | tee "$E/default-runtime.txt"
docker inspect da-ch27-cp --format 'User={{json .Config.User}} PID={{.State.Pid}} Mounts={{json .Mounts}}' > "$E/container-inspect.txt"

6. Mount the host directory, capture failure, then apply a precise lab-only repair

printf 'seed
' > "$DATA/seed.txt"
chmod 0755 "$DATA"
chmod 0644 "$DATA/seed.txt"
stat -c '%u:%g %a %n' "$DATA" "$DATA/seed.txt" | tee "$E/data-before.txt"

docker run --rm --mount type=bind,src="$DATA",dst=/data da-ch27-checkpoint:lab   sh -c 'id; echo first > /data/container.txt' >"$E/mount-first.out" 2>"$E/mount-first.err" || true

# Apply only when ordinary non-remapped native Linux semantics are proven.
if ! docker info --format '{{json .SecurityOptions}}' | grep -Eq 'rootless|name=userns'; then
  chown -R 10001:10001 "$DATA" 2>/dev/null || true
fi

docker run --rm --mount type=bind,src="$DATA",dst=/data da-ch27-checkpoint:lab   sh -c 'id; echo second > /data/container.txt; stat -c "%u:%g %a %n" /data/container.txt'   | tee "$E/mount-second.txt" || true
stat -c '%u:%g %a %n' "$DATA" "$DATA/container.txt" 2>/dev/null | tee "$E/data-after.txt" || true

If the current daemon is remapped/rootless or the host does not permit chown, do not force the command. Preserve the failed second step and calculate/document the correct mapping or choose a group/volume design.

7. Build the ownership matrix for all three daemon models

Add a file $E/ownership-matrix.txt containing the actual current daemon row plus two documented comparison rows:

Mode Container UID 0 host mapping Container UID 10001 mapping Daemon privilege
No userns 0 10001 root
userns-remap subuid_start subuid_start + 10001 root
rootless host user UID subuid_start + 10000 non-root user

For GIDs, apply the same rule using /etc/subgid. If your current host provides an actual rootless/userns context, replace the theoretical row with captured uid_map/gid_map evidence.

8. Prove the negative boundary: unrelated paths remain unwritable

docker run --rm --mount type=bind,src="$DATA",dst=/data da-ch27-checkpoint:lab sh -c '
  id
  echo ok > /data/allowed.txt
  (echo no > /etc/should-not-write 2>/tmp/deny && echo unexpected) || echo "expected: /etc write denied or protected by image permissions/policy"
  stat -c "%u:%g %a %n" /data/allowed.txt
' | tee "$E/negative-boundary.txt"

Exact behavior of /etc depends on the image and runtime configuration; the important evidence is that the application’s intended write surface is explicit and no broader host write authority was introduced.

9. Verification checklist, limitations, and cleanup

  • Image digest/ID and configured USER captured.
  • Runtime numeric UID/GID and UID/GID maps captured.
  • Host directory numeric owner/mode captured before and after.
  • First permission failure preserved.
  • No chmod 777, privileged mode, Docker socket mount, or --userns=host workaround used.
  • Userns-remap/rootless comparison clearly labels measured versus theoretical evidence.
  • Evidence packet records platform limitations (Desktop/native Linux, daemon mode, host chown support).
docker rm -f da-ch27-cp 2>/dev/null || true
docker image rm da-ch27-checkpoint:lab 2>/dev/null || true
tar -C "$HOME" -czf "$HOME/da-ch27-checkpoint.tgz" da-ch27-checkpoint

Review host usernames, paths, context endpoints, and subordinate ranges before sharing the evidence archive.

10. What Chapter 27 adds—and the bridge to Chapter 28

Chapter 27 turns “run as non-root” into a measurable identity/storage contract: exact numeric IDs, namespace translation, supplementary groups, and writable-path ownership are preserved as evidence. Chapter 28 applies the same least-authority reasoning to secrets and configuration—where the question becomes not only who can write, but which process can ever read sensitive values, where they persist, and how they are rotated.

Knowledge check

What proves the checkpoint achieved least privilege?

Why label userns-remap/rootless rows as measured or theoretical?

What host value corresponds to container UID 10001 under userns-remap?

What host value corresponds to container UID 10001 under rootless mode?

Why does this chapter lead naturally into secrets?

Next lesson

Next: Secrets and Configuration Patterns, Runtime Injection, Environment Risk, Build Secrets, and External Secret Stores: 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

  • User namespace remapping — current prerequisites, mapping behavior, daemon configuration, limitations, and migration cautions.
  • Rootless UID/GID mapping — precise difference between userns-remap and rootless host-ID translation.
  • Rootless mode — daemon-level non-root execution and subordinate-ID prerequisites.
  • Dockerfile reference — USER and COPY --chown semantics, including numeric versus name lookup.
  • Compose services reference — service user override and user namespace configuration surface.
  • Bind mounts — host-path ownership, read/write authority, and daemon-host path semantics.
  • Volumes — Docker-managed persistent storage and container mount behavior.
  • Docker Engine 29 release notes — current Engine baseline and user-namespace-related fixes/changes.
Version/platform baseline, verified 2026-09-22.

Docker Engine 29.8.1 is the current Engine release. ' Docker documents two different mappings: in userns-remap, container UID/GID 0 maps to the first subordinate ID and ID n maps to subordinate-start + n; in rootless mode, container UID/GID 0 maps to the host user and IDs ≥1 map into the subordinate range with an offset. ' Docker recommends enabling userns-remap on a new daemon because existing Docker objects become masked by the remapped storage layout, and it warns that host-mounted filesystem ownership must be arranged for the mapped IDs. ' COPY --chown accepts names or numeric IDs; names require matching /etc/passwd//etc/group inside the build rootfs, while numeric IDs do not. ' Compose user overrides the image-configured user. Every lab therefore records the actual Engine/context/security mode and numeric UID/GID evidence instead of assuming usernames imply equivalent authority.

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.