Cybersicherheit

Container-Sicherheit für den Home-Server: Von Rootless Docker bis seccomp

Container-Sicherheit für den Home-Server: Von Rootless Docker bis seccomp

Vor einigen Monaten begann ich, die Dienste auf meinem Home-Server ernsthaft mit Docker zu trennen. Ein Container fuer das Web, einer fuer die Datenbank und ein weiterer fuer kleine Werkzeuge. Das wirkte ordentlich, wie ein Apartment mit vielen Zimmern, die jeweils eine eigene Aufgabe haben. Mit der Zeit wurde mir aber klar: Viele Zimmer machen ein Haus nicht automatisch sicher. Wenn ein Bewohner andere Zimmer oeffnen oder sogar auf die Hauptstrasse gelangen kann, ist diese Trennung nur eine Illusion.

Darum ist Container-Sicherheit ein Thema, das man verstehen sollte, sobald man Docker sicher beherrscht. Container sind leichtgewichtig und praktisch, ihre Isolation unterscheidet sich jedoch von der virtueller Maschinen. Dieser Artikel ist meine Zusammenfassung der drei am einfachsten umsetzbaren Verteidigungsschichten: rootless Container, Capability-Dropping und seccomp.

Ein Container ist keine VM: Die Isolation ist duenn

Ein haeufiges Missverstaendnis ist, dass Container genauso stark isoliert sind wie VMs. Tatsaechlich ist ein Container eher ein Prozess, der mit Namespaces und cgroups umhuellt ist. Container teilen sich denselben Kernel mit dem Host. Gibt es eine Schwachstelle im Kernel oder in der Container-Laufzeit, kann ein Ausbruch aus dem Container auf den Host erfolgen.

Stell dir einen Container wie ein Zimmer in einem gemeinsam genutzten Haus vor. Jedes Zimmer hat ein eigenes Schloss, aber alle teilen sich Waende, Rohre und die Elektrik. Das Zimmer bietet Privatsphaere, ist aber kein eigenes Haus. Wer eine Wand durchbrechen kann, erreicht den Gemeinschaftsbereich.

Die beste Strategie ist nicht, Container nicht mehr zu nutzen, sondern weitere Einschraenkungen hinzuzufuegen, damit jeder Container nur die Faehigkeiten besitzt, die er wirklich braucht.

Erste Schicht: Container nicht als Root ausfuehren

Standardmaessig laufen Prozesse im Container als root (UID 0). Sie sind zwar im Container isoliert, doch bei einem Ausbruch erhaelt ein Angreifer sofort Root-Rechte auf dem Host. Das waere, als bekäme jeder Bewohner den Generalschluessel des Apartmenthauses.

Die einfachste Loesung besteht darin, im Image einen normalen Benutzer anzulegen und anschliessend die Anweisung USER im Dockerfile zu verwenden.

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN addgroup -g 1001 -S appgroup && \
    adduser -u 1001 -S appuser -G appgroup
USER appuser
CMD ["node", "server.js"]

Mit dem Dockerfile oben laeuft die Anwendung als UID 1001. Wenn ein Angreifer aus dem Container ausbricht, wird er nicht sofort zu Root. Das ist ein kleiner Schritt mit grosser Wirkung.

Darueber hinaus gibt es das Konzept von rootless Docker oder rootless Podman. Hier laeuft der Docker-Daemon selbst nicht als Root. Container werden von einem normalen Benutzer ausgefuehrt, und sogar UID 0 im Container wird in Wirklichkeit auf die UID dieses Benutzers auf dem Host abgebildet. Das verringert die Angriffsoberflaeche drastisch.

# Install rootless Docker (Debian/Ubuntu)
dockerd-rootless-setuptool.sh install

# Jalankan container rootless
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
docker run --rm -u 1000:1000 -v $HOME/project:/app:ro myapp

