Docker Socket Security, Docker-in-Docker, Socket Mounting, Build Services, and CI Isolation Tradeoffs: Guided Hands-On Workflow and Core Operations
Inspect Docker authority without exploiting it, then use two disposable Buildx builder instances to model per-job build state while keeping untrusted-job socket and privileged patterns out of the runnable path.
Learning objectives
- Inspect socket, context, daemon and builder identity without giving a new container access to the Docker control plane.
-
Create two disposable Buildx
docker-containerbuilder instances from a trusted controller and keep their build cache/output identities separate. - Distinguish a safe builder-isolation exercise from the much stronger claim that untrusted job code is isolated from the host daemon.
- Compare host-socket, DinD, rootless and remote-BuildKit patterns without executing privileged or socket-mount shortcuts.
- Clean up only the exact builders and files created by the lab.
1. Lab contract and safety boundary
This lab is intentionally split into trusted controller and simulated job roles. You run the Docker/Buildx commands from your normal authorized shell. No lab container receives the host Docker socket, and no lab command launches a privileged container. Two BuildKit workers model per-job build state; they do not claim that the controller’s host daemon has been isolated from itself.
Names beginning with dca35- and the local directory
dca35-lab are the only disposable resources.
2. Preflight: capture platform, context, daemon and builder state
set -eu
LAB=dca35-lab
mkdir -p "$LAB/evidence" "$LAB/src" "$LAB/out-a" "$LAB/out-b"
date -u +%Y-%m-%dT%H:%M:%SZ | tee "$LAB/evidence/time-start.txt"
docker context show | tee "$LAB/evidence/context.txt"
docker version | tee "$LAB/evidence/docker-version.txt"
docker info | tee "$LAB/evidence/docker-info.txt"
docker buildx version | tee "$LAB/evidence/buildx-version.txt"
docker buildx ls | tee "$LAB/evidence/builders-before.txt"
3. Inspect the native socket if one is visible
if [ -S /var/run/docker.sock ]; then
ls -l /var/run/docker.sock | tee dca35-lab/evidence/socket-ls.txt
stat /var/run/docker.sock | tee dca35-lab/evidence/socket-stat.txt
else
printf '%s
' 'No native socket visible; record context/endpoint instead.' | tee dca35-lab/evidence/socket-note.txt
fi
This is evidence collection only. Do not change socket mode or group membership and do not bind it into a lab container.
4. Create a deterministic synthetic build context
cat > dca35-lab/src/Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.22
ARG JOB_ID
RUN printf 'job=%s
' "$JOB_ID" > /artifact.txt
FROM scratch
COPY --from=0 /artifact.txt /artifact.txt
EOF
sha256sum dca35-lab/src/Dockerfile | tee dca35-lab/evidence/dockerfile.sha256
The final filesystem contains only /artifact.txt. No
secret, registry credential, host path, or Docker socket is part of
the build context.
5. Trusted controller creates two disposable builder instances
docker buildx create --name dca35-job-a --driver docker-container --bootstrap
docker buildx create --name dca35-job-b --driver docker-container --bootstrap
docker buildx inspect dca35-job-a | tee dca35-lab/evidence/builder-a.txt
docker buildx inspect dca35-job-b | tee dca35-lab/evidence/builder-b.txt
The controller is using its existing authorized Docker endpoint to provision two BuildKit workers. In a hardened CI system, untrusted repository code should not be the component that receives this controller authority.
6. Build job A into a local output directory
rm -rf dca35-lab/out-a/*
docker buildx build --builder dca35-job-a --progress=plain --build-arg JOB_ID=A --output type=local,dest=dca35-lab/out-a dca35-lab/src | tee dca35-lab/evidence/build-a.log
cat dca35-lab/out-a/artifact.txt | tee dca35-lab/evidence/output-a.txt
type=local exports the final filesystem to the
requested directory. It does not load a runnable image into the
shared Engine image store, which keeps this exercise focused on
builder state.
7. Build job B on the separate builder
rm -rf dca35-lab/out-b/*
docker buildx build --builder dca35-job-b --progress=plain --build-arg JOB_ID=B --output type=local,dest=dca35-lab/out-b dca35-lab/src | tee dca35-lab/evidence/build-b.log
cat dca35-lab/out-b/artifact.txt | tee dca35-lab/evidence/output-b.txt
8. Prove builder and cache namespaces are distinct
docker buildx du --builder dca35-job-a | tee dca35-lab/evidence/cache-a.txt
docker buildx du --builder dca35-job-b | tee dca35-lab/evidence/cache-b.txt
docker buildx ls | tee dca35-lab/evidence/builders-after-build.txt
printf 'A='; cat dca35-lab/out-a/artifact.txt
printf 'B='; cat dca35-lab/out-b/artifact.txt
The two builder names resolve to different BuildKit worker instances and have separate cache accounting. This is useful job-level build isolation. It is not proof that repository code could not control the host daemon if you also gave that repository the Docker socket.
9. Observe the controller-side builder containers without entering them
docker ps \
--filter 'name=buildx_buildkit_dca35-job-a' \
--format '{{.ID}} {{.Names}} {{.Image}} {{.Status}}' | tee dca35-lab/evidence/controller-builder-a-container.txt
docker ps \
--filter 'name=buildx_buildkit_dca35-job-b' \
--format '{{.ID}} {{.Names}} {{.Image}} {{.Status}}' | tee dca35-lab/evidence/controller-builder-b-container.txt
This observation is performed by the trusted controller. An untrusted job should not need host-daemon listing rights merely to submit a build to a remote or pre-provisioned BuildKit endpoint.
10. Architecture comparison without executing dangerous variants
| Pattern | Would this lab run it? | Why / safer teaching path |
|---|---|---|
| Host socket mounted into a job | No | Inspect socket authority and discuss API capability; do not hand host control to a lab container |
| Classic privileged DinD | No | Model architecture and provider guidance; no need to disable container security mechanisms for this lesson |
| Rootless DinD inside Docker | No mandatory run | Docker documents that its containerized rootless DinD image still needs privileged outer execution; use rootless host/VM or provider-managed isolation when evaluating it |
| Rootless BuildKit on Linux | Optional | A non-root BuildKit daemon can provide a narrower build endpoint when RootlessKit/user namespaces are available |
| Remote BuildKit | Design exercise | Use TLS/Unix/SSH-style protected transport and per-job credentials; no remote service is required for the mandatory lab |
11. Optional Linux extension: inspect rootless BuildKit prerequisites
command -v rootlesskit || true
command -v buildkitd || true
command -v buildctl || true
id
[ -r /etc/subuid ] && grep "^$(id -un):" /etc/subuid || true
[ -r /etc/subgid ] && grep "^$(id -gn):" /etc/subgid || true
If these prerequisites are not present, stop at inspection. Do not change kernel/user-namespace policy merely to complete the chapter. A real CI platform can provision an isolated rootless BuildKit worker outside the job instead.
12. Exact cleanup only
docker buildx rm dca35-job-a dca35-job-b
docker buildx ls | tee dca35-lab/evidence/builders-after-cleanup.txt
find dca35-lab -maxdepth 2 -type f -print | sort | tee dca35-lab/evidence/files-before-local-delete.txt
The evidence directory is intentionally retained for review. After
reviewing it, delete only dca35-lab with your normal
file manager or rm -rf dca35-lab while you are in the
directory that contains that exact lab path. No Docker prune
operation is required.
13. Challenge: choose the layer, not the memorized command
Your organization accepts public pull requests. The current runner launches a job container and that job can list unrelated long-lived containers. Which layer is wrong?
Answer target: this is not an image-build bug. The CI job has been connected to a shared daemon/control plane. Fix the runner/build-service architecture so untrusted code receives an isolated or narrow build endpoint, then scope cache and credentials to that worker. Do not attempt to hide unrelated containers with naming conventions.
Knowledge check
What exactly did the two-builder lab prove?
It proved separate BuildKit builder identities, separate cache accounting, and separate build outputs under a trusted controller. It did not prove that host Docker daemon access would be safe for untrusted job code.
Why use local build outputs instead of loading both results into the Engine image store?
It avoids creating extra shared Engine image state and keeps the evidence focused on builder/cache separation.
Why is rootless DinD not the mandatory runnable path here?
Docker documents that the containerized rootless DinD image still requires privileged outer execution for several sandbox/mount requirements; this chapter avoids teaching that shortcut as the default.
What is a safer control-plane split for untrusted build code?
Provision the worker from trusted infrastructure and expose only an authenticated build endpoint with scoped credentials/cache, rather than the host Engine API.
What cleanup operation is required for Docker state?
Only removal of the two explicitly named Buildx builders; no broad prune is needed.
Official references and version notes
Lab baseline: 2026-09-22. The mandatory path uses
local Buildx docker-container builders as an isolation
model and deliberately avoids socket mounts and privileged nested
daemons. It is a build-state isolation exercise, not a claim that
docker-container removes controller daemon authority.
- Docker Docs — Docker Engine security — daemon-control implications and why only trusted users should control the daemon.
- Docker Docs — Linux post-installation steps — Docker group access grants root-level privileges.
- Docker Docs — Protect the Docker daemon socket — SSH and TLS patterns for remote control.
- Docker Docs — Rootless mode — rootless daemon threat reduction and user-namespace boundary.
- Docker Docs — Rootless mode tips — current rootless Docker-in-Docker behavior and caveats.
- Docker Docs — Buildx remote driver — connecting Buildx to externally managed BuildKit over Unix/TCP/TLS endpoints.
-
Docker Docs —
docker buildx create— builder drivers, endpoints, and isolated builder instances. - Docker Docs — BuildKit — BuildKit architecture and remote-builder use.
- Moby BuildKit — Rootless mode — rootless BuildKit prerequisites, RootlessKit, and limitations.
- GitLab Docs — Use Docker to build Docker images — current socket-binding and DinD tradeoffs.
- GitLab Docs — Build Docker images with BuildKit — rootless BuildKit as a daemon-independent CI option.
- GitLab Runner security — privileged/shared-runner risks and trust-boundary guidance.
- GitHub Actions — Secure use reference — self-hosted runner compromise risk and ephemeral-isolation guidance.
- Docker Engine 29 release notes — current Engine 29.8.1 baseline and recent security/runtime changes.
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.