Loading...
Loading...
Comprehensive Docker security guidelines and threat mitigation strategies
npx skill4agent add josiahsiegel/claude-plugin-marketplace docker-security-guide\/D:/repos/project/file.tsxD:\repos\project\file.tsx# Use official images only
FROM node:20.11.0-alpine3.19 # Official, specific version
# NOT
FROM randomuser/node # Unverified source
FROM node:latest # Unpredictable, can break# Verify image source
docker image inspect node:20-alpine | grep -A 5 "Author"
# Enable Docker Content Trust (image signing)
export DOCKER_CONTENT_TRUST=1
docker pull node:20-alpine# Prefer minimal distributions
FROM alpine:3.19 # ~7MB
FROM gcr.io/distroless/static # ~2MB
FROM scratch # 0MB (for static binaries)
# vs
FROM ubuntu:22.04 # ~77MB with more packages# Development stage (includes build tools)
FROM cgr.dev/chainguard/node:latest-dev AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
# Production stage (minimal, zero-CVE goal)
FROM cgr.dev/chainguard/node:latest
WORKDIR /app
COPY /app/node_modules ./node_modules
COPY . .
USER node
ENTRYPOINT ["node", "server.js"]docker-best-practices# Scan with Docker Scout
docker scout cves IMAGE_NAME
docker scout recommendations IMAGE_NAME
# Scan with Trivy
trivy image IMAGE_NAME
trivy image --severity HIGH,CRITICAL IMAGE_NAME
# Scan Dockerfile
trivy config Dockerfile
# Scan for secrets
trivy fs --scanners secret .# GitHub Actions example
- name: Scan image
run: |
docker scout cves my-image:${{ github.sha }}
trivy image --exit-code 1 --severity CRITICAL my-image:${{ github.sha }}# Build stage with build tools
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o app
# Final stage - minimal, no build tools
FROM gcr.io/distroless/base-debian11
COPY /app/app /
USER nonroot:nonroot
ENTRYPOINT ["/app"]# BAD - Secret in layer history
ENV API_KEY=abc123
RUN git clone https://user:password@github.com/repo.git
COPY .env /app/.env# Use BuildKit secrets
# syntax=docker/dockerfile:1
FROM alpine
RUN \
git clone https://$(cat /run/secrets/github_token)@github.com/repo.git# Build with secret (not in image)
docker build --secret id=github_token,src=./token.txt .# syntax=# 🔴 DANGER - Untrusted frontend (code execution risk!)
# syntax=docker/dockerfile:1@sha256:abc123...untrusted
FROM alpine
RUN echo "This frontend could do anything during build"# ✅ Safe - Official Docker frontend
# syntax=docker/dockerfile:1
# ✅ Safe - Specific version
# syntax=docker/dockerfile:1.5
# ✅ Safe - Pinned with digest (verify from docker.com)
# syntax=docker/dockerfile:1@sha256:ac85f380a63b13dfcefa89046420e1781752bab202122f8f50032edf31be0021docker/dockerfile:*# Check all Dockerfiles for potentially malicious syntax directives
grep -r "^# syntax=" . --include="Dockerfile*"
# Verify all frontends are official Docker images
grep -r "^# syntax=" . --include="Dockerfile*" | grep -v "docker/dockerfile"# Restrict frontend sources in BuildKit config
# /etc/buildkit/buildkitd.toml
[frontend."dockerfile.v0"]
# Only allow official Docker frontends
allowedImages = ["docker.io/docker/dockerfile:*"]# syntax=# Generate SBOM for image
docker scout sbom IMAGE_NAME
# Export SBOM in different formats
docker scout sbom --format spdx IMAGE_NAME > sbom.spdx.json
docker scout sbom --format cyclonedx IMAGE_NAME > sbom.cyclonedx.json
# Include SBOM attestation during build
# ⚠️ WARNING: BuildKit attestations are NOT cryptographically signed!
docker buildx build \
--sbom=true \
--provenance=true \
--tag my-image:latest \
.
# View SBOM attestations (unsigned metadata only)
docker buildx imagetools inspect my-image:latest --format "{{ json .SBOM }}"--sbom=true--provenance=true# Install Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh
# Generate SBOM from image
syft my-image:latest
# Generate in specific format
syft my-image:latest -o spdx-json > sbom.spdx.json
syft my-image:latest -o cyclonedx-json > sbom.cyclonedx.json
# Generate from Dockerfile
syft dir:. -o spdx-json > sbom.spdx.json# GitHub Actions example
name: Build with SBOM
jobs:
build:
steps:
- name: Build image with SBOM
run: |
docker buildx build \
--sbom=true \
--provenance=true \
--tag my-image:${{ github.sha }} \
--push \
.
- name: Generate SBOM with Syft
run: |
syft my-image:${{ github.sha }} -o spdx-json > sbom.json
- name: Upload SBOM artifact
uses: actions/upload-artifact@v3
with:
name: sbom
path: sbom.json
- name: Scan SBOM for vulnerabilities
run: |
grype sbom:sbom.json --fail-on high# Scan SBOM instead of image (faster)
grype sbom:sbom.json
trivy sbom sbom.json
# Compare SBOMs between versions
diff <(syft old-image:1.0 -o json) <(syft new-image:2.0 -o json)# Chainguard images include SBOM attestation by default
docker buildx imagetools inspect cgr.dev/chainguard/node:latest
# Extract SBOM
cosign download sbom cgr.dev/chainguard/node:latest > chainguard-node-sbom.jsonFROM node AS builder
ARG NPM_TOKEN
RUN echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc && \
npm install && \
rm .npmrc # Still in layer history!
# Better - secret only in build stage
FROM node AS dependencies
RUN \
npm install
FROM node AS runtime
COPY /app/node_modules ./node_modules
# No .npmrc in final image.dockerignore# Secrets
.env
.env.local
*.key
*.pem
credentials.json
secrets/
# Version control
.git
.gitignore
# Cloud credentials
.aws/
.gcloud/
# Private data
database.sql
backups/
# SSH keys
.ssh/
id_rsa
id_rsa.pub
# Sensitive logs
*.log
logs/# Enable image signing
export DOCKER_CONTENT_TRUST=1
# Set up keys
docker trust key generate my-key
docker trust signer add --key my-key.pub my-name my-image
# Push signed image
docker push my-image:tag
# Pull only signed images
docker pull my-image:tag # Fails if not signed# Create and use non-root user
FROM node:20-alpine
RUN addgroup -g 1001 appuser && \
adduser -S appuser -u 1001 -G appuser
USER appuser
WORKDIR /home/appuser/app
COPY . .
CMD ["node", "server.js"]# Check user in running container
docker exec container-name whoami # Should not be root
docker exec container-name id # Check UID/GID# Drop all, add only needed
docker run \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
my-imageservices:
app:
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICENET_BIND_SERVICENET_ADMINSYS_TIMEdocker run \
--read-only \
--tmpfs /tmp:noexec,nosuid,size=64M \
--tmpfs /var/run:noexec,nosuid,size=64M \
my-imageservices:
app:
read_only: true
tmpfs:
- /tmp:noexec,nosuid,size=64M
- /var/run:noexec,nosuid,size=64Mdocker run --security-opt="no-new-privileges:true" my-imagedocker run --security-opt="apparmor=docker-default" my-imagedocker run --security-opt="label=type:container_runtime_t" my-image# Use default profile
docker run --security-opt="seccomp=default" my-image
# Or custom profile
docker run --security-opt="seccomp=./seccomp-profile.json" my-imagedocker run \
--memory="512m" \
--memory-swap="512m" \ # Disable swap
--cpus="1.0" \
--pids-limit=100 \
--ulimit nofile=1024:1024 \
my-imageservices:
app:
deploy:
resources:
limits:
cpus: '1.0'
memory: 512M
pids: 100
reservations:
cpus: '0.5'
memory: 256M
ulimits:
nofile:
soft: 1024
hard: 1024docker run \
--name secure-app \
--detach \
--restart unless-stopped \
--user 1000:1000 \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--read-only \
--tmpfs /tmp:noexec,nosuid,size=64M \
--security-opt="no-new-privileges:true" \
--security-opt="seccomp=default" \
--memory="512m" \
--cpus="1.0" \
--pids-limit=100 \
--network=isolated-network \
--publish 127.0.0.1:8080:8080 \
--volume secure-data:/data:ro \
--health-cmd="curl -f http://localhost/health || exit 1" \
--health-interval=30s \
my-secure-image:1.2.3networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # No external access
services:
web:
networks:
- frontend
api:
networks:
- frontend
- backend
database:
networks:
- backend # Isolated from frontend# Bind to localhost only
docker run -p 127.0.0.1:8080:8080 my-image
# NOT (binds to all interfaces)
docker run -p 8080:8080 my-imageservices:
app:
ports:
- "127.0.0.1:8080:8080" # Localhost only# Disable default inter-container communication
# /etc/docker/daemon.json
{
"icc": false
}services:
app1:
networks:
- app-network
app2:
networks:
- app-network # Can communicate with app1
networks:
app-network:
driver: bridge# Create secret
echo "mypassword" | docker secret create db_password -
# Use in service
docker service create \
--name my-service \
--secret db_password \
my-image
# Access in container at /run/secrets/db_passwordversion: '3.8'
services:
app:
image: my-image
secrets:
- db_password
secrets:
db_password:
external: truedocker inspectdocker run -v /secure/secrets:/run/secrets:ro my-image# Clone docker-bench-security
git clone https://github.com/docker/docker-bench-security.git
cd docker-bench-security
sudo sh docker-bench-security.sh
# Or run as container
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 /usr/lib/systemd:/usr/lib/systemd:ro \
-v /etc:/etc:ro \
docker/docker-bench-securityservices:
app:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
labels: "service,env"
env: "ENV,VERSION"services:
app:
logging:
driver: "syslog"
options:
syslog-address: "tcp://log-server:514"
tag: "{{.Name}}/{{.ID}}"# Monitor Docker events
docker events --filter 'type=container' --filter 'event=start'
# Watch specific container
docker events --filter "container=my-container"
# Runtime security with Falco
docker run --rm -it \
--privileged \
-v /var/run/docker.sock:/host/var/run/docker.sock \
-v /dev:/host/dev \
-v /proc:/host/proc:ro \
falcosecurity/falco// /etc/docker/daemon.json
{
"userns-remap": "default"
}# Enable SELinux for Docker
setenforce 1
# Run with SELinux labels
docker run --security-opt label=type:svirt_sandbox_file_t my-image
# Volumes with SELinux
docker run -v /host/path:/container/path:z my-image# Check AppArmor status
aa-status
# Run with AppArmor profile
docker run --security-opt apparmor=docker-default my-image# More isolated than process isolation
docker run --isolation=hyperv my-imagelatest--privileged/var/run/docker.socklatest