Rootless Container haben zwar Einschraenkungen. Sie koennen zum Beispiel ohne zusaetzliche Konfiguration keine Ports unter 1024 binden. Fuer einen Home-Server kann man jedoch meist einen Reverse Proxy auf dem Host verwenden, der auf einen hohen Container-Port weiterleitet.

Zweite Schicht: Capabilities entfernen

Unter Linux hat Root viele "Superkraefte", die Capabilities genannt werden: Dateibesitzer aendern, Raw-Ports oeffnen, Sockets binden und mehr. Standardmaessig erhalten Container einen Teil dieser Capabilities. Wir koennen sie begrenzen, indem wir zuerst alles drop und dann nur das hinzufuegen, was wirklich benoetigt wird.

Das Prinzip aehnelt Berechtigungen fuer eine Mobil-App: Gib ihr keinen Zugriff auf Kamera, Mikrofon und Standort, wenn sie nur ein Taschenrechner ist.

# Drop semua capability, lalu tambahkan hanya yang dibutuhkan
docker run -d \
  --cap-drop=ALL \
  --cap-add=NET_BIND_SERVICE \
  --cap-add=CHOWN \
  --name webapp \
  myapp:latest

Bei einem Container, der wirklich nichts Besonderes braucht, genuegt --cap-drop=ALL ohne irgendein --cap-add. Gewoehnliche Anwendungen wie ein statischer Webserver oder ein Queue-Worker benoetigen oft gar keine Capabilities.

Der folgende Befehl zeigt die Standard-Capabilities von Docker. Pruefe sie, bevor du sie begrenzt:

docker run --rm -it ubuntu:24.04 capsh --print

Dritte Schicht: Systemaufrufe mit seccomp filtern

seccomp ist eine Funktion des Linux-Kernels, mit der sich filtern laesst, welche Systemaufrufe ein Prozess ausfuehren darf. Docker hat bereits ein Standard-seccomp-Profil, das etwa 44 gefaehrliche Systemaufrufe blockiert. Viele moderne Anwendungen brauchen aber nicht jeden Systemaufruf, der weiterhin erlaubt ist.

Stell dir seccomp wie eine Zugriffsliste im Buero vor: Sie regelt nicht nur, wer eintreten darf, sondern auch, was dort getan werden darf. Wenn eine Anwendung nur Dateien lesen, an Sockets lauschen und Logs schreiben muss, brauchen Systemaufrufe zum Laden von Kernelmodulen oder zum Aendern von Speicherzuordnungen nicht erlaubt zu werden.

Hier ist ein einfaches seccomp-Profil, das nur gaengige Syscalls erlaubt:

{
  "defaultAction": "SCMP_ACT_ERRNO",
  "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_AARCH64"],
  "syscalls": [
    {
      "names": [
        "accept", "accept4", "bind", "brk", "clone", "close", "connect",
        "epoll_create", "epoll_create1", "epoll_ctl", "epoll_pwait", "epoll_wait",
        "exit", "exit_group", "fcntl", "fstat", "futex", "getcwd", "getpid",
        "getrandom", "getsockname", "getsockopt", "ioctl", "listen", "lseek",
        "mmap", "mprotect", "munmap", "nanosleep", "open", "openat", "poll",
        "read", "readv", "recvfrom", "recvmsg", "rt_sigaction", "rt_sigprocmask",
        "rt_sigreturn", "select", "sendmsg", "sendto", "setitimer", "setsockopt",
        "socket", "socketpair", "stat", "write", "writev"
      ],
      "action": "SCMP_ACT_ALLOW"
    }
  ]
}

Speichere es als seccomp-web.json und starte dann den Container:

docker run -d \
  --security-opt seccomp=seccomp-web.json \
  --cap-drop=ALL \
  --name restricted-app \
  myapp:latest

Ein eigenes seccomp-Profil zu erstellen, erfordert tatsaechlich ein tiefes Verstaendnis. Praktisch ist es, mit Dockers Standardprofil zu beginnen und dann waehrend Integrationstests schrittweise weitere Blockierungen hinzuzufuegen. Wenn die Anwendung fehlschlaegt, notiere den benoetigten Syscall und erlaube ihn mit Bedacht.

