Files
noteflow/docker/CLAUDE.md

32 KiB

Docker Security Rules for Claude Code

These rules guide Claude Code to generate secure Docker configurations, Dockerfiles, and container deployments. Apply these rules when creating or modifying Docker-related files.


GPU Support Configuration

Available Profiles

NoteFlow supports both CPU-only and GPU-accelerated Docker deployments:

Profile Description GPU Required
server CPU-only server (default) No
server-gpu NVIDIA CUDA-enabled server Yes
server-rocm AMD ROCm-enabled server Yes
server-rocm-dev AMD ROCm-enabled server (hot reload) Yes
full CPU server + frontend No
full-gpu GPU server + frontend Yes
gpu Generic GPU profile (use with server-gpu or server-rocm) Yes

Usage Examples

# CPU-only (default, cross-platform)
docker compose --profile server --profile infra up -d

# GPU-enabled (NVIDIA CUDA + nvidia-container-toolkit)
docker compose --profile server-gpu --profile infra up -d

# GPU-enabled (AMD ROCm, Linux only)
docker compose --profile server-rocm --profile infra up -d

# GPU-enabled dev (AMD ROCm with hot reload)
docker compose --profile server-rocm-dev --profile infra up -d

# Full stack with GPU
docker compose --profile full-gpu --profile infra up -d

# Generic GPU profile (choose the service variant explicitly)
docker compose --profile gpu --profile infra up server-gpu
docker compose --profile gpu --profile infra up server-rocm

Platform-Specific GPU Setup

Linux:

# Install NVIDIA container toolkit
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | \
  sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
  sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo systemctl restart docker

Linux (AMD ROCm):

  1. Install ROCm drivers for your distribution
  2. Ensure the host user is in video and render groups
  3. Verify /dev/kfd and /dev/dri are available

Windows (WSL2):

  1. Install NVIDIA GPU drivers for Windows
  2. Enable WSL2 GPU support in Docker Desktop settings
  3. Use WSL2 backend (not Hyper-V)

macOS:

  • GPU passthrough not supported in Docker on macOS
  • Use CPU profile (server or full)
  • For GPU acceleration, run server natively outside Docker

Docker Bake (Parallel Builds)

NoteFlow uses Docker Buildx Bake for efficient parallel builds. Configuration is in docker-bake.hcl.

Available Targets

Target Description Platform
server CPU-only gRPC server linux/amd64, linux/arm64
server-full CPU server with all extras linux/amd64, linux/arm64
server-gpu NVIDIA CUDA GPU server linux/amd64
server-gpu-full GPU server with all extras linux/amd64
server-rocm AMD ROCm GPU server linux/amd64
server-rocm-full ROCm server with all extras linux/amd64
server-rocm-dev ROCm server with hot reload linux/amd64
client-build Tauri client build linux/amd64
client-dev Client development env linux/amd64

Build Groups (Parallel)

Group Targets Use Case
default server Quick dev build
servers server, server-gpu, server-rocm Both CPU/GPU variants
servers-gpu server-gpu, server-rocm GPU variants only
servers-full All server variants Full production build
all Everything Complete rebuild
ci server, server-gpu, server-rocm, client-build CI/CD pipeline

Usage Examples

# Build default (CPU server)
docker buildx bake

# Build GPU server only
docker buildx bake server-gpu

# Build ROCm server only
docker buildx bake server-rocm

# Build ROCm dev server
docker buildx bake server-rocm-dev

# Build CPU and GPU servers in parallel (CUDA + ROCm)
docker buildx bake servers

# Build GPU variants only
docker buildx bake servers-gpu

# Build all targets in parallel
docker buildx bake all

# Show build plan without building
docker buildx bake --print servers

# Build with custom registry and tag
docker buildx bake --set "*.tags=myregistry.io/noteflow:v1.0" servers

# Build and push to registry
docker buildx bake --push servers

