Checkpoint Lab — Docker Socket Security, Docker-in-Docker, Socket Mounting, Build Services, and CI Isolation Tradeoffs
Produce a CI isolation decision record and run a disposable two-job build isolation exercise that proves scoped builder/cache/output identities, then document the stronger boundary required for truly untrusted repositories.
Learning objectives
- Create a CI isolation decision record for trusted and untrusted repository workflows.
- Implement a disposable two-job build-state isolation exercise without handing either simulated job a Docker socket.
- Verify exact builder identities, separate output roots and separate cache accounting.
- Document what the simulation proves and what requires a stronger per-job/remote/rootless daemon boundary.
- Produce an evidence packet and exact cleanup record suitable for operational review.
1. Checkpoint scope and honest claim
The checkpoint has two deliverables:
- a decision record that selects a CI architecture for trusted and untrusted repositories; and
- a local isolation exercise using two Buildx builder instances provisioned by a trusted controller.
The local exercise proves builder/cache/output scoping. It does not falsely claim that a job with host Engine API access is isolated. Your decision record must state that truly untrusted repository code should receive an ephemeral/isolated worker or a narrow rootless/remote builder endpoint instead of the host Docker socket.
2. Create checkpoint workspace and capture assumptions
set -eu
LAB=dca35-checkpoint
EVIDENCE="$LAB/evidence"
mkdir -p "$LAB/src" "$LAB/out-trusted" "$LAB/out-untrusted" "$EVIDENCE"
date -u +%Y-%m-%dT%H:%M:%SZ | tee "$EVIDENCE/time-start.txt"
docker context show | tee "$EVIDENCE/context.txt"
docker version | tee "$EVIDENCE/docker-version.txt"
docker info | tee "$EVIDENCE/docker-info.txt"
docker compose version | tee "$EVIDENCE/compose-version.txt" || true
docker buildx version | tee "$EVIDENCE/buildx-version.txt"
docker buildx ls | tee "$EVIDENCE/builders-before.txt"
3. Write the CI isolation decision record before building
# dca35-checkpoint/decision-record.txt
Trusted release repository/job:
trust basis = protected branch/tag + reviewed workflow
required operation = build + push/sign in later release stage
proposed worker = dedicated or ephemeral trusted builder
host Docker socket = not required by repository code
credentials = short-lived scoped release credentials
Untrusted PR/fork job:
trust basis = attacker-controlled code is possible
required operation = build/test only
proposed worker = ephemeral hosted runner OR isolated rootless/remote BuildKit
host Docker socket = prohibited
privileged nested Docker = avoid by default
credentials = no release secrets; minimal fetch token only if required
Shared policy:
cleanup = exact job/builder identity only
cache = per trust domain / authenticated export
evidence = source revision + builder identity + logs + digest/output
4. Predict state changes before executing them
Prediction 1:
dca35-trusted and dca35-untrusted will resolve to two distinct BuildKit workers.
Prediction 2:
each builder will report its own cache accounting after one build.
Prediction 3:
outputs will be exported only to their own local directories and will contain different job markers.
Prediction 4:
cleanup will remove only the two named builders; no image/container/volume prune is needed.
5. Create deterministic source
cat > dca35-checkpoint/src/Dockerfile <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.22
ARG JOB_CLASS
RUN printf 'job-class=%s
' "$JOB_CLASS" > /result.txt
FROM scratch
COPY --from=0 /result.txt /result.txt
EOF
sha256sum dca35-checkpoint/src/Dockerfile | tee "$EVIDENCE/dockerfile.sha256"
6. Provision two builder identities from the trusted controller
docker buildx create --name dca35-trusted --driver docker-container --bootstrap
docker buildx create --name dca35-untrusted --driver docker-container --bootstrap
docker buildx inspect dca35-trusted | tee "$EVIDENCE/trusted-builder.txt"
docker buildx inspect dca35-untrusted | tee "$EVIDENCE/untrusted-builder.txt"
These builders are disposable lab workers. In a real untrusted CI design, provisioning should be performed outside repository-controlled code, and the job should receive only its assigned builder endpoint.
7. Run the two builds with no job socket mount
docker buildx build \
--builder dca35-trusted \
--build-arg JOB_CLASS=trusted \
--progress=plain \
--output type=local,dest=dca35-checkpoint/out-trusted dca35-checkpoint/src | tee "$EVIDENCE/build-trusted.log"
docker buildx build \
--builder dca35-untrusted \
--build-arg JOB_CLASS=untrusted \
--progress=plain \
--output type=local,dest=dca35-checkpoint/out-untrusted dca35-checkpoint/src | tee "$EVIDENCE/build-untrusted.log"
8. Verify output separation
cat dca35-checkpoint/out-trusted/result.txt | tee "$EVIDENCE/output-trusted.txt"
cat dca35-checkpoint/out-untrusted/result.txt | tee "$EVIDENCE/output-untrusted.txt"
test "$(cat dca35-checkpoint/out-trusted/result.txt)" = 'job-class=trusted'
test "$(cat dca35-checkpoint/out-untrusted/result.txt)" = 'job-class=untrusted'
9. Verify builder and cache separation independently
docker buildx inspect dca35-trusted | tee "$EVIDENCE/trusted-builder-after.txt"
docker buildx inspect dca35-untrusted | tee "$EVIDENCE/untrusted-builder-after.txt"
docker buildx du --builder dca35-trusted | tee "$EVIDENCE/trusted-cache.txt"
docker buildx du --builder dca35-untrusted | tee "$EVIDENCE/untrusted-cache.txt"
Each report is queried against a different builder identity. This demonstrates separate BuildKit state namespaces in the lab. Because the trusted controller can query both builders, the evidence also demonstrates why the controller itself is outside the simulated job boundary.
10. Credential-isolation proof by absence
No registry login, cloud credential, SSH key, BuildKit secret, signing key, or Docker config directory is injected into either build. Record that absence as part of the checkpoint. In a production release workflow, credential evidence should state provider, scope, TTL, recipient worker and revocation event—never the secret value itself.
grep -R -nE '(BEGIN .*PRIVATE KEY|password=|token=|secret=)' dca35-checkpoint/src dca35-checkpoint/out-trusted dca35-checkpoint/out-untrusted > "$EVIDENCE/credential-pattern-scan.txt" || true
cat "$EVIDENCE/credential-pattern-scan.txt"
11. Stronger production proof required for untrusted repositories
For an actual untrusted repository, add evidence from the isolation mechanism you chose:
- Ephemeral VM/hosted runner: worker instance identity and destruction event after the job.
- Rootless BuildKit: non-root BuildKit process identity, user namespace/root, unique worker/cache root, and endpoint available only to the assigned job.
- Remote BuildKit: mTLS/client identity, worker tenancy, cache namespace, network policy and short-lived credential scope.
The key proof is that job A cannot address job B’s control endpoint or retained credentials—not merely that job A chooses not to do so.
12. Exact cleanup
docker buildx rm dca35-trusted dca35-untrusted
docker buildx ls | tee "$EVIDENCE/builders-after-cleanup.txt"
date -u +%Y-%m-%dT%H:%M:%SZ | tee "$EVIDENCE/time-final.txt"
Keep the checkpoint directory until review is complete. Then remove
only dca35-checkpoint. Do not perform system-wide
Docker cleanup.
13. Required evidence packet
| Evidence family | Required evidence |
|---|---|
| Trust decision | trusted/untrusted classification and decision record |
| Host/client | UTC timestamps; Docker/Compose/Buildx versions; platform/context |
| Control plane | context/daemon identity used by trusted controller; statement that jobs did not receive socket mounts |
| Builder identity |
before/after buildx inspect for both builders
|
| Source | Dockerfile checksum and human-readable Alpine base identity |
| Outputs | trusted and untrusted result files with independent assertions |
| Cache | builder-specific buildx du reports |
| Credentials | statement of no credential injection in lab; pattern scan; production scope design |
| Cleanup | exact two builder names removed; no broad prune |
| Limitations | lab demonstrates build-state separation, not full host isolation; production untrusted jobs require inaccessible peer control endpoints |
14. What Chapter 35 adds to the production operating model
You can now evaluate Docker-in-CI designs by the authority they grant rather than by whether commands happen to run inside containers. A secure operating model classifies job trust first, keeps host daemon authority in trusted infrastructure, scopes build workers/cache/credentials to that trust domain, uses ephemeral or rootless/remote build services for untrusted code, records endpoint and builder identity, and cleans only exact owned state.
Knowledge check
What does this checkpoint intentionally prove, and what does it not prove?
It proves separate Buildx builder identities, outputs and cache accounting under a trusted controller. It does not prove that code with access to the controller’s host Docker daemon would be isolated.
For a public pull request, what evidence is stronger than “we use a separate builder name”?
Evidence that the job cannot address the host/peer control endpoint at all: ephemeral worker destruction, rootless worker identity and unique root, or authenticated remote-builder tenancy.
Why is credential absence useful evidence in this synthetic checkpoint?
It establishes that the exercise itself has no hidden registry/cloud/signing authority and clarifies which credentials a production design would need to scope separately.
Why are two builder-specific cache reports required?
They independently bind cache state to each builder instead of assuming isolation from builder names alone.
What is the correct cleanup boundary?
The exact two named builders and the exact local checkpoint directory; no system-wide Docker prune.
Official references and version notes
Checkpoint baseline: 2026-09-22. The runnable exercise is deliberately compatible with a normal local Docker/Buildx installation and does not require a commercial CI platform, host-socket mounting into jobs, or privileged nested containers. Production untrusted-code isolation must be proven at the runner/builder endpoint boundary.
- 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.