Docker Lesson 32 – Docker Security Basics | Dataplexa
Section III · Lesson 32

Docker Security Basics

A security researcher once pulled a popular Docker image from a public registry, ran it, and found an SSH backdoor baked into one of the layers. The image had over 10 million pulls. The maintainer had been compromised months earlier. Container security isn't paranoia — it's the acknowledgment that every image you pull is code you're running on your infrastructure, written by someone you've never met.

This lesson covers the security mistakes hiding in the average Docker setup — and the fixes that take minutes to implement but meaningfully reduce your attack surface. Not theory. Not compliance checkbox security. Practical changes that make a real difference.

The Unsecured vs Secured Setup

Typical "it works" setup

  • Running as root inside the container
  • Base image pulled months ago, never updated
  • All capabilities granted to the process
  • No resource limits — one container starves the host
  • Secrets baked into image layers
  • Writable root filesystem
  • No vulnerability scanning on images

Production-hardened setup

  • Non-root user with minimal permissions
  • Base image pinned and regularly rebuilt
  • Capabilities dropped — only what's needed
  • CPU and memory limits enforced
  • Secrets passed at runtime, never in image
  • Read-only root filesystem where possible
  • Automated scanning in the CI pipeline

Security Fix 1 — Run as a Non-Root User

Containers run as root by default. If an attacker exploits a vulnerability in your application and gets code execution, they land as root inside the container — able to install tools, read any file, modify any configuration, and in some configurations escalate to the host. The fix is one instruction.

FROM node:18-alpine

WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY . .

RUN addgroup -S appgroup && \
    adduser -S appuser -G appgroup && \
    chown -R appuser:appgroup /app
# System group and user — no password, no login shell
# Transfer /app ownership so appuser can read its own files

USER appuser
# All subsequent instructions and the container process run as appuser
# uid ~100, not uid 0 (root)

EXPOSE 3000
CMD ["node", "server.js"]
docker exec payment-api whoami
appuser

docker exec payment-api id
uid=100(appuser) gid=101(appgroup) groups=101(appgroup)

# Attempt a privileged operation
docker exec payment-api apt-get install curl
/bin/sh: apt-get: Permission denied

What just happened?

The process runs as uid 100 — not uid 0. An attacker who gets code execution lands as appuser and immediately hits a wall — cannot install packages, cannot read files outside /app, cannot touch system files, cannot interact with Docker directly. The blast radius of any vulnerability is dramatically contained by one Dockerfile instruction that takes 30 seconds to add. It's the highest-impact security improvement available to any container image.

Security Fix 2 — Read-Only Root Filesystem

A writable root filesystem lets an attacker install persistence tools, modify binaries, and generally make a breach much worse. Most web services never need to write to their own container filesystem. Make it read-only — then provide writable in-memory paths for the temporary files the app legitimately needs.

docker run -d \
  --name payment-api \
  --read-only \
  --tmpfs /tmp \
  --tmpfs /app/tmp \
  -p 3000:3000 \
  payment-api:v1.2.0
# --read-only       → root filesystem is read-only — no writes anywhere
# --tmpfs /tmp      → in-memory writable mount at /tmp
#                     disappears on container stop, never touches disk
# --tmpfs /app/tmp  → additional tmpfs for app-specific temp needs
# Attacker attempts to write a backdoor:
docker exec payment-api touch /usr/local/bin/backdoor
touch: /usr/local/bin/backdoor: Read-only file system

# Legitimate temp file still works:
docker exec payment-api touch /tmp/session-cache.tmp
(success)

What just happened?

Writing a backdoor binary to /usr/local/bin failed instantly. The --tmpfs mounts give the app legitimate writable space without compromising the read-only constraint. Since tmpfs lives entirely in RAM and disappears on container stop, any files an attacker writes there leave no trace after the container is restarted. Combined with the non-root user, this stops the most common post-exploitation persistence techniques completely.

Security Fix 3 — Drop Linux Capabilities

Linux capabilities are fine-grained privileges that determine what a process can do beyond normal user operations. Even non-root containers inherit a default set that includes privileges most applications will never use — and that attackers would love to exploit. The correct posture: drop everything, then add back only what's proven necessary.

docker run -d \
  --name payment-api \
  --cap-drop ALL \
  --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges \
  -p 3000:3000 \
  payment-api:v1.2.0
# --cap-drop ALL            → strip every Linux capability from the container
# --cap-add NET_BIND_SERVICE → restore only the ability to bind ports below 1024
#                             only needed if your app directly binds port 80/443
# --security-opt no-new-privileges → blocks escalation through setuid binaries

Common Linux capabilities — what they allow

