09 - Docker Security

Attack Surface

Docker introduces several attack vectors:

  1. Container breakout -- escaping container isolation to host
  2. Image vulnerabilities -- outdated packages, malware
  3. Misconfiguration -- privileged mode, exposed socket
  4. Supply chain -- compromised base images
  5. Network exposure -- unnecessary port exposure
  6. Secrets leakage -- credentials in images or logs

Principle of Least Privilege

Non-Root Containers

dockerfile
# Create a non-root user RUN addgroup --system --gid 1001 app && \ adduser --system --uid 1001 --ingroup app app # Set ownership COPY --chown=app:app . /app # Switch to non-root USER app # Verify # docker exec mycontainer whoami → app # docker exec mycontainer id → uid=1001(app) gid=1001(app)

Drop Linux Capabilities

bash
# By default, Docker drops many capabilities but keeps some # Drop ALL and add back only what's needed docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myapp # Common capabilities: # NET_BIND_SERVICE - Bind to ports < 1024 # CHOWN - Change file ownership # SETUID/SETGID - Set user/group ID # SYS_PTRACE - Debug processes (needed for some monitoring)

Read-Only Root Filesystem

bash
docker run --read-only \ --tmpfs /tmp \ --tmpfs /var/run \ -v data:/app/data \ myapp

No New Privileges

bash
# Prevent privilege escalation via setuid/setgid docker run --security-opt=no-new-privileges myapp

NEVER Do This

Privileged Mode

bash
# DANGEROUS: Full access to host devices and capabilities docker run --privileged myapp # Only for: Docker-in-Docker, hardware access, kernel modules # Instead, grant specific capabilities: docker run --cap-add=SYS_ADMIN myapp

Mounting Docker Socket

bash
# DANGEROUS: Equivalent to root access on host docker run -v /var/run/docker.sock:/var/run/docker.sock myapp # Container can: create privileged containers, read any volume, etc. # If you must (CI/CD), use rootless Docker or restricted socket proxies

Running as Root with Host Network

bash
# DANGEROUS: Full network access + root docker run --network host --privileged myapp

Image Security

Scan for Vulnerabilities

bash
# Docker Scout (built-in) docker scout cves nginx:latest docker scout recommendations nginx:latest # Trivy trivy image --severity HIGH,CRITICAL myapp:latest # In CI pipeline trivy image --exit-code 1 --severity CRITICAL myapp:latest

Use Trusted Base Images

dockerfile
# Official images from Docker Hub (verified publisher) FROM python:3.12-slim # Google Distroless (minimal, no shell) FROM gcr.io/distroless/python3-debian12 # Chainguard images (signed, minimal) FROM cgr.dev/chainguard/python:latest

Sign and Verify Images

bash
# Docker Content Trust (DCT) export DOCKER_CONTENT_TRUST=1 docker push myregistry/myapp:v1 # Signs the image docker pull myregistry/myapp:v1 # Verifies signature # Cosign (modern alternative by Sigstore) cosign sign myregistry/myapp:v1 cosign verify myregistry/myapp:v1

Pin Image Digests

dockerfile
# Tags can be moved; digests are immutable FROM node:20-alpine@sha256:1a526b97cace6b4006eb35cb51949db8f94f3ccce50fdbf2c1598b504b63f8c0

Secret Management

BuildKit Secrets (Build Time)

dockerfile
# Secret is mounted temporarily, NOT stored in any layer RUN --mount=type=secret,id=github_token \ GITHUB_TOKEN=$(cat /run/secrets/github_token) && \ npm install --registry=https://npm.pkg.github.com/
bash
docker build --secret id=github_token,src=.github_token .

Docker Secrets (Swarm Mode)

bash
# Create a secret echo "my-password" | docker secret create db_password - # Use in service docker service create --name api \ --secret db_password \ myapp # Secret available at /run/secrets/db_password inside container

Runtime Secrets

bash
# Via environment variables (visible in inspect) docker run -e DB_PASSWORD=secret myapp # Via mounted files (more secure) docker run -v ./secrets/db_password:/run/secrets/db_password:ro myapp # Via external secret managers # HashiCorp Vault, AWS Secrets Manager, etc.

Network Security

Limit Network Exposure

bash
# Only expose to localhost (not all interfaces) docker run -p 127.0.0.1:5432:5432 postgres:16 # BAD: Exposed to all interfaces docker run -p 5432:5432 postgres:16

Use Custom Networks for Isolation

yaml
# compose.yml services: web: networks: - frontend api: networks: - frontend - backend db: networks: - backend # Not accessible from frontend networks: frontend: backend: internal: true # No external internet access

Inter-Container Communication

bash
# Disable ICC on default bridge (legacy) # /etc/docker/daemon.json { "icc": false # Containers can't talk unless explicitly linked } # Better: use user-defined networks (isolated by default)

Seccomp and AppArmor

Seccomp (System Call Filtering)

bash
# Docker applies a default seccomp profile that blocks ~44 syscalls # Verify: docker inspect --format='{{.HostConfig.SecurityOpt}}' mycontainer # Custom seccomp profile docker run --security-opt seccomp=./my-profile.json myapp # Disable (DANGEROUS, allows all syscalls) docker run --security-opt seccomp=unconfined myapp

Example custom seccomp profile:

json
{ "defaultAction": "SCMP_ACT_ERRNO", "syscalls": [ { "names": ["read", "write", "open", "close", "stat", "fstat", "mmap", "mprotect", "munmap", "brk", "futex", "getpid", "exit_group"], "action": "SCMP_ACT_ALLOW" } ] }

AppArmor

bash
# Docker applies a default AppArmor profile # Custom profile docker run --security-opt apparmor=my-profile myapp

Rootless Docker

Run the entire Docker daemon without root privileges:

bash
# Install rootless Docker dockerd-rootless-setuptool.sh install # Now Docker runs as your user # No root access even if container escapes # Port mapping below 1024 requires slirp4netns or rootlesskit

Security Scanning in CI/CD

yaml
# GitHub Actions example name: Security Scan on: push jobs: scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build image run: docker build -t myapp:ci . - name: Trivy scan uses: aquasecurity/trivy-action@master with: image-ref: myapp:ci severity: CRITICAL,HIGH exit-code: 1 - name: Dockle lint run: | docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ goodwithtech/dockle myapp:ci

Security Checklist

  • Non-root user in Dockerfile
  • Specific image tags (not :latest)
  • .dockerignore excludes secrets
  • No secrets in ENV or COPY
  • Read-only root filesystem where possible
  • Minimal base image (alpine/distroless)
  • Regular vulnerability scanning
  • Resource limits set (memory, CPU, PIDs)
  • Network segmentation (custom networks)
  • Docker socket NOT mounted in containers
  • No --privileged flag
  • --cap-drop=ALL with minimal --cap-add
  • --security-opt=no-new-privileges
  • Image signing and verification
  • Log driver configured with rotation

FAANG Interview Angle

Common questions:

  1. "How do you secure Docker containers in production?"
  2. "What's a container breakout and how do you prevent it?"
  3. "How do you handle secrets with Docker?"
  4. "What's the risk of mounting the Docker socket?"
  5. "How would you implement image security in CI/CD?"

Key answers:

  • Non-root user, read-only fs, dropped capabilities, resource limits
  • Container breakout exploits kernel vulnerabilities; prevent with patches, seccomp, non-root
  • BuildKit secrets for build, mounted files or vault for runtime
  • Docker socket = root access; use rootless Docker or socket proxies
  • Automated scanning (Trivy/Scout), image signing, digest pinning

Official Links