10 - Security Basics¶
What this session is¶
About 45 minutes. Five things that make any container deployment notably safer. Not exhaustive - just the high-leverage moves.
1. Don't run as root¶
By default, the container's process runs as root inside the container. If an attacker escapes the container (rare but happens), they're root on your host (unless user namespaces are configured, which Docker doesn't do by default).
Always switch to a non-root user:
Or use a numeric UID:
Some images do this for you (e.g. nginx switches to nginx user, postgres to postgres). Many don't. Verify with:
If uid=0(root), you're running as root. Fix.
2. Read-only root filesystem¶
Most apps don't need to write to their root filesystem. Make it read-only:
Anything that tries to write outside the explicit tmpfs mounts (or other mounted volumes) fails. This neutralizes a class of attacks where malware drops a binary into /usr/bin or similar.
For specific writable areas (a cache directory, /var/log), add --tmpfs PATH (in-memory) or -v VOL:PATH (named volume).
In compose:
3. Drop unnecessary capabilities¶
Linux capabilities split root's powers into ~40 distinct privileges. By default Docker grants a subset (~14). For most apps, you can drop them all:
If the app needs one specific capability (e.g. binding to port < 1024):
Most modern apps need no capabilities - they don't try to do privileged things. Drop ALL by default; add only when needed and justified.
In compose:
4. --security-opt no-new-privileges¶
Prevents the container from gaining privileges via setuid binaries (e.g. sudo, su):
Almost always safe to add. Pair with non-root user for defense-in-depth.
In compose:
5. Don't bake secrets into images¶
Never COPY a .env file. Never ENV PASSWORD=hunter2. The secret is in a layer forever - even if you delete it in a later layer, docker history reveals it.
Options:
- Env vars at runtime:
-e PASSWORD=...or--env-file secrets.envatdocker run. Don't commit the env file. - Mounted secret files: mount a directory or file with secrets at runtime.
- Secret managers: Docker Swarm secrets, Kubernetes secrets, HashiCorp Vault, AWS Secrets Manager, etc. For non-toy deployments.
For local dev, env files in .gitignore. For production, a real secret manager.
Quick audit: is this image clean?¶
Run an image vulnerability scanner:
# Trivy (Aqua Security - free, open source):
docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image myapp:1.0
# Grype (Anchore):
grype myapp:1.0
Both produce a list of known CVEs in the image's packages. Triage: fix high/critical first. Many are inherited from the base image; updating the base often fixes batches.
docker scout (built into modern Docker) is another option:
Run scanners as part of your CI. Don't ship images with known critical CVEs without explicit acknowledgement.
Putting it together: a hardened Dockerfile¶
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
# Non-root user with a known UID
RUN useradd --create-home --shell /bin/bash --uid 1001 app && \
chown -R app:app /app
USER 1001:1001
EXPOSE 8000
# Don't put secrets here - pass at runtime.
CMD ["python", "app.py"]
Run it hardened:
docker run -d --rm \
--name app \
-p 8000:8000 \
--read-only \
--tmpfs /tmp \
--cap-drop=ALL \
--security-opt=no-new-privileges \
-e DATABASE_URL=postgres://... \
myapp:1.0
Compose form:
services:
app:
image: myapp:1.0
ports: ["8000:8000"]
read_only: true
tmpfs: [/tmp]
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
environment:
DATABASE_URL: ${DATABASE_URL}
The big footguns¶
Things to never do (or do only with very deliberate awareness):
--privileged- disables most isolation. Equivalent to "this container IS the host." Used by Docker-in-Docker and a few other special cases; almost never appropriate.-v /:/hostor-v /var/run/docker.sock:...- mounting host paths into the container. Especially the Docker socket - anyone in the container can issue Docker commands, which means root on the host.- Running with
--user 0when the image has a non-root default. You're undoing the safety. --network=hostwith untrusted images. The container has full access to your host's network stack.
What you're not doing here¶
This page is the basics. Real container security also includes: pod-level policies (in Kubernetes), seccomp profiles, AppArmor/SELinux, signed images, supply-chain attestations, runtime detection (Falco). Those are advanced topics; the "Container Internals" senior reference path covers them.
For a first deployment, the five basics on this page get you 80% of the value.
Exercise¶
-
Identify which images run as root:
-
Run an app hardened:
(nginx needs writabledocker run -d --rm \ --name web \ -p 8090:80 \ --read-only \ --tmpfs /var/cache/nginx \ --tmpfs /var/run \ --cap-drop=ALL \ --cap-add=NET_BIND_SERVICE \ --security-opt=no-new-privileges \ nginx curl -s http://localhost:8090 | head docker stop web/var/cache/nginxand/var/run, hence the tmpfs mounts.) -
Scan an image:
Read the report. Note how many CVEs - and how many are critical vs informational.
What you might wonder¶
"Should I rebase my images regularly?" Yes. Rebuild your images periodically with the same Dockerfile - the base image you depend on receives security updates that flow into your image only when you rebuild. Automate via CI (rebuild weekly, run image scans, alert on new vulns).
"What about supply-chain attacks?"
Pin base images by digest (FROM nginx@sha256:abc...) not tag. Use signed images. Verify via cosign / sigstore. Way beyond beginner; mentioned for awareness.
"What's seccomp?" A Linux kernel feature that filters which syscalls a process can make. Docker has a default seccomp profile that blocks a handful of dangerous syscalls. You can customize. Beyond beginner.
Done¶
- Run containers as non-root.
- Make root filesystems read-only.
- Drop unneeded Linux capabilities.
- Use
--security-opt=no-new-privileges. - Keep secrets out of images.
- Run image scanners.