# Build and push to git.baked.rocks
REGISTRY=git.baked.rocks/vasceannie docker buildx bake --push servers-gpu

# Use GitHub Actions cache (in CI)
docker buildx bake server-ci server-gpu-ci

Variables

Override at build time with --set:

# Use different CUDA version
docker buildx bake --set server-gpu.args.CUDA_VERSION=12.5.0 server-gpu

# Use different ROCm base version
docker buildx bake --set server-rocm.args.ROCM_VERSION=6.4.1 --set server-rocm.args.ROCM_PYTORCH_RELEASE=2.6.0 server-rocm

# Use custom registry
docker buildx bake --set "*.tags=ghcr.io/myorg/noteflow:sha-abc123" all
Variable Default Description
REGISTRY (none) Container registry prefix
TAG latest Image tag
PYTHON_VERSION 3.12 Python version
CUDA_VERSION 12.4.1 CUDA version for GPU builds
ROCM_VERSION 6.4.1 ROCm version for AMD GPU builds
ROCM_PYTORCH_RELEASE 2.6.0 PyTorch release in ROCm base image

Integration with Compose

After building with bake, use pre-built images in compose:

# Build images
docker buildx bake servers

# Run with pre-built images (no --build needed)
docker compose --profile server-gpu --profile infra up -d
docker compose --profile server-rocm --profile infra up -d

Rule: Minimal Base Images

Level: strict

When: Creating Dockerfiles or selecting base images

Do: Use minimal base images appropriate for your application

# Best for compiled languages (Go, Rust, C++)
FROM scratch AS runtime
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/binary /binary
USER 65534:65534
ENTRYPOINT ["/binary"]

# Best for most applications - Google's distroless
FROM gcr.io/distroless/base-debian12:nonroot AS runtime
COPY --from=builder /app /app
USER nonroot:nonroot
ENTRYPOINT ["/app/server"]

# Good for interpreted languages requiring packages
FROM python:3.12-alpine AS runtime
RUN apk add --no-cache ca-certificates tini && \
    rm -rf /var/cache/apk/* /tmp/*
USER nobody:nobody
ENTRYPOINT ["/sbin/tini", "--"]

# For Java applications
FROM gcr.io/distroless/java21-debian12:nonroot
COPY --from=builder /app/app.jar /app/app.jar
USER nonroot:nonroot
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Don't: Use full OS images or unnecessarily large base images

# Vulnerable: Full Ubuntu with unnecessary tools
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
    python3 python3-pip \
    curl wget vim nano \
    net-tools iputils-ping telnet \
    ssh-client \
    unzip tar gzip
# Problems:
# - 500+ packages with potential vulnerabilities
# - Includes shells for attackers
# - Package manager available for installing malware
# - Networking tools for reconnaissance

# Vulnerable: Using :latest tag
FROM node:latest
# Problem: Unpredictable, may change and break builds

Why: Minimal images dramatically reduce attack surface. A typical Ubuntu image contains 100+ binaries that can be used for privilege escalation (sudo, su), lateral movement (ssh, curl, wget), or data exfiltration (nc, tar). Distroless images contain only the application runtime, reducing CVE count by 80-90%.

Refs: CWE-250, CIS Docker Benchmark 4.1, NIST 800-190 Section 3.1


Rule: Non-Root User Directive

Level: strict

When: Creating Dockerfiles

Do: Create and switch to a non-root user

FROM node:20-alpine

# Create non-root user with specific UID for consistency across containers
RUN addgroup -g 10001 -S appgroup && \
    adduser -u 10001 -S -G appgroup -h /app -s /sbin/nologin appuser

WORKDIR /app

# Copy with correct ownership
COPY --chown=appuser:appgroup package*.json ./
RUN npm ci --only=production && npm cache clean --force

COPY --chown=appuser:appgroup . .

# Switch to non-root user
USER appuser:appgroup

EXPOSE 3000
CMD ["node", "server.js"]
# For Python applications
FROM python:3.12-alpine

RUN addgroup -g 10001 -S appgroup && \
    adduser -u 10001 -S -G appgroup -h /app -s /sbin/nologin appuser

WORKDIR /app

COPY --chown=appuser:appgroup requirements.txt .
RUN pip install --no-cache-dir --user -r requirements.txt

COPY --chown=appuser:appgroup . .

USER appuser:appgroup

ENV PATH="/app/.local/bin:$PATH"
EXPOSE 8000
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# Using numeric UID for scratch/distroless
FROM golang:1.22-alpine AS builder
WORKDIR /build
COPY . .
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app

FROM scratch
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app /app
# Use numeric UID (nobody = 65534)
USER 65534:65534
ENTRYPOINT ["/app"]

Don't: Run containers as root

# Vulnerable: No USER directive (runs as root)
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "server.js"]
# Risk: Container escape = root access on host

# Vulnerable: Explicitly running as root
USER root
RUN npm install
# Never switch back to non-root
CMD ["node", "server.js"]

Why: Container escape vulnerabilities like CVE-2019-5736 (runc) and CVE-2020-15257 (containerd) allow attackers to break out of containers. If the container runs as root (UID 0), the attacker gains root on the host. Running as non-root limits impact to unprivileged user access, significantly reducing the severity of container escapes.

Refs: CWE-250, CWE-269, CIS Docker Benchmark 4.1, NIST 800-190 Section 4.2.1


Rule: Multi-Stage Builds

Level: strict

When: Building application containers

Do: Use multi-stage builds to separate build and runtime environments

# Stage 1: Build environment with all build tools
FROM golang:1.22-alpine AS builder
RUN apk add --no-cache git ca-certificates tzdata

WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download && go mod verify

COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-s -w -X main.version=${VERSION}" \
    -o /app/server ./cmd/server

# Stage 2: Minimal runtime
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /app/server /server
USER nonroot:nonroot
ENTRYPOINT ["/server"]
# Node.js multi-stage build
FROM node:20-alpine AS builder
WORKDIR /build
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build && npm prune --production

# Runtime stage
FROM node:20-alpine AS runtime
RUN apk add --no-cache tini && \
    addgroup -g 10001 -S appgroup && \
    adduser -u 10001 -S appuser -G appgroup

WORKDIR /app
COPY --from=builder --chown=appuser:appgroup /build/dist ./dist
COPY --from=builder --chown=appuser:appgroup /build/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /build/package.json ./

USER appuser:appgroup
EXPOSE 3000
ENTRYPOINT ["/sbin/tini", "--"]
CMD ["node", "dist/server.js"]
# Python multi-stage with virtual environment
FROM python:3.12-alpine AS builder
RUN apk add --no-cache build-base libffi-dev
WORKDIR /build
COPY requirements.txt .
RUN python -m venv /opt/venv && \
    /opt/venv/bin/pip install --no-cache-dir -r requirements.txt

FROM python:3.12-alpine AS runtime
RUN addgroup -g 10001 -S appgroup && \
    adduser -u 10001 -S appuser -G appgroup
COPY --from=builder /opt/venv /opt/venv
COPY --chown=appuser:appgroup . /app
WORKDIR /app
USER appuser:appgroup
ENV PATH="/opt/venv/bin:$PATH"
CMD ["python", "app.py"]

Don't: Include build tools in runtime images

# Vulnerable: Build tools in runtime image
FROM node:20
WORKDIR /app
COPY . .
RUN npm install && npm run build
# npm, build tools, dev dependencies all in final image
EXPOSE 3000
CMD ["node", "dist/server.js"]
# Problems:
# - Includes npm (can install malware)
# - Includes dev dependencies (larger attack surface)
# - Source code visible in image

Why: Build environments contain compilers, package managers, development tools, and source code that aren't needed at runtime and increase attack surface. Attackers can use these tools to download malware, compile exploits, or exfiltrate data. Multi-stage builds produce minimal images with only runtime dependencies.

Refs: CIS Docker Benchmark 4.9, NIST 800-190 Section 3.1


Rule: No Secrets in Build Arguments or Layers

Level: strict

When: Handling secrets during container build or runtime

Do: Use Docker BuildKit secrets or runtime injection

# syntax=docker/dockerfile:1.4

FROM python:3.12-alpine AS builder

# Mount secrets during build (not stored in layers)
RUN --mount=type=secret,id=pip_token \
    pip install --no-cache-dir \
    --extra-index-url https://$(cat /run/secrets/pip_token)@pypi.example.com/simple \
    -r requirements.txt

FROM python:3.12-alpine AS runtime
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY . /app
USER nobody:nobody
CMD ["python", "/app/main.py"]
# Build with BuildKit secrets
DOCKER_BUILDKIT=1 docker build \
  --secret id=pip_token,src=./pip_token.txt \
  -t myapp:latest .
# Runtime secret injection via environment (from orchestrator)
FROM python:3.12-alpine
COPY . /app
USER nobody:nobody
# Secrets injected at runtime by Docker/Kubernetes
CMD ["python", "/app/main.py"]
# docker-compose.yml with secrets
version: '3.8'
services:
  app:
    build: .
    secrets:
      - db_password
      - api_key
    environment:
      - DATABASE_PASSWORD_FILE=/run/secrets/db_password
      - API_KEY_FILE=/run/secrets/api_key

secrets:
  db_password:
    external: true
  api_key:
    external: true

Don't: Embed secrets in images

# Vulnerable: Secrets in ARG (visible in history)
ARG DATABASE_PASSWORD
ENV DATABASE_PASSWORD=${DATABASE_PASSWORD}
# docker history shows the value

# Vulnerable: Secrets in ENV
ENV API_KEY=sk-1234567890abcdef
# Plaintext in image configuration

# Vulnerable: Copying secret files
COPY credentials.json /app/
COPY .env /app/
# Secrets extractable from image layers

# Vulnerable: Secrets in RUN commands
RUN curl -H "Authorization: Bearer sk-secret123" https://api.example.com
# Visible in layer history

Why: Docker image layers are immutable and can be inspected. Secrets in ARG values appear in docker history. Secrets in ENV are stored in image config. Secrets in COPY persist in layers even if deleted later. Anyone with image access can extract these secrets. This violates secret management principles and makes rotation impossible.

Refs: CWE-798, CWE-522, CIS Docker Benchmark 4.10, NIST 800-190 Section 4.2.3


Rule: Image Vulnerability Scanning

Level: strict

When: Building and deploying Docker images

Do: Integrate vulnerability scanning in CI/CD pipelines

# GitHub Actions with Trivy
name: Build and Scan
on: [push, pull_request]

jobs:
  build-and-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Scan for vulnerabilities
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          format: 'table'
          exit-code: '1'
          severity: 'CRITICAL,HIGH'
          ignore-unfixed: true

      - name: Scan for secrets
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          scanners: 'secret'
          exit-code: '1'

      - name: Scan Dockerfile
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: 'config'
          scan-ref: '.'
          exit-code: '1'
          severity: 'CRITICAL,HIGH'
# Local scanning with Trivy
trivy image --severity CRITICAL,HIGH --exit-code 1 myapp:latest

# Scan with vulnerability database update
trivy image --download-db-only
trivy image --skip-db-update myapp:latest

# Scan for secrets and misconfigurations
trivy image --scanners vuln,secret,config myapp:latest

# Generate SBOM
trivy image --format cyclonedx --output sbom.json myapp:latest

# Scan with Grype
grype myapp:latest --fail-on high

# Scan with Docker Scout
docker scout cves myapp:latest --exit-code --only-severity critical,high
# Dockerfile best practices scanner
# hadolint Dockerfile
FROM python:3.12-alpine
# hadolint will flag issues like:
# - Using :latest tag
# - Missing USER directive
# - Curl/wget without verification

Don't: Deploy without vulnerability scanning

# Vulnerable: No security scanning
jobs:
  deploy:
    steps:
      - run: docker build -t myapp:latest .
      - run: docker push myapp:latest
      - run: kubectl set image deployment/app app=myapp:latest
# Risk: Critical CVEs deployed to production

Why: Container images contain vulnerabilities in base images, system packages, and application dependencies. Average images contain 50-200 vulnerabilities. Without scanning, critical vulnerabilities like remote code execution can be deployed. Automated scanning catches known CVEs before deployment and generates SBOMs for compliance.

Refs: CWE-1104, NIST 800-190 Section 3.2, CIS Docker Benchmark 4.4


Rule: Content Trust and Image Signing

Level: warning

When: Distributing or consuming Docker images

Do: Sign images and enable content trust

# Enable Docker Content Trust
export DOCKER_CONTENT_TRUST=1
export DOCKER_CONTENT_TRUST_SERVER=https://notary.example.com

# Push will automatically sign
docker push myregistry.io/myapp:v1.0.0

# Pull will verify signature
docker pull myregistry.io/myapp:v1.0.0

# Sign with Cosign (recommended)
cosign generate-key-pair

# Sign image
cosign sign --key cosign.key myregistry.io/myapp:v1.0.0

# Verify signature
cosign verify --key cosign.pub myregistry.io/myapp:v1.0.0

# Keyless signing with OIDC (GitHub Actions)
cosign sign --oidc-issuer=https://token.actions.githubusercontent.com \
  myregistry.io/myapp:v1.0.0
# GitHub Actions: Keyless signing with Cosign
- name: Sign image with Cosign
  run: |
    cosign sign --yes \
      --oidc-issuer=https://token.actions.githubusercontent.com \
      ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
  env:
    COSIGN_EXPERIMENTAL: "true"

Don't: Use unsigned images without verification

# Vulnerable: No signature verification
docker pull someregistry.io/app:latest
docker run someregistry.io/app:latest
# Risk: Image may be tampered or from malicious source

# Vulnerable: Disabled content trust
export DOCKER_CONTENT_TRUST=0
docker pull myregistry.io/myapp:latest

Why: Without signing, attackers can replace legitimate images through registry compromise, man-in-the-middle attacks, or typosquatting (e.g., myap vs myapp). Image signing provides cryptographic proof of origin and integrity. Content trust prevents pulling unsigned or tampered images.

Refs: CWE-494, CIS Docker Benchmark 4.5, NIST 800-190 Section 3.3


Rule: Read-Only Root Filesystem

Level: warning

When: Running Docker containers

Do: Mount root filesystem as read-only with explicit writable directories

# Run with read-only root filesystem
docker run --read-only \
  --tmpfs /tmp:rw,noexec,nosuid,size=100m \
  --tmpfs /var/run:rw,noexec,nosuid,size=10m \
  -v app-logs:/var/log/app:rw \
  myapp:latest

# Docker Compose
version: '3.8'
services:
  app:
    image: myapp:latest
    read_only: true
    tmpfs:
      - /tmp:size=100m,mode=1777
      - /var/run:size=10m
    volumes:
      - app-logs:/var/log/app:rw
      - app-data:/app/data:rw
# Prepare application for read-only filesystem
FROM python:3.12-alpine

WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt && \
    mkdir -p /app/tmp /app/logs /app/.cache && \
    chown -R nobody:nobody /app

USER nobody:nobody

# Configure app to use specific writable directories
ENV TMPDIR=/app/tmp
ENV CACHE_DIR=/app/.cache
CMD ["python", "app.py"]

Don't: Allow unrestricted filesystem writes

# Vulnerable: Writable filesystem
docker run myapp:latest
# Attacker can:
# - Modify application binaries
# - Install backdoors
# - Write malware
# - Modify configuration files

Why: A writable root filesystem allows attackers to modify application binaries, install backdoors, write malware, or tamper with configuration. Read-only filesystems prevent persistent modifications and limit the impact of application compromise. tmpfs mounts provide necessary writable space without persistence.

Refs: CWE-284, CIS Docker Benchmark 5.12, NIST 800-190 Section 4.2.2


Rule: Drop All Capabilities

Level: strict

When: Running Docker containers

Do: Drop all Linux capabilities and add only required ones

# Drop all capabilities
docker run --cap-drop=ALL \
  --security-opt=no-new-privileges:true \
  myapp:latest

# Add back specific capability if absolutely required
docker run --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --user 1000:1000 \
  --security-opt=no-new-privileges:true \
  myapp:latest
# Docker Compose
version: '3.8'
services:
  app:
    image: myapp:latest
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    # Only if binding to port < 1024
    # cap_add:
    #   - NET_BIND_SERVICE

Common capabilities and their risks:

# Dangerous capabilities to never add:
# CAP_SYS_ADMIN   - Mount filesystems, load kernel modules
# CAP_SYS_PTRACE  - Debug processes, bypass security
# CAP_SYS_RAWIO   - Direct I/O access
# CAP_NET_ADMIN   - Network configuration changes
# CAP_DAC_OVERRIDE - Bypass file permission checks

# Rarely needed capabilities:
# CAP_NET_BIND_SERVICE - Bind to ports < 1024
# CAP_CHOWN           - Change file ownership
# CAP_SETUID          - Change UID (dangerous!)

Don't: Run with default or elevated capabilities

# Vulnerable: Default capabilities (13 capabilities)
docker run myapp:latest

# Critical: All capabilities
docker run --cap-add=ALL myapp:latest
# Equivalent to root on host

# Vulnerable: Dangerous capabilities
docker run --cap-add=SYS_ADMIN myapp:latest
docker run --cap-add=SYS_PTRACE myapp:latest

Why: Linux capabilities divide root privileges into distinct units. Docker containers have 13 capabilities by default, including dangerous ones like CAP_NET_RAW (ARP spoofing, packet sniffing) and CAP_SETFCAP (setting file capabilities). Dropping all capabilities and adding only required ones follows least privilege and significantly reduces attack surface.

Refs: CWE-250, CWE-269, CIS Docker Benchmark 5.3-5.4, NIST 800-190 Section 4.2.1


Rule: No Privileged Containers

Level: strict

When: Running Docker containers

Do: Run containers with restricted privileges

# Secure container runtime
docker run \
  --cap-drop=ALL \
  --security-opt=no-new-privileges:true \
  --security-opt=seccomp=default.json \
  --read-only \
  --user 10001:10001 \
  myapp:latest
# Docker Compose with security options
version: '3.8'
services:
  app:
    image: myapp:latest
    user: "10001:10001"
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
      - seccomp:seccomp-profile.json
    read_only: true
    tmpfs:
      - /tmp

Don't: Run privileged containers

# Critical vulnerability: Privileged mode
docker run --privileged myapp:latest
# Gives full host access:
# - All capabilities
# - Access to all devices
# - Can mount host filesystem
# - Can load kernel modules
# - Effectively root on host

# Vulnerable: Elevated privileges
docker run --cap-add=SYS_ADMIN myapp:latest
docker run --device=/dev/sda myapp:latest
docker run -v /:/host myapp:latest

Why: Privileged containers have full access to the host system, effectively negating container isolation. An attacker who compromises a privileged container has root access to the host and can escape the container trivially. This is the most severe container security misconfiguration.

Refs: CWE-250, CWE-269, CIS Docker Benchmark 5.4, NIST 800-190 Section 4.2.1


Rule: Container Health Checks

Level: advisory

When: Creating production Dockerfiles

Do: Implement comprehensive health checks

FROM python:3.12-alpine

WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt

USER nobody:nobody

# Health check with appropriate intervals
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:8000/health || exit 1

EXPOSE 8000
CMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
# For containers without wget/curl
FROM gcr.io/distroless/base-debian12:nonroot

COPY --from=builder /app/server /server
COPY --from=builder /app/healthcheck /healthcheck

USER nonroot:nonroot

HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
  CMD ["/healthcheck"]

ENTRYPOINT ["/server"]
# Multiple health check approaches
# HTTP endpoint
HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1

# TCP port check
HEALTHCHECK CMD nc -z localhost 5432 || exit 1

# Custom script
HEALTHCHECK CMD /app/healthcheck.sh || exit 1

# PostgreSQL
HEALTHCHECK CMD pg_isready -U postgres || exit 1

# Redis
HEALTHCHECK CMD redis-cli ping || exit 1

Don't: Skip health checks in production

# Vulnerable: No health check
FROM python:3.12-alpine
COPY . /app
CMD ["python", "/app/main.py"]
# Problems:
# - No automatic restart on failure
# - No readiness detection
# - Harder to detect compromised containers

Why: Without health checks, containers that crash, deadlock, or become unresponsive continue running. Orchestrators can't automatically restart failed containers or route traffic away from unhealthy instances. Health checks enable automatic recovery and can detect anomalies that may indicate compromise.

Refs: CIS Docker Benchmark 4.6, NIST 800-190 Section 4.4.1


Rule: Resource Limits

Level: warning

When: Running Docker containers in production

Do: Set memory, CPU, and PID limits

# Run with resource limits
docker run \
  --memory="512m" \
  --memory-swap="512m" \
  --memory-reservation="256m" \
  --cpus="1.0" \
  --cpu-shares=1024 \
  --pids-limit=100 \
  --ulimit nofile=1024:1024 \
  --ulimit nproc=64:64 \
  myapp:latest
# Docker Compose resource limits
version: '3.8'
services:
  app:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
          pids: 100
        reservations:
          cpus: '0.5'
          memory: 256M
    ulimits:
      nofile:
        soft: 1024
        hard: 1024
      nproc:
        soft: 64
        hard: 64
# System-wide Docker daemon defaults
# /etc/docker/daemon.json
{
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 1024,
      "Soft": 1024
    },
    "nproc": {
      "Name": "nproc",
      "Hard": 64,
      "Soft": 64
    }
  }
}

Don't: Run containers without resource limits

# Vulnerable: No limits
docker run myapp:latest
# Risks:
# - Fork bombs can exhaust PIDs
# - Memory leaks can OOM host
# - CPU mining can starve other containers
# - File descriptor exhaustion

Why: Containers without resource limits can consume all host resources, causing denial of service to other containers and the host system. Attackers exploit this through fork bombs (--pids-limit prevents), memory exhaustion (--memory prevents), and CPU abuse (--cpus prevents). Limits ensure fair resource sharing and contain compromise impact.

Refs: CWE-400, CWE-770, CIS Docker Benchmark 5.10-5.14, NIST 800-190 Section 4.2.4


Rule: Secure .dockerignore

Level: warning

When: Building Docker images

Do: Create comprehensive .dockerignore to exclude sensitive files

# .dockerignore

# Version control
.git
.gitignore
.svn

# Secrets and credentials
.env
.env.*
*.pem
*.key
*.crt
**/secrets/
credentials.json
service-account.json
*.secret

# Build artifacts and dependencies
node_modules
__pycache__
*.pyc
.pytest_cache
.coverage
coverage/
dist/
build/
target/

# IDE and editor files
.vscode
.idea
*.swp
*.swo
*~

# Documentation and tests (usually not needed in runtime)
docs/
*.md
README*
CHANGELOG*
test/
tests/
*_test.go
*.test.js

# Docker files
Dockerfile*
docker-compose*.yml
.dockerignore

# CI/CD
.github/
.gitlab-ci.yml
.travis.yml
Jenkinsfile

# OS files
.DS_Store
Thumbs.db

# Log files
*.log
logs/

Don't: Build images without .dockerignore

# Vulnerable: No .dockerignore
# The following get copied into the image:
# - .git directory (full history including deleted secrets)
# - .env files with credentials
# - Private keys and certificates
# - IDE settings with personal info
# - Test files and coverage reports
# - node_modules (may differ from npm ci)

Why: Without .dockerignore, COPY and ADD instructions include all files in the build context, including sensitive data like .git directories (containing full history), environment files with credentials, private keys, and IDE configurations. These become extractable from image layers and may be exposed if the image is published.

Refs: CWE-200, CWE-522, CIS Docker Benchmark 4.10


Additional Security Configurations

Seccomp Profile

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_AARCH64"],
  "syscalls": [
    {
      "names": [
        "read", "write", "open", "close", "stat", "fstat",
        "lstat", "poll", "lseek", "mmap", "mprotect", "munmap",
        "brk", "rt_sigaction", "rt_sigprocmask", "ioctl",
        "access", "pipe", "select", "sched_yield", "mremap",
        "msync", "mincore", "madvise", "shmget", "shmat",
        "exit", "exit_group", "wait4", "kill", "uname",
        "fcntl", "flock", "fsync", "fdatasync", "truncate",
        "ftruncate", "getdents", "getcwd", "chdir", "fchdir",
        "rename", "mkdir", "rmdir", "link", "unlink", "symlink",
        "readlink", "chmod", "fchmod", "chown", "fchown",
        "lchown", "umask", "gettimeofday", "getuid", "getgid",
        "geteuid", "getegid", "getpgid", "getppid", "getpgrp",
        "setsid", "setpgid", "getgroups", "setresuid", "setresgid",
        "getresuid", "getresgid", "sigaltstack", "rt_sigreturn",
        "clock_gettime", "clock_getres", "clock_nanosleep",
        "futex", "sched_getaffinity", "epoll_create", "epoll_ctl",
        "epoll_wait", "epoll_pwait", "epoll_create1", "dup",
        "dup2", "dup3", "socket", "connect", "accept", "accept4",
        "sendto", "recvfrom", "sendmsg", "recvmsg", "bind",
        "listen", "getsockname", "getpeername", "socketpair",
        "setsockopt", "getsockopt", "clone", "execve", "arch_prctl",
        "prctl", "pread64", "pwrite64", "readv", "writev",
        "getrandom", "memfd_create", "openat", "fstatat", "unlinkat",
        "renameat", "faccessat", "fchmodat", "fchownat", "newfstatat"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

AppArmor Profile

# /etc/apparmor.d/docker-myapp
#include <tunables/global>

profile docker-myapp flags=(attach_disconnected,mediate_deleted) {
  #include <abstractions/base>

  network inet tcp,
  network inet udp,
  network inet icmp,

  deny @{PROC}/* w,
  deny @{PROC}/sys/** w,
  deny /sys/** w,

  /app/** r,
  /app/logs/** rw,
  /tmp/** rw,

  deny /etc/shadow r,
  deny /etc/passwd r,
}

Docker Daemon Security Configuration

{
  "icc": false,
  "userns-remap": "default",
  "no-new-privileges": true,
  "seccomp-profile": "/etc/docker/seccomp-profile.json",
  "live-restore": true,
  "userland-proxy": false,
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "storage-driver": "overlay2",
  "default-ulimits": {
    "nofile": {
      "Name": "nofile",
      "Hard": 65535,
      "Soft": 65535
    }
  }
}

Refs: CIS Docker Benchmark Section 2, NIST 800-190 Section 4