Sandbox ohne Netz: gVisor, runsc und die Kunst, einem Scanner das Internet wegzunehmen
Container teilen sich den Host-Kernel. Für eine Malware-Scan-Pipeline ist das zu wenig. Warum gVisor als User-Space-Kernel die richtige zweite Schicht ist und wie ein Scanner gänzlich ohne Netzwerk-Egress gebaut wird.
Problem
Linux-Container sind leichtgewichtig, weil sie sich mit dem Host einen Kernel teilen. Namespaces, Cgroups und Seccomp trennen die Sichtbarkeit, aber die Ausführungsumgebung bleibt derselbe Linux-Kernel. Ein Kernel-Exploit aus einem Container heraus (Dirty Pipe, Dirty Cred, und viele andere der letzten Jahre) bricht die Isolation und landet direkt auf dem Host.
Für die meisten Web-Anwendungen ist das hinnehmbares Restrisiko. Für einen Malware-Scanner ist es untragbar. Der Scanner öffnet per Definition Dateien, die potentiell darauf ausgelegt sind, genau solche Lücken auszunutzen. Ein Container-Breakout beim Öffnen eines eingeschleusten Samples ist keine theoretische Möglichkeit, sondern ein realistisch zu erwartendes Szenario.
Kurze Antwort
gVisor (Google, Open Source, Apache 2.0) schiebt eine zweite Kernel-Schicht zwischen den containerisierten Prozess und den Host-Kernel. Die Komponente runsc ersetzt den OCI-Runtime und startet den Container mit einem eigenen User-Space-Kernel (Sentry), der Syscalls abfängt und auf einem minimalen Subset echter Host-Syscalls abbildet. Ein Kernel-Exploit im Container trifft auf Sentry, nicht auf den Host-Kernel.
Kombiniert mit network_mode: none (der Container bekommt überhaupt keine Netzwerk-Schnittstelle) ergibt sich ein Scanner, der ausgeführt werden kann, ohne dass ein erfolgreicher Breakout ins Internet exfiltrieren könnte.
Tiefgang
Was gVisor im Kern anders macht
Ein normaler Container macht einen Syscall über die Kernel-Schnittstelle des Hosts. gVisor hängt sich zwischen: der Anwendungs-Syscall wird per ptrace oder per KVM abgefangen und an Sentry weitergereicht, einen in Go geschriebenen User-Space-Kernel. Sentry implementiert Hunderte Linux-Syscalls selbst und ruft nur eine kleine Kernmenge an echte Host-Syscalls auf, um I/O und Speicher zu managen.
Das hat zwei Konsequenzen:
- Die Angriffsfläche gegen den Host-Kernel schrumpft drastisch. Der Container kann in Sentry so viel bohren wie er will, aber Sentry selbst ruft nur wenige, gut geprüfte Host-Syscalls auf.
- Nicht alle Syscalls sind in Sentry implementiert. Manche Anwendungen brechen beim Umstieg.
Datei-I/O läuft über einen zweiten Prozess namens Gofer, der als Dateizugriffs-Proxy für Sentry fungiert. Gofer hält die wenigen Dateihandles, Sentry bekommt nur die Daten.
Performance-Charakteristik
gVisor ist langsamer als runc. Die typische Bandbreite:
- Rein CPU-gebundene Arbeitslasten (Hashing, Komprimierung, Krypto): 10 bis 20 Prozent Overhead.
- Syscall-intensive Workloads (viele kleine File-I/O-Operationen, Socket-Verbindungen): 30 bis 60 Prozent Overhead.
- Netzwerk-intensive Workloads: variabel, mit
--network=hostbzw. geteiltem Network-Stack besser, mit eigenem Network-Stack schlechter.
Für einen Scanner, der pro Sample einige Sekunden bis Minuten an CPU-Arbeit verrichtet und nur gelegentlich I/O macht, ist der Overhead in der Praxis verschmerzbar.
Einbindung in docker-compose
Ein Service unter runsc:
# compose.yml
services:
scanner:
image: myscanner:latest
runtime: runsc
network_mode: none
read_only: true
tmpfs:
- /work:rw,size=512M,mode=0700,nodev,nosuid,noexec
cap_drop: [ALL]
security_opt:
- no-new-privileges:trueDie Kombination der Direktiven:
runtime: runscwählt gVisor statt runc.network_mode: nonegibt dem Container keine Netzwerkschnittstelle, nicht einmal loopback.read_only: truemacht das Wurzel-Dateisystem lesend; Schreibpfade sind explizit per tmpfs zu gewähren.tmpfsunter/worklegt den Arbeitsbereich in den RAM, mitnoexecgegen Tropfen-Ausführung.cap_drop: [ALL]entfernt alle Linux-Capabilities.no-new-privilegesverhindert Privilege-Eskalation via setuid-Binaries.
Ein Scanner mit dieser Konfiguration kann ein Sample öffnen, analysieren, Ergebnisse in tmpfs schreiben und per Pipe oder shared volume an den übergeordneten Worker übergeben. Exfiltrations-Pfade ins Internet existieren im Normalbetrieb nicht.
Wann eine einzelne Egress-Insel sinnvoll ist
Signaturen müssen aktualisiert werden. ClamAV-Feeds, SaneSecurity, YARA-Forge, all das braucht Internet-Zugang zu externen Quellen. Die Lösung ist ein eigener, deutlich kleinerer Container (ein Update-Daemon), der als einziger Teil der Pipeline Zugang zu einer expliziten Allowlist hat: nur diese Domains, nur HTTPS, nichts sonst. Er schreibt heruntergeladene Signaturen in ein Volume, aus dem die Scanner read-only lesen.
Die Scanner selbst bleiben network_mode: none. Der Update-Container liegt mit seiner internen Signatur-Ablage auf einem separaten internen Netz und ist nach aussen per Firewall-Regel auf genau die erlaubten Domains eingeschränkt.
Was gVisor nicht ersetzt
gVisor ist ein zweiter Verteidigungsring, kein Ersatz für Härtung im Inneren des Containers. Seccomp-Profile, minimal-privilegierte User-Accounts, read-only Root, Capabilities-Drop bleiben Pflicht. Die Schichten ergänzen sich: Seccomp engt ein, was der Container überhaupt anfragt; gVisor fängt ab, was doch durch die Maschen schlüpft.
Abgelehnte Alternativen und Mythen
"runc mit Seccomp reicht." Seccomp filtert Syscalls, lässt aber eine grosse Oberfläche gegen den Host-Kernel offen. Für gemeinsam genutzte Web-Services ausreichend, für Malware-Scanner zu schmal.
"Firecracker wäre besser, das ist eine richtige micro-VM." Firecracker (AWS Lambda, Fly.io) startet eine kleine KVM-VM pro Workload. Die Isolation ist stärker als gVisor (echter Hypervisor), der Setup-Aufwand aber merklich höher und die Start-Latenz pro Job grösser. Für einen Scanner mit hohem Durchsatz kann gVisor das pragmatischere Mass sein; für wirklich sensible Einzel-Jobs ist Firecracker die bessere Wahl.
"Kata Containers sind dasselbe." Kata startet ebenfalls eine leichte VM pro Container (über QEMU oder Cloud Hypervisor). Stärkere Isolation als gVisor, aber auch höherer Overhead. Im Umfeld von OCI-kompatiblen Setups eine valide Alternative.
"Volle Hypervisor-VMs pro Scan." Funktioniert, kostet aber Sekunden Startzeit pro Sample. Für niedrige Volumina akzeptabel, für Massen-Scans zu teuer.
"Nabla, sandboxing via unikernel." Forschungsprojekt, produktionsreife Werkzeuge fehlen.
Wie Svelnor hier hilft
Svelnor Scan und Svelnor Clean laufen ihre Engines und Konvertierer in genau der hier skizzierten Konfiguration: gVisor-Runtime, network_mode: none, read-only Root, tmpfs-Arbeitsbereich mit noexec, cap_drop ALL, no-new-privileges. Signatur-Updates (ClamAV, yara-forge) laufen über einen getrennten Update-Daemon mit enger Domain-Allowlist; die Scanner selbst sehen nie das Internet. Das geplante Svelnor Observatorium erweitert das Muster um dynamische Detonation in Firecracker-VMs und ist der Folgeschritt zu rein statischer Analyse.
Verifikation
- gVisor-Projekt: gvisor.dev.
- USENIX ATC 2019 Paper: "The True Cost of Containing" (Young et al.), Analyse der gVisor-Architektur und -Performance.
- OCI-Runtime-Spezifikation: opencontainers.org.
- Testen:
docker run --runtime=runsc --network=none hello-world;docker inspect $(container) | jq '.[0].HostConfig.Runtime'. - Performance-Benchmarks: öffentlich von Google (eigenes Blog), Cloudflare (Einsatz in Workers), von externen Forschungsgruppen.
Offene Punkte
Syscall-Kompatibilität. Sentry implementiert die wichtigsten Linux-Syscalls, aber nicht alle. Manche Workloads (insbesondere solche mit unüblichen Systemfunktionen wie perf_event_open, kexec, io_uring) brechen unter gVisor. Testen, bevor eine Scan-Pipeline live geht.
Kernel-Update von Sentry. gVisor selbst ist Software mit eigenen CVEs. Der Update-Pfad für runsc ist ebenso wichtig wie für den Host-Kernel.
Beobachtbarkeit. Standard-Tools wie strace, ftrace, perf verhalten sich in gVisor-Containern anders oder funktionieren nicht. Wer Scanner debuggen muss, braucht alternative Wege (strukturierte Logs, Metriken, Audit-Logs auf dem Host-Volume).