DOCKER SECURITY HARDENING
CONTAINER USER SECURITY#
# Never run as root USER nonroot # Or in Dockerfile RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser # Run with read-only filesystem docker run --read-only --tmpfs /tmp myimage # Drop all capabilities, add only needed docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myimage # No new privileges docker run --security-opt=no-new-privileges myimage
DOCKERFILE BEST PRACTICES#
# Use minimal base images FROM alpine:3.19 FROM gcr.io/distroless/static-debian12 # Multi-stage builds (don't ship build tools) FROM golang:1.22 AS builder WORKDIR /app COPY . . RUN CGO_ENABLED=0 go build -o /app/server FROM gcr.io/distroless/static COPY --from=builder /app/server /server CMD ["/server"] # Don't store secrets in layers # BAD: COPY secrets.env /app/ # GOOD: Use Docker secrets or env at runtime # Pin versions FROM alpine:3.19.1@sha256:abc123... # Use .dockerignore echo ".git\n.env\nnode_modules\n*.md" > .dockerignore # Scan for vulnerabilities docker scout cves myimage trivy image myimage grype myimage
IMAGE SECURITY#
# Sign images docker trust sign myregistry/myimage:latest # Verify image signatures docker trust inspect myregistry/myimage:latest # Use content trust export DOCKER_CONTENT_TRUST=1 # Scan with Trivy trivy image --severity HIGH,CRITICAL myimage # Scan with Grype grype myimage --only-fixed # Scan with Docker Scout docker scout cves myimage
NETWORK SECURITY#
# Create isolated networks docker network create --driver bridge isolated_net # Don't use host network mode # BAD: docker run --net=host myimage # GOOD: docker run --network=isolated_net myimage # Disable inter-container communication docker network create --driver bridge -o com.docker.network.bridge.enable_icc=false secure_net # Don't expose unnecessary ports # BAD: docker run -p 0.0.0.0:3306:3306 mysql # GOOD: docker run -p 127.0.0.1:3306:3306 mysql
RESOURCE LIMITS#
# Memory limits docker run --memory="512m" --memory-swap="1g" myimage # CPU limits docker run --cpus="1.5" myimage docker run --cpu-shares=512 myimage # PID limits (prevent fork bombs) docker run --pids-limit=100 myimage # Restart policy docker run --restart=on-failure:5 myimage # Ulimits docker run --ulimit nofile=1024:2048 myimage
SECURITY PROFILES#
# AppArmor docker run --security-opt apparmor=docker-default myimage # Seccomp (block dangerous syscalls) docker run --security-opt seccomp=/path/to/seccomp-profile.json myimage # Default seccomp profile blocks ~44 syscalls # Create custom profile for tighter restrictions # SELinux docker run --security-opt label=type:container_t myimage
DOCKER DAEMON SECURITY#
# Enable TLS for remote Docker API
dockerd --tlsverify \
--tlscacert=/certs/ca.pem \
--tlscert=/certs/server-cert.pem \
--tlskey=/certs/server-key.pem \
-H=0.0.0.0:2376
# Don't expose Docker socket to containers
# BAD: docker run -v /var/run/docker.sock:/var/run/docker.sock
# Enable user namespaces
# /etc/docker/daemon.json
{
"userns-remap": "default",
"no-new-privileges": true,
"live-restore": true,
"log-driver": "json-file",
"log-opts": {"max-size": "10m", "max-file": "3"}
}
SECRETS MANAGEMENT#
# Docker Swarm secrets
echo "db_password" | docker secret create db_pass -
docker service create --secret db_pass myimage
# BuildKit secrets (don't leak in layers)
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=mytoken \
cat /run/secrets/mytoken
# Build with secret
docker build --secret id=mytoken,src=./token.txt .
# Use environment variables at runtime (not build time)
docker run -e DB_PASS_FILE=/run/secrets/db_pass myimage
DOCKER COMPOSE SECURITY#
# docker-compose.yml
services:
app:
image: myimage:latest
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
networks:
- backend
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
CIS DOCKER BENCHMARK KEY CHECKS#
# 1. Host configuration
auditctl -w /usr/bin/docker -p wa # Audit Docker binary
auditctl -w /etc/docker -p wa # Audit Docker config
auditctl -w /var/lib/docker -p wa # Audit Docker data
# 2. Check running containers
docker ps --format '{{.Names}}: User={{.Label "user"}} Privileged={{.Label "privileged"}}'
# 3. Find privileged containers
docker inspect --format='{{.HostConfig.Privileged}}' $(docker ps -q)
# 4. Find containers with host PID namespace
docker inspect --format='{{.HostConfig.PidMode}}' $(docker ps -q)
# 5. Check container capabilities
docker inspect --format='{{.HostConfig.CapAdd}}' $(docker ps -q)
CONTAINER ESCAPE PREVENTION#
# Never run privileged containers in production # BAD: docker run --privileged myimage # Don't mount sensitive host paths # BAD: docker run -v /:/host myimage # BAD: docker run -v /etc:/etc myimage # Prevent ptrace docker run --security-opt=no-new-privileges --cap-drop=SYS_PTRACE myimage # Use rootless Docker dockerd-rootless-setuptool.sh install
AUDITING COMMANDS#
# List all containers and their security settings
docker inspect $(docker ps -aq) | jq '.[].HostConfig | {Privileged, CapAdd, CapDrop, SecurityOpt, PidMode, NetworkMode}'
# Check for containers running as root
docker ps -q | xargs -I {} docker exec {} whoami 2>/dev/null
# Find images with known vulnerabilities
trivy image --severity CRITICAL $(docker images -q)
# Check Docker daemon configuration
docker system info --format '{{json .SecurityOptions}}'
DOCKER SECURITY HARDENING CHEATSHEET
=====================================
Source: https://cheatsheet.johlem.net
CONTAINER USER SECURITY
-----------------------
# Never run as root
USER nonroot
# Or in Dockerfile
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
# Run with read-only filesystem
docker run --read-only --tmpfs /tmp myimage
# Drop all capabilities, add only needed
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE myimage
# No new privileges
docker run --security-opt=no-new-privileges myimage
DOCKERFILE BEST PRACTICES
--------------------------
# Use minimal base images
FROM alpine:3.19
FROM gcr.io/distroless/static-debian12
# Multi-stage builds (don't ship build tools)
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server
FROM gcr.io/distroless/static
COPY --from=builder /app/server /server
CMD ["/server"]
# Don't store secrets in layers
# BAD: COPY secrets.env /app/
# GOOD: Use Docker secrets or env at runtime
# Pin versions
FROM alpine:3.19.1@sha256:abc123...
# Use .dockerignore
echo ".git\n.env\nnode_modules\n*.md" > .dockerignore
# Scan for vulnerabilities
docker scout cves myimage
trivy image myimage
grype myimage
IMAGE SECURITY
--------------
# Sign images
docker trust sign myregistry/myimage:latest
# Verify image signatures
docker trust inspect myregistry/myimage:latest
# Use content trust
export DOCKER_CONTENT_TRUST=1
# Scan with Trivy
trivy image --severity HIGH,CRITICAL myimage
# Scan with Grype
grype myimage --only-fixed
# Scan with Docker Scout
docker scout cves myimage
NETWORK SECURITY
----------------
# Create isolated networks
docker network create --driver bridge isolated_net
# Don't use host network mode
# BAD: docker run --net=host myimage
# GOOD: docker run --network=isolated_net myimage
# Disable inter-container communication
docker network create --driver bridge -o com.docker.network.bridge.enable_icc=false secure_net
# Don't expose unnecessary ports
# BAD: docker run -p 0.0.0.0:3306:3306 mysql
# GOOD: docker run -p 127.0.0.1:3306:3306 mysql
RESOURCE LIMITS
---------------
# Memory limits
docker run --memory="512m" --memory-swap="1g" myimage
# CPU limits
docker run --cpus="1.5" myimage
docker run --cpu-shares=512 myimage
# PID limits (prevent fork bombs)
docker run --pids-limit=100 myimage
# Restart policy
docker run --restart=on-failure:5 myimage
# Ulimits
docker run --ulimit nofile=1024:2048 myimage
SECURITY PROFILES
-----------------
# AppArmor
docker run --security-opt apparmor=docker-default myimage
# Seccomp (block dangerous syscalls)
docker run --security-opt seccomp=/path/to/seccomp-profile.json myimage
# Default seccomp profile blocks ~44 syscalls
# Create custom profile for tighter restrictions
# SELinux
docker run --security-opt label=type:container_t myimage
DOCKER DAEMON SECURITY
-----------------------
# Enable TLS for remote Docker API
dockerd --tlsverify \
--tlscacert=/certs/ca.pem \
--tlscert=/certs/server-cert.pem \
--tlskey=/certs/server-key.pem \
-H=0.0.0.0:2376
# Don't expose Docker socket to containers
# BAD: docker run -v /var/run/docker.sock:/var/run/docker.sock
# Enable user namespaces
# /etc/docker/daemon.json
{
"userns-remap": "default",
"no-new-privileges": true,
"live-restore": true,
"log-driver": "json-file",
"log-opts": {"max-size": "10m", "max-file": "3"}
}
SECRETS MANAGEMENT
------------------
# Docker Swarm secrets
echo "db_password" | docker secret create db_pass -
docker service create --secret db_pass myimage
# BuildKit secrets (don't leak in layers)
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=mytoken \
cat /run/secrets/mytoken
# Build with secret
docker build --secret id=mytoken,src=./token.txt .
# Use environment variables at runtime (not build time)
docker run -e DB_PASS_FILE=/run/secrets/db_pass myimage
DOCKER COMPOSE SECURITY
------------------------
# docker-compose.yml
services:
app:
image: myimage:latest
read_only: true
tmpfs:
- /tmp
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
deploy:
resources:
limits:
memory: 512M
cpus: '1.0'
networks:
- backend
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost/health"]
interval: 30s
timeout: 10s
retries: 3
CIS DOCKER BENCHMARK KEY CHECKS
---------------------------------
# 1. Host configuration
auditctl -w /usr/bin/docker -p wa # Audit Docker binary
auditctl -w /etc/docker -p wa # Audit Docker config
auditctl -w /var/lib/docker -p wa # Audit Docker data
# 2. Check running containers
docker ps --format '{{.Names}}: User={{.Label "user"}} Privileged={{.Label "privileged"}}'
# 3. Find privileged containers
docker inspect --format='{{.HostConfig.Privileged}}' $(docker ps -q)
# 4. Find containers with host PID namespace
docker inspect --format='{{.HostConfig.PidMode}}' $(docker ps -q)
# 5. Check container capabilities
docker inspect --format='{{.HostConfig.CapAdd}}' $(docker ps -q)
CONTAINER ESCAPE PREVENTION
-----------------------------
# Never run privileged containers in production
# BAD: docker run --privileged myimage
# Don't mount sensitive host paths
# BAD: docker run -v /:/host myimage
# BAD: docker run -v /etc:/etc myimage
# Prevent ptrace
docker run --security-opt=no-new-privileges --cap-drop=SYS_PTRACE myimage
# Use rootless Docker
dockerd-rootless-setuptool.sh install
AUDITING COMMANDS
-----------------
# List all containers and their security settings
docker inspect $(docker ps -aq) | jq '.[].HostConfig | {Privileged, CapAdd, CapDrop, SecurityOpt, PidMode, NetworkMode}'
# Check for containers running as root
docker ps -q | xargs -I {} docker exec {} whoami 2>/dev/null
# Find images with known vulnerabilities
trivy image --severity CRITICAL $(docker images -q)
# Check Docker daemon configuration
docker system info --format '{{json .SecurityOptions}}'
Defensive reference on CyberRamen. Offensive / red-team sheets live on OffensiveRamen.com.