09 - Docker Security
Attack Surface
Docker introduces several attack vectors:
- Container breakout -- escaping container isolation to host
- Image vulnerabilities -- outdated packages, malware
- Misconfiguration -- privileged mode, exposed socket
- Supply chain -- compromised base images
- Network exposure -- unnecessary port exposure
- 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 . /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
bashdocker 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 \ GITHUB_TOKEN=$(cat /run/secrets/github_token) && \ npm install --registry=https://npm.pkg.github.com/
bashdocker 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
--privilegedflag -
--cap-drop=ALLwith minimal--cap-add -
--security-opt=no-new-privileges - Image signing and verification
- Log driver configured with rotation
FAANG Interview Angle
Common questions:
- "How do you secure Docker containers in production?"
- "What's a container breakout and how do you prevent it?"
- "How do you handle secrets with Docker?"
- "What's the risk of mounting the Docker socket?"
- "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
- Docker Security
- Docker Scout
- Rootless Mode
- Seccomp Profiles
- Docker Content Trust
- CIS Docker Benchmark