Warum Container-Security kein nachträglicher Gedanke sein darf
Container bieten Isolation, sind aber standardmäßig keine Sicherheitsgrenze. Ein fehlkonfigurierter Container kann Host-Dateisysteme freilegen, Zugangsdaten preisgeben oder einem Angreifer einen Ansatzpunkt bieten, um sich lateral durch Ihre Infrastruktur zu bewegen. Das Shared-Kernel-Modell bedeutet: Ein Container-Escape betrifft jeden Workload auf dem Host.
Bei DevOpsVibe auditieren wir regelmäßig Container-Umgebungen. Dies sind die zehn Praktiken, die den größten Unterschied zwischen einem verwundbaren und einem gehärteten Deployment ausmachen.
1. Minimale Base Images verwenden
Jedes Paket in Ihrem Image ist eine potenzielle Angriffsfläche. Beginnen Sie mit dem kleinsten Image, das funktioniert:
Vorher (verwundbar):
FROM ubuntu:22.04
RUN apt-get update && apt-get install -y python3 python3-pip curl wget vim
COPY . /app
RUN pip3 install -r /app/requirements.txt
CMD ["python3", "/app/main.py"]
Nachher (gehärtet):
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM gcr.io/distroless/python3-debian12
COPY --from=builder /install /usr/local
COPY . /app
WORKDIR /app
CMD ["main.py"]
Das Distroless-Image hat keine Shell, keinen Paketmanager und keine unnötigen Werkzeuge. Wenn ein Angreifer Codeausführung erlangt, gibt es kaum etwas, womit er arbeiten könnte.
Vergleich der Image-Größen:
ubuntu:22.04mit Abhängigkeiten: ~450MBpython:3.12-slimMulti-Stage mit Distroless: ~85MB
2. Niemals als root laufen
Standardmäßig führt Docker Prozesse im Container als root aus. Das ist gefährlich, weil ein Container-Escape dem Angreifer root auf dem Host verschafft:
FROM node:20-slim
# Create a non-root user
RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser
WORKDIR /app
COPY --chown=appuser:appuser . .
RUN npm ci --only=production
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]
Zur Laufzeit verifizieren:
docker run --rm my-app whoami
# Output: appuser
3. Images auf Schwachstellen scannen
Integrieren Sie Image-Scanning in Ihre CI/CD-Pipeline. Wir empfehlen Trivy wegen seiner Geschwindigkeit und umfassenden Datenbank:
# Scan during build
trivy image --severity HIGH,CRITICAL --exit-code 1 my-app:latest
# Example output:
# my-app:latest (debian 12.4)
# Total: 0 (HIGH: 0, CRITICAL: 0)
Automatisieren Sie das in Ihrer Pipeline:
# .github/workflows/security.yml
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.IMAGE }}
format: sarif
output: trivy-results.sarif
severity: HIGH,CRITICAL
exit-code: "1"
- name: Upload scan results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy-results.sarif
Scannen Sie auch Ihre Dockerfiles auf Fehlkonfigurationen:
trivy config --severity HIGH,CRITICAL ./Dockerfile
4. Image-Digests pinnen, nicht nur Tags
Tags sind veränderbar. Jemand kann ein kompromittiertes Image unter python:3.12-slim pushen und jeder Build, der diesen Tag zieht, bekommt die bösartige Version:
# Risky: tag can be overwritten
FROM python:3.12-slim
# Secure: digest is immutable
FROM python:3.12-slim@sha256:a3e58c29e4a3b692a57d0e68b4b9c4f2e63e547e1b3f0a8e93c2a4fb7d3e1c9a
Den Digest erhalten Sie mit:
docker inspect --format='{{index .RepoDigests 0}}' python:3.12-slim
5. Read-Only-Dateisysteme implementieren
Verhindern Sie Laufzeit-Modifikationen des Container-Dateisystems. Wenn Ihre Anwendung schreiben muss, mounten Sie spezifische beschreibbare Volumes:
docker run --read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--volume /app/data:/data:rw \
my-app:latest
In Kubernetes:
securityContext:
readOnlyRootFilesystem: true
allowPrivilegeEscalation: false
runAsNonRoot: true
runAsUser: 1000
capabilities:
drop:
- ALL
6. Secrets sauber verwalten
Backen Sie niemals Secrets in Images. Sie bleiben in der Layer-History erhalten, selbst wenn Sie sie in einem späteren Layer löschen:
Falsch:
# NEVER do this
ENV DATABASE_URL=postgres://admin:password@db:5432/myapp
COPY .env /app/.env
Richtig -- Build-Time-Secrets nutzen (BuildKit):
# syntax=docker/dockerfile:1
RUN --mount=type=secret,id=db_url \
export DATABASE_URL=$(cat /run/secrets/db_url) && \
python manage.py collectstatic
docker buildx build --secret id=db_url,src=./db_url.txt -t my-app .
Zur Laufzeit nutzen Sie das Secrets-Management Ihres Orchestrators:
# Kubernetes secret injection
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: api-secrets
key: database-url
7. Container-Ressourcen begrenzen
Ein unbegrenzter Container kann sämtliche Host-Ressourcen verbrauchen und so einen Denial-of-Service verursachen:
docker run \
--memory=512m \
--memory-swap=512m \
--cpus=1.0 \
--pids-limit=256 \
--ulimit nofile=1024:1024 \
my-app:latest
In Kubernetes setzen Sie immer Resource Requests und Limits:
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "1000m"
8. Alle Capabilities entfernen
Linux Capabilities geben feingranulare Kontrolle darüber, was ein Prozess tun darf. Entfernen Sie alle und fügen Sie nur das wieder hinzu, was Sie wirklich brauchen:
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE my-app:latest
Häufige Capabilities und wann Sie sie tatsächlich brauchen:
| Capability | Anwendungsfall |
|---|---|
NET_BIND_SERVICE | An Ports unter 1024 binden |
CHOWN | Dateieigentümer ändern (selten nötig) |
SYS_PTRACE | Nur zum Debugging, niemals in Produktion |
SYS_ADMIN | Fast nie -- das ist nahezu gleichwertig mit root |
9. Multi-Stage-Builds nutzen, um Build-Tools auszuschließen
Build-Abhängigkeiten wie Compiler, Debugger und Quellcode haben in Ihrem Produktions-Image nichts zu suchen:
# Stage 1: Build
FROM golang:1.22 AS builder
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server ./cmd/server
# Stage 2: Production
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
ENTRYPOINT ["/server"]
Das finale Image enthält nur das kompilierte Binary und die TLS-Zertifikate. Keine Go-Toolchain, kein Quellcode, keine Shell.
10. Docker Content Trust aktivieren und Images signieren
Stellen Sie sicher, dass in Ihrer Umgebung nur verifizierte Images laufen:
# Enable content trust
export DOCKER_CONTENT_TRUST=1
# Sign and push
docker trust sign myregistry.com/my-app:v1.2.3
# Verify
docker trust inspect myregistry.com/my-app:v1.2.3
Verwenden Sie in Kubernetes-Umgebungen Admission Controller wie Kyverno oder OPA Gatekeeper, um Image-Policies zu erzwingen:
# Kyverno policy: require signed images
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
rules:
- name: verify-signature
match:
resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "myregistry.com/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
Security-Checkliste
Bevor Sie irgendeinen Container in Produktion deployen, verifizieren Sie:
- Base Image ist minimal (Distroless, Alpine oder scratch)
- Image ist auf CVEs gescannt, keine HIGH/CRITICAL-Funde
- Container läuft als Non-Root-Benutzer
- Dateisystem ist wo möglich read-only
- Keine Secrets ins Image eingebaut
- Resource Limits sind gesetzt
- Alle unnötigen Capabilities sind entfernt
- Image-Tags sind auf Digests gepinnt
- Images sind signiert und verifiziert
- Network Policies beschränken die Container-Kommunikation
Fazit
Container-Security ist kein einzelnes Tool und kein einmaliges Audit. Sie ist ein Bündel von Praktiken, das in jeder Phase Ihrer Pipeline verankert ist -- vom Dockerfile bis zur Runtime-Konfiguration. Die zehn oben beschriebenen Praktiken adressieren die häufigsten Angriffsvektoren, die wir in Produktionsumgebungen sehen.
Bei DevOpsVibe helfen wir Teams, sichere Container-Pipelines von Grund auf aufzubauen. Ob Sie ein Security-Audit Ihres bestehenden Setups benötigen oder DevSecOps-Praktiken organisationsweit etablieren wollen -- unser Team verfügt über die Expertise, es richtig umzusetzen. Nehmen Sie Kontakt auf, um das Gespräch zu beginnen.