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.
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.
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:
- The image’s default runtime process will have UID/GID 10001:10001 and will not be host/container root.
- The process will write its prepared image path but will fail on a mounted directory whose numeric ownership does not authorize it.
- After precise ownership/group preparation of the disposable directory, only that path becomes writable; unrelated image/root paths remain protected.
- 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.
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=hostworkaround 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?
The process runs with the intended non-root numeric identity, can write only the declared data path, and no broader host or container write authority was granted.
Why label userns-remap/rootless rows as measured or theoretical?
Because mappings depend on the actual daemon/context and subordinate ranges; documentation can explain rules but should not be misrepresented as host evidence.
What host value corresponds to container UID 10001 under userns-remap?
subuid_start + 10001, assuming that range is the active mapping.
What host value corresponds to container UID 10001 under rootless mode?
subuid_start + 10000 because rootless maps container UID 0 to the real host UID and shifts IDs 1+ into the subordinate range.
Why does this chapter lead naturally into secrets?
Both problems are authority design: first who can write/read filesystem state, next which identities/processes can access sensitive configuration and where that data persists.
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.
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.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.