Zum Inhalt springen

Docker-Container in Produktion absichern

Praktische Techniken zur Härtung von Docker-Containern: Image-Scans, Least Privilege, Secrets-Management, Netzwerkrichtlinien und Laufzeit-Monitoring.

5 Min. Lesezeit
Docker-Container mit Sicherheitsebenen, die Image-Scanning, Netzwerkrichtlinien und Laufzeitüberwachung zeigen

Der Betrieb von Docker-Containern in Produktion bringt eine Reihe spezifischer Sicherheitsrisiken mit sich. Container teilen sich den Kernel des Hosts, Images enthalten oft mehr Software als nötig, und Fehlkonfigurationen können den gesamten Host offenlegen. Die meisten Sicherheitsvorfälle bei Containern beruhen auf einfachen Fehlern: Ausführung als root, ungepatchte Basis-Images oder das Einbinden des Docker-Sockets in Container.

Dieser Leitfaden beschreibt die praktischen Schritte, mit denen sich die häufigsten Container-Schwachstellen beseitigen lassen. Keiner davon erfordert exotische Werkzeuge — nur Disziplin und die richtige Konfiguration.

Sichere Images erstellen

Sicherheit beginnt beim Image. Jedes Paket in deinem Image vergrößert die Angriffsfläche. Ein minimales Image, das nur die Laufzeitumgebung und den Code deiner Anwendung enthält, lässt sich deutlich schwerer angreifen als eines auf Basis einer vollständigen Betriebssystem-Distribution.

dockerfiledockerfile
# ❌ Bloated image with unnecessary attack surface
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y \
    python3 python3-pip curl wget vim git openssh-client
COPY . /app
WORKDIR /app
RUN pip install -r requirements.txt
CMD ["python3", "app.py"]
# Result: 850MB image with SSH client, git, vim, wget
# Every extra binary is a potential exploit vector
 
# ✅ Multi-stage build with minimal runtime image
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/deps -r requirements.txt
 
FROM python:3.11-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
COPY --from=builder /deps /usr/local/lib/python3.11/site-packages/
COPY --chown=appuser:appuser . .
USER appuser
EXPOSE 8000
CMD ["python3", "app.py"]
# Result: 180MB image, no extra tools, non-root user
dockerfiledockerfile
# Even better: distroless images (no shell, no package manager)
FROM python:3.11-slim AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --target=/deps -r requirements.txt
 
FROM gcr.io/distroless/python3-debian12
WORKDIR /app
COPY --from=builder /deps /usr/local/lib/python3.11/site-packages/
COPY . .
USER nonroot
CMD ["app.py"]
# No shell means an attacker who gets code execution
# cannot spawn a reverse shell or run arbitrary commands

Image-Scanning und Sicherheit der Lieferkette

Scanne Images auf bekannte Schwachstellen, bevor sie in Produktion gehen. Integriere das Scanning in deine CI-Pipeline, damit verwundbare Images gar nicht erst ausgerollt werden.

ymlyaml
# GitHub Actions: scan images on every push
name: Container Security Scan
on: [push]
 
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
 
      - name: Build image
        run: docker build -t myapp:${{ github.sha }} .
 
      - name: Run Trivy vulnerability scanner
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: myapp:${{ github.sha }}
          format: table
          exit-code: 1
          severity: CRITICAL,HIGH
          # Fail the build if CRITICAL or HIGH vulnerabilities found
shbash
# Local scanning with Trivy
trivy image myapp:latest
 
# Scan for misconfigurations in Dockerfile
trivy config --severity HIGH,CRITICAL .
 
# Pin base image digests to prevent supply chain attacks
# Instead of:  FROM python:3.11-slim
# Use:         FROM python:3.11-slim@sha256:abc123...
# This ensures the exact same base image every build
docker inspect --format='{{index .RepoDigests 0}}' python:3.11-slim

Container mit minimalen Rechten betreiben

Die Standardkonfiguration von Docker gewährt Containern mehr Rechte, als sie eigentlich brauchen. Beschränke jeden Container auf den minimal notwendigen Zugriff.

ymlyaml
# docker-compose.yml with security hardening
version: "3.8"
 