SYS_ADMIN Mount filesystems, configure namespaces, set hostname. Effectively root on the host. Drop immediately.
NET_ADMIN Configure network interfaces, routing, firewalls. Only needed for network tools — drop for all web services.
SYS_PTRACE Trace and debug other processes. An attacker with this can read memory of every process on the system.
NET_BIND_SERVICE Bind to privileged ports below 1024. Add only if the app serves directly on port 80 or 443.

Security Fix 4 — Scan Images for Vulnerabilities

Every layer in your image is built on software that contains bugs. Some of those bugs are security vulnerabilities with published CVEs. The node:18-alpine image you pulled last month may have critical vulnerabilities discovered and published last week. Scanning tells you before an attacker does.

# Docker Scout — built into Docker Desktop and Docker CLI
docker scout cves payment-api:v1.2.0

# Trivy — open-source, the standard in CI pipelines
trivy image payment-api:v1.2.0

# Run Trivy as a container — no local install needed
docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy:latest image payment-api:v1.2.0

# In CI — fail the build on critical CVEs
trivy image --exit-code 1 --severity CRITICAL payment-api:v1.2.0
# Non-zero exit code fails the CI step → image never reaches the registry
payment-api:v1.2.0 (alpine 3.18.4)
Total: 3 (CRITICAL: 1, HIGH: 2)

Library     Vulnerability   Severity  Installed   Fixed
openssl     CVE-2023-5363   CRITICAL  3.1.3-r0    3.1.4-r0
libcrypto   CVE-2023-4807   HIGH      3.1.3-r0    3.1.4-r0
libssl      CVE-2023-3817   HIGH      3.1.3-r0    3.1.4-r0

Fix: Update base image → node:18-alpine3.19 (includes openssl 3.1.4-r0)

What just happened?

Three vulnerabilities found — one critical, two high — all in OpenSSL, which was shipped with the Alpine base image. The application code is completely clean. The CVEs were published weeks after the image was last rebuilt. The fix: one line change in the Dockerfile, one rebuild. Five minutes. Without a scanner in CI, this image would have shipped to production and sat there undetected, fully exploitable by anyone who read the CVE advisory.

Security Fix 5 — Never Mount the Docker Socket

Mounting the Docker socket (/var/run/docker.sock) into a container gives that container complete control over the Docker Daemon — it can start containers, pull any image, and mount any host directory. It is full root access to the host, dressed up as a container operation.

The Docker Socket Is a Root Backdoor

A container with socket access can run docker run -v /:/host alpine chroot /host — mounting the entire host filesystem and dropping into a root shell on the host machine. Any vulnerability in a socket-mounted container is a full host compromise. Use Kaniko or Buildah for CI image builds — they build OCI images without requiring the Docker Daemon, so the socket never needs to be exposed.

# NEVER do this in production:
docker run -v /var/run/docker.sock:/var/run/docker.sock some-ci-tool

# The host escape from inside the compromised container:
docker run -it --rm -v /:/host alpine chroot /host
# → root shell on the HOST operating system

# Safe alternative — Kaniko builds images without the Daemon:
docker run \
  -v /workspace:/workspace \
  gcr.io/kaniko-project/executor:latest \
  --context=/workspace \
  --destination=acmecorp/payment-api:latest
# No socket needed — builds and pushes entirely within the container

The Security Checklist

Before any container goes to production

Non-root USER in Dockerfile — never run as uid 0
Read-only filesystem--read-only + --tmpfs for writable paths
Drop all capabilities--cap-drop ALL, add back only what's needed
no-new-privileges — blocks escalation through setuid binaries
Vulnerability scan in CI — fail on CRITICAL before image reaches registry
No secrets in images — all sensitive values passed at runtime
No Docker socket mount — never expose /var/run/docker.sock
Resource limits — CPU and memory caps prevent one container from starving the host
Internal networksinternal: true keeps databases and caches off the internet

Teacher's Note

Start with the non-root user and the vulnerability scan in CI. Those two alone put you ahead of the vast majority of Docker deployments running in production right now. Add the rest progressively.

Practice Questions

1. To mount a container's root filesystem as read-only — preventing any process inside from writing files to it — which docker run flag do you use?



2. To strip every Linux capability from a container — so only what is explicitly added back is permitted — which flag do you use?



3. Mounting this specific file into a container gives it complete control over the Docker Daemon — effectively root access to the host. What is the file path?



Quiz

1. A CI tool mounts /var/run/docker.sock into a container and an attacker exploits a vulnerability in it. What is the worst-case outcome?


2. A container runs with --read-only but crashes because it needs to write temp files to /tmp. The correct fix is:


3. A vulnerability scan on a three-week-old image reports a CRITICAL CVE in OpenSSL. The application code hasn't changed. Cause and fix:


Up Next · Lesson 33

Secrets Management

Security basics covered — now the harder problem: how do you get database passwords, API keys, and TLS certificates into containers securely at scale, without ever touching a .env file on a production server?