Lab 03 — Run, Build, and Inspect a Container (and See Where Isolation Ends)¶
Variant D · skill-first, one light predict. ← Back to the module concept
Setup¶
This is a reference lab — it ships a one-command environment in the companion
plaintext-labs repo. It builds and runs a
small demo web service and walks the container lifecycle for you, then you redo it by hand.
git clone https://github.com/plaintext-security/plaintext-labs.git
cd plaintext-labs/foundations/03-docker
make demo # build + run the sample app, publish a port, inspect the running user
make clean # remove the demo image when done
Docker must be installed on your host (you isolated this in module 02). No compose needed — the demo uses Docker directly.
Only run containers and images you trust or built yourself. This lab uses a small sample app shipped in the repo and stock public base images — nothing here attacks a remote target.
Scenario¶
Get fluent with the container lifecycle you'll use in every later lab — and prove to yourself, with
docker inspect and /proc, the one fact from the module: a container is a host process with a
restricted view, not a sealed machine. You'll watch namespaces and cgroups do the isolating, and watch
the shared kernel set the limit.
Do¶
-
[ ] Throwaway container — run, look, exit. Run an interactive throwaway container from a small base image (e.g.
alpine), look around its filesystem, and exit. Confirm withdocker ps -athat--rmcleaned it up. Goal: the run/inspect/ remove loop is reflexive. -
[ ] Service container — build, publish, reach. Build the sample app's image from its
Dockerfile, run it detached, publish a port to your host, andcurlthe service. Goal: read adocker run -pline and know exactly what it exposed. -
[ ] Predict, then prove: is this a separate machine? Predict: before you look — when you list processes inside the container, will you see your host's processes? Will the container have its own process IDs? Write your guess. Do: exec into the running container and run
ps aux(it sees only its own processes, and your app is PID 1 — a namespace). Then, on the host, runps auxand find that same container process — it's right there in the host's process list with a host PID. Reveal: same process, two views. The container's "PID 1" is just a normal host process wearing a PID namespace. Goal: feel that "isolated" means filtered-view, not separate-computer. -
[ ] See the isolation, and its limit. Inspect the kernel features doing the work:
docker inspect <container>(look at the namespaces, mounts, and anyPrivileged/ capability fields), andcat /sys/fs/cgroup/...ordocker statsto see the cgroup limits. Then confirm the shared kernel directly: rununame -rinside the container and on the host — same kernel version, because it's the same kernel. Goal: name what isolates the container (namespaces + cgroups) and what limits that isolation (the shared kernel). -
[ ] Find the security-relevant default. Check what user the container runs as:
docker run --rm <image> whoami. It'sroot. Note why that matters — root in the container, on a shared kernel, is one missing restriction away from root on the host (the module's case). Then write a tinyDockerfilethat fixes it: pin the base image by tag, add a non-root user, andUSERto it beforeCMD. Build, run, and confirmwhoaminow prints your user. Goal: a Dockerfile you wrote and can defend line by line.
Success criteria — you're done when¶
- [ ] You can run, list, and remove containers and images by hand, and published a port you reached with
curl. - [ ] You showed the same container process from two views (host PID list vs. namespaced
psinside) and can say why it's one process, not two machines. - [ ] You confirmed the shared kernel (
uname -rmatches inside and on the host) and can name namespaces and cgroups as what isolates the container. - [ ] You built and ran your own image from a Dockerfile, and it runs as a non-root user.
- [ ] You scored your step-3 prediction against what you saw, and can state in one sentence why a container is not a sealed machine.
Deliverables¶
docker-notes.md — your fixed Dockerfile, the docker run lines you used, the uname -r output from
inside and host side by side, and one paragraph: what isolates a container, and where that isolation
ends. Commit the notes and the Dockerfile. Do not commit images, container logs, or anything the
containers wrote out.
Automate & own it¶
Required. Turn your manual inspect pass into a small reviewable script — inspect.py (or .sh)
that takes a container or image name and prints the things you checked by hand: the running user,
whether it's privileged, its capabilities, its mounts (flagging any that mount a host path like
/ or /var/run/docker.sock), and the kernel version. In other words, a tiny "is this container box
leaky?" reporter built on docker inspect. Have a model draft it; review every line — confirm it
actually flags a privileged container and a host-root mount (test it against one you deliberately run
that way), and that it doesn't shell out unsafely on the name you pass it. Commit it beside your notes.
AI acceleration¶
Ask a model to generate a Dockerfile for the sample app. Then audit its output against the module's
list: pinned base image (not latest), a non-root USER, no secrets baked into layers, no
--privileged in any run instructions, no broad volume mounts. Models routinely omit the non-root user
and the pinned tag — catching that is the skill. Then point your inspect.py at a container and ask the
model to find a misconfiguration your script misses; if it can, widen the script.
Connects forward¶
This is the substrate for every later lab in the curriculum, and the direct groundwork for the
container & Kubernetes security track, where "removing restrictions on a host process" becomes a real
container escape. Your inspect.py is the seed of a posture check — the same review reflex that, in the
cloud track, audits a generated IAM policy for the over-broad default.
Marketable proof¶
"I'm fluent with containers — I can run, build, publish, and
inspectthem, show the same process from the host and from inside its namespaces, and explain why a container shares the host kernel and is not a security boundary by default. I wrote a small script that flags a privileged or host-mounting container."
Stretch¶
- Run a container as a non-root user with a read-only root filesystem (
--read-only) and dropped capabilities (--cap-drop ALL); note what breaks, what you had to add back, and what that buys you. Expect a surprise: for a clean app like this one (writes nothing, binds a high port) nothing breaks — and that is the finding (deny-all costs you nothing here). To feel the constraint instead of just asserting it, break it on purpose: have the app write under/appand watch--read-onlyreject it, then hand back the minimum with--tmpfs /app. The lesson is the discipline: deny everything, re-grant only the one path the app proves it needs. - Deliberately run one container
--privilegedand one with-v /:/host, pointinspect.pyat both, and from inside, read a file off the real host to feel the hole the module described — on a VM you own and can throw away. - Where isolation actually ended — a real CVE. Read the write-up of
CVE-2019-5736, the 2019
runccontainer-escape flaw: a process inside a container could overwrite the host'sruncbinary and get code execution on the host the next time any container was started — the shared-kernel limit from the module turned into a real breakout, not a thought experiment. (The original disclosure is the openwall advisory; a clear technical walk-through is Palo Alto Unit 42 — Breaking out of Docker via runC or Aqua Security's analysis, with Red Hat's advisory for the vendor view. In two sentences indocker-notes.md, tie it back to the module: which restriction failed, and why "a container is not a security boundary by default" is the exact sentence this CVE proves. (Do not try to reproduce the exploit — read it; the point is connecting the model to a named, real escape.)
Comments
Sign in with GitHub to comment. Choose the type: Feedback (errors or suggestions on this page) · Hints (help for fellow learners — no spoilers) · General (anything else).