services:
  api:
    image: myapp:latest
    user: "1000:1000"  # Run as non-root
    read_only: true     # Read-only filesystem
    tmpfs:
      - /tmp:noexec,nosuid,size=100m  # Writable tmp with limits
    security_opt:
      - no-new-privileges:true  # Prevent privilege escalation
    cap_drop:
      - ALL                     # Drop all Linux capabilities
    cap_add:
      - NET_BIND_SERVICE        # Only add what's needed
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 512M
        reservations:
          cpus: "0.25"
          memory: 128M
    networks:
      - frontend
    healthcheck:
      test: ["CMD", "wget", "--spider", "-q", "http://localhost:8000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
 
  database:
    image: postgres:15-alpine
    user: "999:999"
    read_only: true
    tmpfs:
      - /tmp:noexec,nosuid
      - /var/run/postgresql:noexec,nosuid
    volumes:
      - db-data:/var/lib/postgresql/data
    security_opt:
      - no-new-privileges:true
    cap_drop:
      - ALL
    networks:
      - backend  # Database only accessible from backend network
 
networks:
  frontend:
  backend:
    internal: true  # No external access
 
volumes:
  db-data:
tstypescript
// ❌ Common Docker security mistakes
 
// Mounting the Docker socket — gives container full host control
// -v /var/run/docker.sock:/var/run/docker.sock
// An attacker can create privileged containers, access host filesystem
 
// Running as root (the default)
// Any container escape gives root on the host
 
// Using --privileged flag
// Disables all security features: capabilities, seccomp, AppArmor
 
// Hardcoded secrets in environment variables
// docker run -e DATABASE_PASSWORD=hunter2 myapp
 
// ✅ Secure alternatives
const securityChecklist = {
  noDockerSocket: "Never mount Docker socket into application containers",
  nonRootUser: "Always specify USER in Dockerfile",
  noPrivileged: "Never use --privileged; add specific capabilities instead",
  secretsManagement: "Use Docker secrets or external vault for credentials",
  readOnlyFs: "Use read_only: true with explicit tmpfs for writable paths",
  resourceLimits: "Always set CPU and memory limits",
  networkSegmentation: "Use separate networks; mark internal where possible",
};

Secrets-Management in Containern

Secrets sollten niemals fest in Images eingebacken oder als Klartext-Umgebungsvariablen übergeben werden. Docker Secrets, gemountete Dateien oder externe Vaults bieten eine deutlich bessere Isolation.

ymlyaml
# Docker Swarm secrets (encrypted at rest and in transit)
version: "3.8"
 
services:
  api:
    image: myapp:latest
    secrets:
      - db_password
      - api_key
    environment:
      DB_PASSWORD_FILE: /run/secrets/db_password
      API_KEY_FILE: /run/secrets/api_key
 
secrets:
  db_password:
    external: true
  api_key:
    external: true
tstypescript
import { readFileSync } from "fs";
 
// Read secrets from mounted files (works with Docker secrets,
// Kubernetes secrets, and Vault agent injector)
function getSecret(name: string): string {
  const filePath = process.env[`${name}_FILE`];
  if (filePath) {
    return readFileSync(filePath, "utf-8").trim();
  }
 
  // Fallback to environment variable for local development
  const envValue = process.env[name];
  if (envValue) {
    return envValue;
  }
 
  throw new Error(`Secret ${name} not configured`);
}
 
const dbPassword = getSecret("DB_PASSWORD");
const apiKey = getSecret("API_KEY");

Sicherheitsüberwachung zur Laufzeit

Image-Scanning erkennt bekannte Schwachstellen. Die Laufzeitüberwachung erkennt unerwartetes Verhalten — Prozesse, die eigentlich nicht laufen sollten, Netzwerkverbindungen zu unerwarteten Zielen oder Änderungen am Dateisystem in eigentlich schreibgeschützten Containern.

ymlyaml
# Falco rules for runtime container monitoring
# Falco watches syscalls and alerts on suspicious activity
 
- rule: Unexpected outbound connection
  desc: Detect container connecting to IP not in allowlist
  condition: >
    evt.type=connect and fd.typechar=4
    and container.id != host
    and not fd.sip in (allowed_outbound_ips)
  output: >
    Unexpected outbound connection
    (container=%container.name image=%container.image.repository
     connection=%fd.name user=%user.name)
  priority: WARNING
 
- rule: Shell spawned in container
  desc: Detect shell execution inside a container
  condition: >
    spawned_process and container
    and proc.name in (bash, sh, zsh, dash, csh)
  output: >
    Shell spawned in container
    (container=%container.name shell=%proc.name
     parent=%proc.pname user=%user.name)
  priority: WARNING
shbash
# Health check script that verifies container security posture
#!/bin/bash
set -euo pipefail
 
echo "=== Container Security Audit ==="
 
# Check if running as root
if [ "$(id -u)" -eq 0 ]; then
  echo "FAIL: Running as root"
else
  echo "PASS: Running as non-root (uid=$(id -u))"
fi
 
# Check if filesystem is read-only
if touch /test-write 2>/dev/null; then
  rm /test-write
  echo "FAIL: Root filesystem is writable"
else
  echo "PASS: Root filesystem is read-only"
fi
 
# Check for unnecessary capabilities
if capsh --print 2>/dev/null | grep -q "cap_sys_admin"; then
  echo "FAIL: Container has CAP_SYS_ADMIN"
else
  echo "PASS: No dangerous capabilities"
fi
 
# Check for mounted Docker socket
if [ -e /var/run/docker.sock ]; then
  echo "FAIL: Docker socket is mounted"
else
  echo "PASS: No Docker socket"
fi

Die wichtigsten Erkenntnisse

  1. Nutze Multi-Stage-Builds und distroless Images — jede Binärdatei in deinem Image vergrößert die Angriffsfläche; liefere nur das aus, was deine Anwendung wirklich zum Laufen braucht
  2. Führe Container niemals als root aus — lege in deinem Dockerfile einen Nicht-root-Benutzer an und setze USER; kombiniere dies mit no-new-privileges, um Rechteausweitung zu verhindern
  3. Entferne alle Capabilities und füge sie gezielt wieder hinzu — cap_drop: ALL zusammen mit gezielten cap_add-Einträgen ist sicherer als der Standardsatz an Capabilities
  4. Scanne Images in der CI und blockiere verwundbare Builds — integriere Trivy oder einen vergleichbaren Scanner in deine Pipeline mit exit-code: 1 für kritische Schwachstellen
  5. Binde Secrets als Dateien ein, nicht als Umgebungsvariablen — Umgebungsvariablen tauchen in Logs, Prozesslisten und Crash-Dumps auf; dateibasierte Secrets sind besser abgeschottet
  6. Überwache das Laufzeitverhalten — Image-Scanning findet bekannte CVEs, kann aber keine Zero-Days oder unerwartetes Verhalten erkennen; die Laufzeitüberwachung erfasst Anomalien in dem Moment, in dem sie auftreten
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX