02. Hardened Dockerfiles & Image Security
Container security starts at the build phase. An insecure container image introduces vulnerable system packages, embedded API secrets, build compilers (gcc, make), shell utilities (curl, nc), and default root execution (UID 0) directly into production environments.
Building production-grade container images requires enforcing the Principle of Least Privilege: minimal footprint, zero unnecessary binaries, non-root user execution, and verifiable image integrity.
1. Anatomy of an Insecure Container Imageβ
An insecure Dockerfile introduces multiple critical attack vectors:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β INSECURE DOCKERFILE ANTI-PATTERNS β
ββββββββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββββββββββββ€
β Anti-Pattern β Security Risk & Exploitation Impact β
ββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββ€
β **Bloated Base Images** β Inherits hundreds of OS vulnerabilities β
β (e.g. `ubuntu:latest`, `node`) β and unnecessary binaries (`curl`, `bash`). β
ββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββ€
β **Default Root User (UID 0)** β If container is breached, attacker gets β
β β root permissions inside container namespaceβ
ββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββ€
β **Compilers in Production** β Build tools (`gcc`, `pip`, `npm`) allow β
β β attackers to compile exploits on target. β
ββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββ€
β **Hardcoded Secrets** β `ARG` or `ENV` variables leak credentials β
β β permanently into image layer metadata. β
ββββββββββββββββββββββββββββββββββΌβββββββββββββββββββββββββββββββββββββββββββββ€
β **Unpinned Base Image Tags** β `FROM node:latest` leads to non-deterministicβ
β β builds and unexpected breaking updates. β
ββββββββββββββββββββββββββββββββββ΄βββββββββββββββββββββββββββββββββββββββββββββ
2. Core Image Hardening Principlesβ
A. Multi-Stage Buildsβ
Multi-stage builds separate the compilation environment from the runtime execution environment. Compilers, build dependencies, header files, and temporary artifacts remain strictly in stage 1, producing a lean, production-ready stage 2 artifact.
B. Distroless and Scratch Base Imagesβ
- Alpine Linux: Minimal (~5 MB), but includes
apkpackage manager andshshell. - Google Distroless: Minimal (~20 MB), contains only the application runtime and its dynamic dependencies. Does not include shells (
/bin/sh,/bin/bash), package managers, or standard Unix utilities (ls,cat,curl). - Scratch: Empty base image (0 MB), ideal for statically compiled binaries (Go, Rust).
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β BASE IMAGE SECURITY COMPARISON MATRIX β
βββββββββββββββββββ¬βββββββββββ¬βββββββββββββββ¬βββββββββββββββ¬βββββββββββββββββββ€
β Base Image β Size β Package Mgr β Shell Access β Vulnerability β
β β β Included β (/bin/sh) β Surface β
βββββββββββββββββββΌβββββββββββΌβββββββββββββββΌβββββββββββββββΌβββββββββββββββββββ€
β `ubuntu:22.04` β ~78 MB β `apt` β Yes β High (100+ CVEs) β
β `node:20` β ~1.1 GB β `apt` β Yes β High (200+ CVEs) β
β `node:20-alpine`β ~170 MB β `apk` β Yes β Low (10-20 CVEs) β
β `distroless` β ~25 MB β **None** β **None** β Minimal (0-2 CVEs)β
β `scratch` β 0 MB β **None** β **None** β Zero β
βββββββββββββββββββ΄βββββββββββ΄βββββββββββββββ΄βββββββββββββββ΄βββββββββββββββββββ
C. Non-Root Execution (USER nonroot)β
Always specify an explicit non-root UID and GID (e.g. USER 65532:65532 or USER nonroot). Never rely on the default UID 0.
D. Secure Secret Handling with Docker BuildKitβ
Never store API keys or private certificates in ENV or ARG statementsβthey are saved in plain text within layer history (docker history myimage). Use BuildKit secret mounts:
# SECURE: Secret is mounted in memory for the RUN command only and never written to image layers!
RUN \
TOKEN=$(cat /run/secrets/github_token) && \
git clone https://x-access-token:${TOKEN}@github.com/org/private-repo.git
3. Side-by-Side Code Comparisons (Multi-Language)β
Pattern 1: Node.js Express Applicationβ
β Insecure Dockerfileβ
# INSECURE: Bloated base image, runs as root, includes npm build tools in runtime
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
# Hardcoded secret in build argument leaks into image layer history!
ARG API_SECRET=sk_live_9988776655
ENV API_SECRET=${API_SECRET}
EXPOSE 3000
CMD ["node", "server.js"]
β Production-Hardened Dockerfile (Multi-Stage + Distroless)β
# STAGE 1: Build & Dependency Installation
FROM node:20-alpine AS builder
WORKDIR /app
# Copy dependency manifests first for optimal layer caching
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
# STAGE 2: Minimal Distroless Production Runtime
FROM gcr.io/distroless/nodejs20-debian12:nonroot
WORKDIR /app
COPY /app /app
# Explicitly set non-root user (UID 65532)
USER nonroot
EXPOSE 3000
ENV NODE_ENV=production
CMD ["server.js"]
Pattern 2: Python FastAPI Applicationβ
β Insecure Dockerfileβ
# INSECURE: Runs as root, installs compilers in runtime layer, includes pip cache
FROM python:3.11
WORKDIR /app
COPY . /app
RUN pip install -r requirements.txt
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
β Production-Hardened Dockerfile (Multi-Stage + Non-Root User)β
# STAGE 1: Builder
FROM python:3.11-slim AS builder
WORKDIR /app
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# STAGE 2: Hardened Runtime Environment
FROM python:3.11-slim AS runner
# Create dedicated unprivileged system user and group
RUN groupadd -g 10001 appgroup && \
useradd -u 10001 -g appgroup -s /sbin/nologin -M appuser
WORKDIR /app
COPY /opt/venv /opt/venv
COPY . /app
ENV PATH="/opt/venv/bin:$PATH"
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
USER 10001:10001
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Pattern 3: Go Microservice (Scratch Base Image)β
β Insecure Dockerfileβ
# INSECURE: Leaves Go compiler and source code in runtime container
FROM golang:1.22
WORKDIR /go/src/app
COPY . .
RUN go build -o /app
CMD ["/app"]
β Production-Hardened Dockerfile (Static Binary on Scratch)β
# STAGE 1: Build statically compiled binary
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# CGO_ENABLED=0 builds a completely static binary without glibc dependencies
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \
-ldflags="-w -s" \
-o /bin/server .
# STAGE 2: Scratch Base Image (0 MB Overhead)
FROM scratch
# Import CA certificates for TLS support
COPY /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY /bin/server /server
# Non-root user ID for scratch containers
USER 65532:65532
EXPOSE 8080
ENTRYPOINT ["/server"]
Pattern 4: Java Spring Boot Applicationβ
β Insecure Dockerfileβ
# INSECURE: Full JDK in runtime, runs as root, unoptimized layer cache
FROM maven:3.9-eclipse-temurin-17
WORKDIR /app
COPY . .
RUN mvn package
CMD ["java", "-jar", "target/app.jar"]
β Production-Hardened Dockerfile (Distroless Java Runtime)β
# STAGE 1: Maven Build
FROM maven:3.9-eclipse-temurin-17-alpine AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
# STAGE 2: Distroless JRE 17 Runtime
FROM gcr.io/distroless/java17-debian12:nonroot
WORKDIR /app
COPY /build/target/*.jar /app/app.jar
USER nonroot
EXPOSE 8080
CMD ["/app/app.jar"]
4. Automated Image Security Pipeline ( Hadolint + Trivy + Cosign )β
Integrate automated security verification gates into your CI/CD workflow:
A. Dockerfile Linting with Hadolintβ
Hadolint parses Dockerfiles into an AST and checks them against best-practice rules:
# Run Hadolint CLI against local Dockerfile
hadolint Dockerfile
Sample .hadolint.yaml Configuration File:β
# .hadolint.yaml
ignored:
- DL3018 # Ignore pinning alpine package versions if using strict release tags
trustedRegistries:
- docker.io
- gcr.io
- myregistry.azurecr.io
strict-labels: true
B. Image Vulnerability Scanning & SBOM Generation with Trivyβ
Trivy scans container images for OS package and language dependency vulnerabilities (CVEs):
# 1. Fail build on HIGH or CRITICAL severity vulnerabilities
trivy image \
--exit-code 1 \
--severity HIGH,CRITICAL \
--ignore-unfixed \
myregistry.azurecr.io/appsec-atlas/api:v1.0.0
# 2. Generate Software Bill of Materials (SBOM) in CycloneDX format
trivy image \
--format cyclonedx \
--output sbom.json \
myregistry.azurecr.io/appsec-atlas/api:v1.0.0
C. Container Image Signing & Verification with Cosignβ
Cosign (part of the Linux Foundation Sigstore project) signs container images to ensure supply chain integrity:
# 1. Keyless signing using OpenID Connect (OIDC) identity in GitHub Actions
cosign sign --yes myregistry.azurecr.io/appsec-atlas/api:v1.0.0
# 2. Verify image signature before deploying to Kubernetes cluster
cosign verify \
--certificate-identity "https://github.com/my-org/my-repo/.github/workflows/deploy.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
myregistry.azurecr.io/appsec-atlas/api:v1.0.0
Next Chapter: 03. Kubernetes SecurityContext & Pod Standards β