Docker Security
OS-agnostic Docker security hardening. For core Docker concepts and Dockerfile patterns, see docker-fundamentals. For OS-specific host hardening, see ubuntu-docker-host or rhel-docker-host.
1. Image Security
Scanning with Trivy
# Install Trivy (universal)
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
# Scan a local image
trivy image myapp:latest
# Scan with severity filter
trivy image --severity HIGH,CRITICAL myapp:latest
# Scan and fail CI if critical vulnerabilities found
trivy image --exit-code 1 --severity CRITICAL myapp:latest
# Scan a Dockerfile for misconfigurations
trivy config Dockerfile
# Scan filesystem (source code dependencies)
trivy fs --scanners vuln,secret,misconfig .
# Generate JSON report
trivy image --format json --output report.json myapp:latest
# Ignore unfixed vulnerabilities
trivy image --ignore-unfixed myapp:latest
# Scan with SBOM output
trivy image --format spdx-json --output sbom.json myapp:latest
Docker Scout
# Enable Docker Scout (Docker Desktop or CLI plugin)
docker scout quickview myapp:latest
# Full CVE analysis
docker scout cves myapp:latest
# Compare two image versions
docker scout compare --to myapp:v1.0.0 myapp:v1.1.0
# Recommendations for base image updates
docker scout recommendations myapp:latest
# SBOM generation
docker scout sbom myapp:latest
Snyk Container Scanning
# Authenticate
snyk auth
# Scan image
snyk container test myapp:latest
# Scan and monitor (continuous tracking)
snyk container monitor myapp:latest
# Scan with severity threshold
snyk container test --severity-threshold=high myapp:latest
# Scan Dockerfile for best practices
snyk container test --file=Dockerfile myapp:latest
Choosing Secure Base Images
# BEST: Distroless — no shell, no package manager, minimal attack surface
FROM gcr.io/distroless/static-debian12 # Go/Rust static binaries
FROM gcr.io/distroless/base-debian12 # C/C++ with glibc
FROM gcr.io/distroless/java21-debian12 # Java apps
FROM gcr.io/distroless/nodejs22-debian12 # Node.js apps
FROM gcr.io/distroless/python3-debian12 # Python apps
# GOOD: Alpine — small (~5MB), uses musl libc
FROM node:22-alpine
FROM python:3.12-alpine
# ACCEPTABLE: Slim — Debian with non-essential packages removed
FROM node:22-slim
FROM python:3.12-slim
# AVOID: Full images — large, many unnecessary packages
# FROM node:22 # ~350MB more attack surface
# FROM ubuntu:24.04 # general-purpose, not optimized for containers
Pinning Image Digests
# BAD: Tags are mutable — "latest" or even "1.27" can change underneath you
FROM nginx:1.27-alpine
# GOOD: Pin to digest for reproducible, tamper-proof builds
FROM nginx:1.27-alpine@sha256:a5127daff3d6f4a3ed1db3...
# Find digest
# docker inspect --format='{{index .RepoDigests 0}}' nginx:1.27-alpine
# docker pull nginx:1.27-alpine && docker images --digests nginx
SBOM Generation and Provenance
# Generate SBOM with Trivy
trivy image --format spdx-json --output sbom.spdx.json myapp:latest
trivy image --format cyclonedx --output sbom.cdx.json myapp:latest
# Generate SBOM with syft
syft myapp:latest -o spdx-json > sbom.spdx.json
syft myapp:latest -o cyclonedx-json > sbom.cdx.json
# Build with provenance attestation (BuildKit)
docker buildx build --provenance=true --sbom=true -t myapp:latest .
# Inspect attestations
docker buildx imagetools inspect myapp:latest --format '{{json .Provenance}}'
docker buildx imagetools inspect myapp:latest --format '{{json .SBOM}}'
2. Image Signing
Current guidance (verified 2026-06-10): sign and verify images with Sigstore cosign (keyless or key-based) or Notation (Notary Project / Notary v2), and pin base images by digest. Docker Content Trust (DCT) is RETIRED — Docker announced its retirement in 2025; see the legacy reference at the end of this section for the timeline.
cosign (Sigstore) — primary recommendation
# Install cosign
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
# Or download a release binary: https://github.com/sigstore/cosign/releases
# ALWAYS sign by digest, not tag — tags are mutable
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' myregistry.example.com/myapp:v1.0.0)
# Sign image (keyless — uses your OIDC identity, records to the Rekor transparency log)
cosign sign "$DIGEST"
# Sign image with a key pair (air-gapped / no-OIDC environments)
cosign generate-key-pair
cosign sign --key cosign.key "$DIGEST"
# Verify a key-based signature
cosign verify --key cosign.pub myregistry.example.com/myapp:v1.0.0
# Verify a keyless signature (pin BOTH identity and issuer)
cosign verify --certificate-identity user@example.com \
--certificate-oidc-issuer https://accounts.google.com \
myregistry.example.com/myapp:v1.0.0
# Keyless in CI (e.g., GitHub Actions OIDC) — verify the workflow identity
cosign verify \
--certificate-identity-regexp '^https://github.com/myorg/myapp/\.github/workflows/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
myregistry.example.com/myapp:v1.0.0
Notation (Notary Project / Notary v2)
# Install notation — see https://notaryproject.dev for current releases
# Generate a test key + self-signed cert (production: use a CA-issued cert
# or a KMS plugin — Azure Key Vault, AWS Signer, HashiCorp Vault)
notation cert generate-test --default "myorg.example.com"
# Sign (by digest)
notation sign registry.example.com/myapp@sha256:abc123...
# List signatures on an artifact
notation ls registry.example.com/myapp@sha256:abc123...
# Configure a trust policy, then verify
notation policy import trustpolicy.json
notation verify registry.example.com/myapp@sha256:abc123...
Notation is spec-driven (OCI signatures attached in the registry), supports multiple signatures per artifact, and integrates with enterprise PKI/KMS — it is the natural DCT successor for organizations with existing certificate infrastructure. cosign keyless is the lower-friction choice for OIDC-centric (CI-driven) workflows.
Docker Content Trust (RETIRED — legacy reference)
RETIRED. Docker announced the retirement of Docker Content Trust in 2025 (announced 2025; verify current status):
- 2025-08-08 — the oldest Docker Official Image (DOI) DCT signing certificates began expiring. With
DOCKER_CONTENT_TRUST=1set, DOI pulls fail;docker trust inspectalso fails for DOI.- 2025-09-30 — DCT can no longer be enabled on new registries.
- 2028-03-31 — all DCT trust data will be permanently deleted and the feature removed.
Upstream Notary v1 is unmaintained, and fewer than 0.05% of Docker Hub pulls used DCT. Docker's official recommendation is to migrate to Sigstore cosign or Notation (above). Do NOT set
DOCKER_CONTENT_TRUST=1in new setups — and unset it where present to avoid pull failures.
The commands below are retained ONLY for auditing/decommissioning legacy DCT estates (e.g., a private registry still running Notary v1):
# Find lingering DCT enablement (remove it — causes pull failures since 2025-08)
env | grep DOCKER_CONTENT_TRUST
unset DOCKER_CONTENT_TRUST
# Inspect remaining trust data on a legacy private registry
docker trust inspect --pretty registry.example.com/myapp
# Inventory local signing keys before key destruction
docker trust key list
# Revoke trust data for a tag during decommissioning
docker trust revoke registry.example.com/myapp:v1.0.0
3. Rootless Docker
# Install rootless Docker (run as non-root user, NOT as root)
dockerd-rootless-setuptool.sh install
# Verify rootless mode
docker info --format '{{.SecurityOptions}}'
# Should include "rootless"
# Start/stop rootless daemon
systemctl --user start docker
systemctl --user stop docker
systemctl --user enable docker # auto-start on login
# Enable lingering (keep services running after logout)
sudo loginctl enable-linger $(whoami)
Rootless Configuration
# Rootless Docker stores data in user home
# Default: ~/.local/share/docker
# Set custom data root
mkdir -p ~/.config/docker
cat > ~/.config/docker/daemon.json <<'EOF'
{
"data-root": "/data/docker-rootless"
}
EOF
Rootless Limitations
| Feature | Rootless Support | Workaround |
|---|---|---|
| Binding ports < 1024 | No (by default) | sysctl net.ipv4.ip_unprivileged_port_start=0 or use slirp4netns port driver |
--net=host |
Limited | Use slirp4netns (default) or pasta |
| AppArmor | No | Use rootless + user namespaces instead |
| Overlay network (Swarm) | No | Use alternative orchestration |
| cgroup v1 resource limits | No | Migrate to cgroup v2 |
| Ping | No (by default) | sysctl net.ipv4.ping_group_range="0 2147483647" |
| NFS/FUSE mounts in containers | Limited | Bind mount from host |
When to Use Rootless
- Use rootless for development environments, CI runners, multi-tenant hosts, and anywhere the daemon itself should not run as root.
- Use rootful with hardening for production workloads needing privileged ports, overlay networking, or AppArmor/SELinux enforcement.
4. User Namespaces
User namespaces remap container UID/GID ranges so that root (UID 0) inside the container maps to an unprivileged user on the host.
Enable userns-remap
# Create subordinate UID/GID ranges
sudo usermod --add-subuids 100000-165535 dockremap
sudo usermod --add-subgids 100000-165535 dockremap
# Or manually edit
# /etc/subuid: dockremap:100000:65536
# /etc/subgid: dockremap:100000:65536
# Configure daemon
sudo tee /etc/docker/daemon.json <<'EOF'
{
"userns-remap": "default"
}
EOF
# "default" creates a dockremap user automatically
# Or specify a user: "userns-remap": "myuser"
# Restart Docker
sudo systemctl restart docker
# Verify — root in container should map to high UID on host
docker run --rm alpine id
# uid=0(root) gid=0(root) — but host sees UID 100000
Volume Permission Implications
# With userns-remap, container UID 0 maps to host UID 100000
# Volumes must be owned by the remapped UID on the host
# Example: container writes as UID 1000 -> host UID 101000
sudo chown -R 101000:101000 /data/myapp
# To find the mapped UID:
# Container UID + subordinate UID start = host UID
# 1000 + 100000 = 101000
# Disable userns-remap per container (requires daemon-level setting)
docker run --userns=host myapp # runs without remapping
5. Container Hardening
Non-Root USER in Dockerfile
# Create user and group
RUN groupadd -r appgroup && useradd -r -g appgroup -d /app -s /sbin/nologin appuser
# Own application files
COPY --chown=appuser:appgroup . /app
# Switch to non-root user (do this LAST, after all root-required operations)
USER appuser
Read-Only Root Filesystem
# Run with read-only filesystem
docker run -d --read-only myapp
# Containers often need tmpfs for temporary files
docker run -d --read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=100m \
--tmpfs /run:rw,noexec,nosuid \
myapp
# With writable volume for data
docker run -d --read-only \
--tmpfs /tmp \
-v appdata:/app/data \
myapp
Dropping Capabilities
# Drop ALL capabilities, then add only what is needed
docker run -d \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
myapp
# Common capabilities to add back selectively:
# NET_BIND_SERVICE — bind ports < 1024
# CHOWN — change file ownership
# SETUID/SETGID — change process UID/GID
# DAC_OVERRIDE — bypass file permission checks (avoid if possible)
# SYS_PTRACE — needed for debugging tools (never in production)
# List current container capabilities
docker exec <container> cat /proc/1/status | grep -i cap
# Decode capability hex
capsh --decode=00000000a80425fb
No-New-Privileges
# Prevent privilege escalation via setuid/setgid binaries
docker run -d --security-opt=no-new-privileges myapp
# This blocks:
# - setuid binaries (su, sudo, ping)
# - setgid binaries
# - Any process from gaining more privileges than its parent
Disable Inter-Container Communication
# Default: containers on the same bridge can communicate
# Disable at daemon level
sudo tee /etc/docker/daemon.json <<'EOF'
{
"icc": false
}
EOF
sudo systemctl restart docker
# With icc=false, containers must use explicit --link or user-defined networks
# User-defined networks provide DNS-based discovery and automatic isolation
Combined Hardened Run
docker run -d \
--name secure-app \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges \
--security-opt=apparmor=docker-default \
--memory=512m \
--cpus=1 \
--pids-limit=100 \
--user 1000:1000 \
--restart=unless-stopped \
--health-cmd="curl -f http://localhost:8080/health || exit 1" \
--health-interval=30s \
myapp:v1.0.0@sha256:abc123...
6. Seccomp Profiles
Seccomp (Secure Computing) filters restrict which Linux syscalls a container can make.
Default Profile
# Docker applies a default seccomp profile that blocks ~44 dangerous syscalls
# including: mount, reboot, clock_settime, init_module, delete_module, etc.
# Verify seccomp is active
docker info --format '{{.SecurityOptions}}'
# Run with default profile (explicit)
docker run --security-opt seccomp=unconfined myapp # DANGEROUS: disables seccomp
docker run --security-opt seccomp=default myapp # explicit default (Docker 20.10+)
Custom Seccomp Profile
# Generate default profile as starting point
docker run --rm -it alpine cat /proc/1/status | grep Seccomp
# Create custom profile
cat > seccomp-strict.json <<'PROFILE'
{
"defaultAction": "SCMP_ACT_ERRNO",
"defaultErrnoRet": 1,
"architectures": [
"SCMP_ARCH_X86_64",
"SCMP_ARCH_AARCH64"
],
"syscalls": [
{
"names": [
"accept", "accept4", "access", "bind", "brk", "chdir",
"chmod", "chown", "close", "connect", "dup", "dup2", "dup3",
"epoll_create", "epoll_create1", "epoll_ctl", "epoll_wait",
"epoll_pwait", "execve", "exit", "exit_group", "faccessat",
"fchmod", "fchown", "fcntl", "fstat", "fstatfs", "futex",
"getcwd", "getdents64", "getegid", "geteuid", "getgid",
"getpid", "getppid", "getuid", "ioctl", "listen", "lseek",
"lstat", "madvise", "memfd_create", "mkdir", "mmap", "mprotect",
"munmap", "nanosleep", "newfstatat", "open", "openat",
"pipe", "pipe2", "poll", "ppoll", "pread64", "pwrite64",
"read", "readlink", "recvfrom", "recvmsg", "rename",
"rt_sigaction", "rt_sigprocmask", "rt_sigreturn",
"sched_getaffinity", "sched_yield", "select", "sendmsg",
"sendto", "set_robust_list", "set_tid_address", "setgid",
"setgroups", "setsockopt", "setuid", "shutdown", "sigaltstack",
"socket", "stat", "statfs", "symlink", "tgkill", "umask",
"uname", "unlink", "wait4", "write", "writev"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
PROFILE
# Apply custom profile
docker run -d --security-opt seccomp=seccomp-strict.json myapp
Profiling Syscalls (Build a Minimal Profile)
# Use strace to discover which syscalls your app actually uses
docker run --rm --security-opt seccomp=unconfined \
strace -c -f myapp 2>&1 | tail -30
# Or use OCI seccomp BPF logging to generate a profile
# https://github.com/containers/oci-seccomp-bpf-hook
7. AppArmor / SELinux
AppArmor (Debian/Ubuntu)
# Docker applies "docker-default" AppArmor profile automatically
# Check current profile
docker inspect --format='{{.AppArmorProfile}}' <container>
# Run with specific profile
docker run -d --security-opt apparmor=docker-default myapp
# Run without AppArmor (not recommended)
docker run -d --security-opt apparmor=unconfined myapp
# Load custom AppArmor profile
sudo apparmor_parser -r -W /etc/apparmor.d/docker-myapp
docker run -d --security-opt apparmor=docker-myapp myapp
Custom AppArmor Profile Example
# /etc/apparmor.d/docker-myapp
#include <tunables/global>
profile docker-myapp flags=(attach_disconnected,mediate_deleted) {
#include <abstractions/base>
# Allow network access
network inet tcp,
network inet udp,
network inet6 tcp,
# Allow reading application files
/app/** r,
/app/node_modules/** mr,
# Allow tmp writes
/tmp/** rw,
# Deny sensitive paths
deny /etc/shadow r,
deny /etc/passwd w,
deny /proc/*/mem rw,
deny /sys/firmware/** r,
# Deny mount operations
deny mount,
deny umount,
# Deny raw socket access
deny network raw,
deny network packet,
}
SELinux (RHEL/Fedora/CentOS)
# Check SELinux status
getenforce # Enforcing, Permissive, or Disabled
sestatus # detailed status
# Docker volume SELinux labels
docker run -v /host/data:/data:Z myapp # :Z = private (single container)
docker run -v /host/data:/data:z myapp # :z = shared (multiple containers)
# The :Z flag relabels the volume with the container's exact SELinux label
# The :z flag relabels the volume with a shared label
# Check container SELinux context
docker inspect --format='{{.ProcessLabel}}' <container>
ps -eZ | grep docker
# Custom SELinux type for container process
docker run -d --security-opt label=type:svirt_apache_t myapp
# Disable SELinux confinement for a container (not recommended)
docker run -d --security-opt label=disable myapp
When to Use Which
| System | AppArmor | SELinux |
|---|---|---|
| Ubuntu/Debian | Default, simpler profiles | Available but not default |
| RHEL/CentOS/Fedora | Not default | Default, mandatory |
| Complexity | Path-based, easier to write | Label-based, steeper learning curve |
| Granularity | File path rules | Process + file + network labels |
| Docker default profile | Yes (docker-default) |
Yes (automatic labeling) |
| Recommendation | Use where available | Use where it is the OS default |
8. Secrets Management
Docker Secrets (Swarm Mode)
# Create a secret
echo "s3cr3t_pa55w0rd" | docker secret create db_password -
docker secret create tls_cert /path/to/cert.pem
# List and inspect
docker secret ls
docker secret inspect db_password
# Use in a service
docker service create \
--name mydb \
--secret db_password \
postgres:16
# Secret is mounted at /run/secrets/db_password inside the container
# Application reads it:
# password = open('/run/secrets/db_password').read().strip()
BuildKit Secret Mounts (Build-Time)
# syntax=docker/dockerfile:1
# Secret is available during build but NEVER stored in any layer
RUN --mount=type=secret,id=npm_token \
NPM_TOKEN=$(cat /run/secrets/npm_token) \
npm ci --production
# For .npmrc or pip.conf
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci --production
# Multiple secrets
RUN --mount=type=secret,id=aws_key \
--mount=type=secret,id=aws_secret \
aws s3 cp s3://bucket/data /app/data
# Pass secrets during build
docker build \
--secret id=npm_token,src=$HOME/.npm_token \
--secret id=npmrc,src=$HOME/.npmrc \
-t myapp .
# From environment variable
echo "$NPM_TOKEN" | docker build --secret id=npm_token -t myapp .
Environment Variable Risks
# BAD: Secrets in ENV are exposed everywhere
docker run -e DB_PASSWORD=secret123 myapp
# Visible via inspect
docker inspect <container> --format='{{.Config.Env}}'
# Output: [DB_PASSWORD=secret123 ...]
# Visible inside container
docker exec <container> env | grep DB_PASSWORD
# Visible in /proc
docker exec <container> cat /proc/1/environ | tr '\0' '\n'
External Vault Integration Patterns
# HashiCorp Vault — agent sidecar pattern
docker run -d \
--name vault-agent \
-v vault-secrets:/secrets \
-e VAULT_ADDR=https://vault.example.com:8200 \
hashicorp/vault agent -config=/etc/vault/agent.hcl
docker run -d \
--name myapp \
-v vault-secrets:/secrets:ro \
myapp
# Application reads /secrets/db-password written by vault-agent
# AWS Secrets Manager — init container pattern
docker run --rm \
-v app-secrets:/secrets \
-e AWS_DEFAULT_REGION=us-east-1 \
amazon/aws-cli secretsmanager get-secret-value \
--secret-id prod/myapp/db --query SecretString \
--output text > /secrets/db_password
docker run -d -v app-secrets:/secrets:ro myapp
9. Network Security
Internal Networks
# Create internal network — no external/internet access
docker network create --internal isolated_net
# Containers on internal networks can talk to each other
# but cannot reach the internet or be reached from outside
docker run -d --network isolated_net --name backend myapp
docker run -d --network isolated_net --name db postgres:16
# Multi-network pattern: frontend talks to internet AND backend
docker network create frontend_net
docker network create --internal backend_net
docker run -d --network frontend_net --name web nginx
docker network connect backend_net web # web on both networks
docker run -d --network backend_net --name api myapi # api on internal only
docker run -d --network backend_net --name db postgres:16 # db on internal only
Encrypted Overlay Networks (Swarm)
# Create encrypted overlay (IPsec encryption for data plane)
docker network create --driver overlay --opt encrypted secure_overlay
# All traffic between nodes on this network is encrypted via IPsec
docker service create --network secure_overlay --name myservice myapp
TLS for Docker Daemon
# Generate CA and server certificates
openssl genrsa -aes256 -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
openssl genrsa -out server-key.pem 4096
openssl req -new -key server-key.pem -subj "/CN=docker-host" -out server.csr
echo subjectAltName = DNS:docker-host,IP:192.168.1.100 > extfile.cnf
echo extendedKeyUsage = serverAuth >> extfile.cnf
openssl x509 -req -days 365 -sha256 -in server.csr \
-CA ca.pem -CAkey ca-key.pem -CAcreateserial \
-out server-cert.pem -extfile extfile.cnf
# Generate client certificates
openssl genrsa -out key.pem 4096
openssl req -new -key key.pem -subj "/CN=client" -out client.csr
echo extendedKeyUsage = clientAuth > extfile-client.cnf
openssl x509 -req -days 365 -sha256 -in client.csr \
-CA ca.pem -CAkey ca-key.pem -CAcreateserial \
-out cert.pem -extfile extfile-client.cnf
# Configure daemon for TLS
sudo tee /etc/docker/daemon.json <<'EOF'
{
"tls": true,
"tlsverify": true,
"tlscacert": "/etc/docker/certs/ca.pem",
"tlscert": "/etc/docker/certs/server-cert.pem",
"tlskey": "/etc/docker/certs/server-key.pem",
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"]
}
EOF
# Client usage
docker --tlsverify \
--tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem \
-H=tcp://docker-host:2376 ps
Restricting Published Ports
# Bind to specific interface (not 0.0.0.0)
docker run -d -p 127.0.0.1:8080:8080 myapp # localhost only
docker run -d -p 192.168.1.100:8080:8080 myapp # specific interface
# Default daemon binding (affects all published ports)
sudo tee /etc/docker/daemon.json <<'EOF'
{
"ip": "127.0.0.1"
}
EOF
# Docker bypasses iptables/firewalld for port publishing
# Use DOCKER-USER chain for firewall rules that survive restarts
sudo iptables -I DOCKER-USER -i eth0 -j DROP
sudo iptables -I DOCKER-USER -i eth0 -p tcp --dport 443 -j ACCEPT
sudo iptables -I DOCKER-USER -i eth0 -p tcp --dport 80 -j ACCEPT
10. CIS Docker Benchmark
The CIS Docker Benchmark provides consensus security configuration guidelines. Use docker-bench-security for automated checking.
Automated Scanning
# Run docker-bench-security
docker run --rm --net host --pid host --userns host --cap-add audit_control \
-e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
-v /var/lib:/var/lib:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /usr/lib/systemd:/usr/lib/systemd:ro \
-v /etc:/etc:ro \
docker/docker-bench-security
# Note: older CIS benchmark versions include a "Content trust enabled"
# check (DOCKER_CONTENT_TRUST=1). DCT is retired (announced 2025) — treat
# that check as obsolete; satisfy the signing intent with cosign/Notation
# verification gates instead (see Section 2).
# Output JSON report
docker run --rm --net host --pid host --userns host --cap-add audit_control \
-v /var/lib:/var/lib:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /etc:/etc:ro \
-l docker_bench_security \
docker/docker-bench-security -l /dev/stdout 2>/dev/null
Key CIS Recommendations
Host Configuration:
| # | Recommendation | How |
|---|---|---|
| 1.1.1 | Use a separate partition for containers | Mount /var/lib/docker on dedicated partition |
| 1.1.3 | Audit Docker daemon | auditctl -w /usr/bin/dockerd -k docker |
| 1.1.4 | Audit Docker files | auditctl -w /var/lib/docker -k docker |
| 1.1.5 | Audit docker.sock | auditctl -w /var/run/docker.sock -k docker |
| 1.1.7 | Audit Docker config | auditctl -w /etc/docker -k docker |
Daemon Configuration:
| # | Recommendation | How |
|---|---|---|
| 2.1 | Restrict network traffic between containers | "icc": false in daemon.json |
| 2.2 | Set logging level to info | "log-level": "info" in daemon.json |
| 2.3 | Allow Docker to make iptables changes | "iptables": true (default) |
| 2.5 | Do not use insecure registries | Ensure "insecure-registries" is empty |
| 2.6 | Setup a centralized log driver | "log-driver": "syslog" or "json-file" with rotation |
| 2.8 | Enable user namespace support | "userns-remap": "default" |
| 2.11 | Use authorization plugin | "authorization-plugins": ["auth-plugin"] |
| 2.14 | Enable live restore | "live-restore": true |
| 2.17 | Do not use Swarm if not needed | docker swarm leave --force |
Container Runtime:
| # | Recommendation | How |
|---|---|---|
| 5.1 | Do not disable AppArmor | Never use --security-opt apparmor=unconfined |
| 5.2 | Verify SELinux if applicable | Use :Z or :z on volumes |
| 5.3 | Restrict Linux capabilities | --cap-drop=ALL --cap-add=... |
| 5.4 | Do not use privileged containers | Never use --privileged |
| 5.7 | Do not map privileged ports | Avoid mapping to ports < 1024 unless necessary |
| 5.10 | Limit memory | --memory=512m |
| 5.12 | Mount root filesystem read-only | --read-only |
| 5.15 | Do not share host process namespace | Never use --pid=host |
| 5.16 | Do not share host IPC namespace | Never use --ipc=host |
| 5.25 | Restrict container from gaining privileges | --security-opt=no-new-privileges |
| 5.26 | Check container health | Define HEALTHCHECK in Dockerfile |
| 5.28 | Use PIDs limit | --pids-limit=100 |
| 5.31 | Do not mount Docker socket | Never mount /var/run/docker.sock |
daemon.json CIS-Aligned Configuration
{
"icc": false,
"log-level": "info",
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "5"
},
"storage-driver": "overlay2",
"live-restore": true,
"userland-proxy": false,
"no-new-privileges": true,
"userns-remap": "default",
"seccomp-profile": "/etc/docker/seccomp-default.json",
"default-ulimits": {
"nofile": {
"Name": "nofile",
"Hard": 65536,
"Soft": 32768
},
"nproc": {
"Name": "nproc",
"Hard": 4096,
"Soft": 2048
}
}
}
11. Runtime Security
Falco (Runtime Threat Detection)
# Run Falco in a container (OS-agnostic — recommended)
# For host-level installation, see ubuntu-docker-host or rhel-docker-host
docker run -d --name falco \
--privileged \
-v /var/run/docker.sock:/host/var/run/docker.sock:ro \
-v /proc:/host/proc:ro \
-v /dev:/host/dev \
-v /etc:/host/etc:ro \
falcosecurity/falco:latest
Custom Falco Rules
# /etc/falco/falco_rules.local.yaml
# Detect shell spawned in container
- rule: Shell Spawned in Container
desc: Detect shell execution in a container
condition: >
spawned_process and container and
proc.name in (bash, sh, zsh, dash, ash, csh, ksh, fish)
output: >
Shell spawned in container
(container=%container.name command=%proc.cmdline user=%user.name image=%container.image.repository)
priority: WARNING
# Detect crypto mining processes
- rule: Detect Cryptominer
desc: Detect known cryptominer process names
condition: >
spawned_process and container and
(proc.name in (xmrig, minerd, minergate, cpuminer, ethminer) or
proc.cmdline contains "stratum+tcp" or
proc.cmdline contains "stratum+ssl" or
proc.cmdline contains "cryptonight")
output: >
Cryptominer detected in container
(container=%container.name command=%proc.cmdline image=%container.image.repository)
priority: CRITICAL
# Detect sensitive file access
- rule: Read Sensitive File in Container
desc: Detect reads to sensitive files
condition: >
open_read and container and
fd.name in (/etc/shadow, /etc/sudoers, /root/.ssh/authorized_keys, /root/.bash_history)
output: >
Sensitive file read in container
(container=%container.name file=%fd.name command=%proc.cmdline)
priority: ERROR
Audit Logging
# Enable audit rules for Docker
sudo tee /etc/audit/rules.d/docker.rules <<'EOF'
-w /usr/bin/dockerd -p rwxa -k docker
-w /var/lib/docker -p rwxa -k docker
-w /etc/docker -p rwxa -k docker
-w /lib/systemd/system/docker.service -p rwxa -k docker
-w /lib/systemd/system/docker.socket -p rwxa -k docker
-w /var/run/docker.sock -p rwxa -k docker
-w /etc/default/docker -p rwxa -k docker
-w /etc/docker/daemon.json -p rwxa -k docker
EOF
sudo systemctl restart auditd
# Query audit logs
sudo ausearch -k docker --start today
Container Behavior Monitoring
# Monitor container events in real time
docker events --filter type=container
# Monitor with formatting
docker events --filter type=container \
--format '{{.Time}} {{.Action}} {{.Actor.Attributes.name}} {{.Actor.Attributes.image}}'
# Monitor specific actions
docker events --filter event=start --filter event=die --filter event=oom
# Check for containers running as root
docker ps -q | xargs -I{} docker inspect --format '{{.Name}}: User={{.Config.User}}' {}
# Detect containers without resource limits
docker ps -q | xargs -I{} docker inspect \
--format '{{.Name}}: Memory={{.HostConfig.Memory}} CPUs={{.HostConfig.NanoCpus}}' {}
12. Supply Chain Security
Trusted Base Images
# Use Docker Official Images (library/*)
docker pull nginx # docker.io/library/nginx — Docker Official
docker pull python:3.12-slim # Docker Official
# Use Docker Verified Publisher images
docker pull bitnami/postgresql
# Check image provenance/attestations (do NOT use `docker trust inspect` —
# DCT is retired and it fails for Docker Official Images since 2025-08)
docker buildx imagetools inspect nginx --format '{{json .Provenance}}'
docker scout quickview nginx
# Pin by digest regardless of signing tooling
docker inspect --format='{{index .RepoDigests 0}}' nginx
# Never pull from unverified third-party registries in production
Dockerfile Linting with Hadolint
# Run hadolint
docker run --rm -i hadolint/hadolint < Dockerfile
# With ignore rules
docker run --rm -i hadolint/hadolint \
--ignore DL3008 --ignore DL3009 < Dockerfile
# .hadolint.yaml configuration
cat > .hadolint.yaml <<'EOF'
ignored:
- DL3008 # Pin apt package versions
- DL3009 # Delete apt lists
trustedRegistries:
- docker.io
- gcr.io
- ghcr.io
override:
error:
- DL3001 # Do not use "last" tag
- DL3002 # Do not use setuid/setgid in RUN
warning:
- DL3042 # Avoid cache dir with pip
EOF
docker run --rm -i -v $(pwd)/.hadolint.yaml:/.config/hadolint.yaml \
hadolint/hadolint < Dockerfile
Key Hadolint Rules
| Rule | Description |
|---|---|
| DL3000 | Use absolute WORKDIR |
| DL3001 | Do not use apt-get dist-upgrade |
| DL3002 | Last USER should not be root |
| DL3003 | Use WORKDIR instead of cd |
| DL3006 | Always tag image in FROM |
| DL3007 | Using latest is prone to errors |
| DL3008 | Pin versions in apt-get install |
| DL3018 | Pin versions in apk add |
| DL3025 | Use JSON notation for CMD/ENTRYPOINT |
| DL3059 | Multiple consecutive RUN — consider consolidation |
| DL4006 | Set SHELL option -o pipefail before RUN with pipe |
CI Scanning Gates
# GitHub Actions — scan and gate
name: Container Security
on: push
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # pin to SHA in production
- name: Lint Dockerfile
uses: hadolint/hadolint-action@v3 # pin to SHA in production
with:
dockerfile: Dockerfile
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@v0.24.0 # pin to SHA in production
with:
image-ref: myapp:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v3 # pin to SHA in production
if: always()
with:
sarif_file: 'trivy-results.sarif'
# GitLab CI — scan and gate
container_scan:
stage: test
image: docker:latest
services:
- docker:dind
script:
- docker build -t myapp:${CI_COMMIT_SHA} .
- |
docker run --rm \
-v /var/run/docker.sock:/var/run/docker.sock \
aquasec/trivy image \
--exit-code 1 \
--severity HIGH,CRITICAL \
myapp:${CI_COMMIT_SHA}
allow_failure: false
SLSA Provenance
# Build with SLSA provenance (BuildKit)
docker buildx build \
--provenance=mode=max \
--sbom=true \
--push \
-t registry.example.com/myapp:v1.0.0 .
# Verify provenance
docker buildx imagetools inspect registry.example.com/myapp:v1.0.0 \
--format '{{json .Provenance.SLSA}}'
# Using SLSA GitHub Generator for container images
# Generates SLSA Level 3 provenance in GitHub Actions
# See: https://github.com/slsa-framework/slsa-github-generator
# Verify with slsa-verifier
slsa-verifier verify-image registry.example.com/myapp:v1.0.0 \
--source-uri github.com/myorg/myapp \
--source-tag v1.0.0
Quick Reference: Security Checklist
BUILD TIME:
[ ] Multi-stage builds to minimize final image
[ ] Distroless or minimal base image
[ ] Image digest pinning in FROM
[ ] Non-root USER instruction
[ ] BuildKit secret mounts (no secrets in layers)
[ ] COPY --chown for proper file ownership
[ ] hadolint in CI pipeline
[ ] Trivy/Scout/Snyk scan gate in CI
RUNTIME:
[ ] --cap-drop=ALL with selective --cap-add
[ ] --read-only with tmpfs where needed
[ ] --security-opt=no-new-privileges
[ ] --memory and --cpus limits set
[ ] --pids-limit set
[ ] --user (non-root) or USER in Dockerfile
[ ] Health checks defined
[ ] Seccomp profile active (default or custom)
[ ] AppArmor/SELinux not disabled
INFRASTRUCTURE:
[ ] Docker daemon TLS enabled (if remote API)
[ ] icc=false or user-defined networks
[ ] Published ports bound to specific interfaces
[ ] Image signing with cosign or Notation + digest pinning (Docker Content Trust retired 2025 — unset DOCKER_CONTENT_TRUST)
[ ] Centralized logging configured
[ ] Audit rules for Docker files and socket
[ ] Runtime monitoring (Falco or equivalent)
[ ] docker-bench-security run regularly
Anti-Patterns
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
Running containers with --privileged flag |
Disables all security boundaries; container has full host access; equivalent to running on bare metal | Use specific capabilities (--cap-add) for only what is needed; never use --privileged in production |
| Using images from untrusted registries without verification | Supply chain attacks; malicious layers; cryptominers in base images | Pin images by digest, not tag; sign/verify with cosign or Notation (Docker Content Trust retired 2025 — do not rely on it); scan with Trivy/Scout |
| Mounting Docker socket into containers | Any container with socket access can create privileged containers; full host compromise | Use Docker-in-Docker (dind) with TLS for CI; or use socket proxy with read-only API access |
| Storing secrets in environment variables | Visible in docker inspect, process listing, and container logs |
Use Docker secrets, tmpfs-mounted files, or external secret managers (Vault, AWS SM) |
| Not setting read-only root filesystem | Attackers can modify binaries, plant backdoors, or write malware inside the container | Use --read-only flag; mount specific writable directories as tmpfs for temp files |
Related Skills
| Topic | Skill |
|---|---|
| Core Docker concepts, Dockerfile patterns, BuildKit | docker-fundamentals |
| Bridge, overlay, DNS, multi-host networking | docker-networking |
| Volumes, bind mounts, storage drivers | docker-storage |
| CI/CD pipelines, multi-platform builds, registries | docker-cicd |
| Compose patterns and advanced orchestration | docker-compose-patterns |
| Operational admin, cross-platform, AD mapping | docker-admin |
| Docker on Ubuntu 24.04 | ubuntu-docker-host |
| Podman/Docker on RHEL 9 | rhel-docker-host |
| Python authentication and security patterns | python-auth-security |