Netzwerk und Volumes: Die Reichweite eines Containers begrenzen

Isolation endet nicht bei Prozessen. Auch das Netzwerk muss konfiguriert werden. Standardmaessig koennen Container ueber ein Bridge-Netzwerk miteinander kommunizieren. Wird ein Container kompromittiert, kann ein Angreifer andere Container im selben Netzwerk scannen. Auf einem Home-Server laesst sich das meist mit einem benutzerdefinierten Netzwerk begrenzen oder, falls nicht benoetigt, durch vollstaendigen Verzicht auf Netzwerkzugriff.

# Container tanpa akses jaringan (misalnya worker lokal)
docker run -d --network none myworker

# Container hanya bisa diakses lewat reverse proxy
docker run -d --network my-private-net --name webapp myapp
docker run -d --network my-private-net --name db postgres

Auch Volumes brauchen Aufmerksamkeit. Binde keine sensiblen Host-Ordner in einen Container ein, ausser es ist wirklich erforderlich. Falls es erforderlich ist, nutze die Read-only-Option (:ro), damit der Container ihren Inhalt nicht veraendern kann.

docker run -d \
  -v /home/adam/project:/app:ro \
  -v /home/adam/project/data:/app/data:rw \
  myapp

Zusaetzliche Praktiken, die leicht vergessen werden

Neben den drei Schichten oben gibt es einige kleine Gewohnheiten, die ich anzuwenden versuche:

  • Host-Ports nur bei Bedarf veroeffentlichen. Verwende fuer eine Datenbank nicht -p 3306:3306, wenn nur andere Container Zugriff brauchen. Nutze stattdessen ein internes Netzwerk.
  • Minimale Images verwenden. alpine, distroless oder slim reduzieren die Angriffsoberflaeche. Je weniger Werkzeuge ein Image enthaelt, desto weniger koennen missbraucht werden.
  • Images regelmaessig scannen. Werkzeuge wie trivy oder grype koennen CVEs in Image-Abhaengigkeiten erkennen.
  • Secrets rotieren. Keine Passwoerter, API-Schluessel oder Tokens in Images fest einbauen. Verwende Umgebungsvariablen oder Secret-Management.
  • Laufzeit und Kernel aktualisieren. Container-Sicherheit haengt stark vom Kernel ab. Halte den Host-Kernel aktuell.

Fazit

Container machen das Deployment von Anwendungen auf einem Home-Server ordentlicher, aber ordentlich ist nicht gleich sicher. Die drei grundlegendsten Schritte, die sich sofort umsetzen lassen, sind: Container als Nicht-Root ausfuehren, nicht benoetigte Capabilities drop und seccomp zur Begrenzung von Systemaufrufen in Betracht ziehen. Zusammen mit strikten Netzwerk- und Volume-Einstellungen senken sie das Risiko deutlich.

Bei Container-Sicherheit geht es nicht darum, einen Server zu 100 % uneinnehmbar zu machen; das ist praktisch unmoeglich. Es geht darum, Angreifern mit mehreren Einschraenkungsschichten das Leben schwer zu machen, damit sich die Auswirkungen eines kompromittierten Containers nicht sofort auf das gesamte System ausbreiten.

Auf meinem eigenen Home-Server habe ich inzwischen jeden laufenden Container auf einen Nicht-Root-Benutzer und Capability-Dropping umgestellt. Der Prozess ist schrittweise, aber es beruhigt zu wissen, dass jeder Bewohner dieses digitalen Apartments nur einen Schluessel zu seinem eigenen Zimmer hat.

Hast du Container-Sicherheitspraktiken, die du auf einem Home-Server regelmaessig einsetzt? Oder hoerst du gerade zum ersten Mal von rootless Docker und seccomp? Schreib es in die Kommentare; ich lerne auch gern aus deinen Erfahrungen.