Docker Security

Docker Intermediate/Advanced
Chapter 6 ยท Docker Security

๐Ÿ”’ Docker Security

Chapters 4โ€“5 secured the network and the data. This chapter hardens the container itself โ€” who it runs as, what it can modify at runtime, whether its own base image is carrying known vulnerabilities, and whether a secret was ever accidentally baked into it.

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.

FROM node:18-slim RUN useradd --create-home appuser USER appuser WORKDIR /home/appuser/app COPY --chown=appuser:appuser . . CMD ["node", "server.js"]

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.

docker run --read-only --tmpfs /tmp myapp

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.

trivy image myapp:latest

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
ENV DATABASE_PASSWORD=Sup3rSecret
Good: Injected at Runtime
docker run -e DATABASE_PASSWORD="$DB_PASSWORD" myapp # or a secrets manager / orchestrator-native secret
Security MeasureProtects Against
Non-root userEscalated damage from a container-escape vulnerability
Read-only filesystemA compromised process modifying code or persisting malware on disk
Image vulnerability scanningKnown, unpatched CVEs in the base image's own packages
No baked-in credentialsA 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.

โ†’ Solution

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."

โ†’ Solution

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.

โ†’ Solution

โš ๏ธ Gotcha: A "Deleted" Secret Is Still Sitting in an Earlier Layer

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.