Docker Security
๐ Docker Security
Running as Non-Root
Unless told otherwise, a container's main process runs as root inside the container. If an attacker ever manages to escape the application layer into the container's own OS layer (via a vulnerability or misconfiguration), running as root there is a meaningfully worse starting point than a non-privileged user โ root inside the container has far more latitude to do damage.
Root vs. Non-Root Container Process
Root
A container-escape vulnerability, combined with a root process, hands an attacker significantly more capability inside that boundary.
Non-Root
The same escape scenario leaves the attacker with only the limited privileges of an unprivileged user โ the blast radius stays contained.
Read-Only Filesystems
The --read-only flag (or read_only: true in Compose) makes a container's filesystem immutable at runtime, except for explicitly mounted writable paths โ a tmpfs mount (Chapter 5) for anything that genuinely needs to write, like /tmp.
This limits what a compromised process can actually do: it can't modify the application's own code or binaries at runtime, and can't drop a malicious script onto disk to persist itself โ a genuine defense-in-depth measure, not a substitute for fixing the underlying vulnerability.
Scanning Images for Vulnerabilities
A base image bundles OS packages and libraries that can carry their own known, publicly documented CVEs โ exactly the outdated-components risk owasp1-6 and dbsec1-9 already covered, applied here specifically to a container image's own contents. Tools like Trivy (open source) or docker scan check an image's installed packages against known vulnerability databases.
This belongs in the build/CI pipeline (Chapter 7, and the site's pipelines1 course generally) as an automated, recurring check โ not a one-time manual scan run once and forgotten while the base image quietly accumulates newly-disclosed vulnerabilities over time.
Never Baking Credentials Into an Image
A classic, real mistake: hardcoding a secret directly in a Dockerfile's ENV instruction, or COPYing a .env file containing real credentials into the image. This is the exact same discipline pipelines1-5 established for Git commits โ a credential baked into an image is compromised the moment that image exists, regardless of what happens afterward.
Baked-In Secret vs. Runtime Injection
Bad: Hardcoded in the Dockerfile
Good: Injected at Runtime
| Security Measure | Protects Against |
|---|---|
| Non-root user | Escalated damage from a container-escape vulnerability |
| Read-only filesystem | A compromised process modifying code or persisting malware on disk |
| Image vulnerability scanning | Known, unpatched CVEs in the base image's own packages |
| No baked-in credentials | A secret being extractable from the image itself, indefinitely |
๐ป Coding Challenges
Challenge 1: Add a Non-Root User
Rewrite this Dockerfile snippet to run as a non-root user: FROM python:3.12-slim, WORKDIR /app, COPY . ., CMD ["python", "app.py"].
Goal: Practice adding a dedicated user and switching to it before the app runs.
Challenge 2: Explain the Value of Read-Only
Explain what a --read-only container specifically prevents an attacker from doing, even after successfully exploiting a vulnerability in the running application.
Goal: Practice articulating read-only's defense-in-depth value precisely, not just "it's more secure."
Challenge 3: Spot the Credential Leak
A Dockerfile includes ENV API_KEY=abc123 in one layer, then a later layer runs RUN unset API_KEY. Explain why the API key is still compromised despite this.
Goal: Practice connecting Chapter 2's layer-diff mechanics to a real security consequence, not just a size one.
Chapter 2 explained that deleting a file in a later layer doesn't shrink the image, because each layer is a diff and the earlier layer's contents are still there. Applied to secrets, this is a genuine security hazard, not just a size one: an ENV value or a COPY'd .env file set in one layer and "removed" in a later one is still fully extractable โ anyone with the image can run docker history, or simply unpack the image's layers directly, and read the credential straight out of the earlier layer, regardless of what any later instruction did. The only real fix is the same one Git commits need: the secret should never enter any layer in the first place โ inject it at runtime (an environment variable passed to docker run, a secrets manager, an orchestrator-native secret) instead of writing it into the Dockerfile at build time.
๐ฏ What's Next
The next chapter is Docker in CI/CD โ reviewing and building on the CI/CD Pipelines course's own Docker-in-CI chapter, registry push/pull, and tagging strategy.