Infrastruktur
Wie aus einem Einbruch eine Prüfregel wurde
Über eine Lücke schrieb sich ein fremder Prozess auf ein System, das wir betreiben. Aus dem Fund wurde eine Härtungsregel, die seither auf jedem Container läuft. Genau dieser Angriff läuft jetzt überall ins Leere.
Jeder Container, den wir betreiben, läuft nach derselben Regel: Das Dateisystem ist unveränderbar, die wenigen beschreibbaren Pfade dürfen nichts ausführen. Diese Regel gibt es, weil wir einmal genau gesehen haben, was passiert, wenn sie fehlt. Hier ist die Geschichte, wie aus einem Vorfall eine Prüfung wurde, die den Angriff überall ins Leere laufen lässt.
Über eine Sicherheitslücke in einem Web-Framework fand ein fremder Prozess seinen Weg auf ein System, das wir betreiben. Der Ablauf ist ein Klassiker und immer derselbe: Die Lücke erlaubt es, von außen Code auszuführen. Der Angreifer nutzt das, um ein kleines Programm nachzuladen, es irgendwo abzulegen und zu starten, meist einen Kryptominer, der fremde Rechenleistung verheizt. In diesem Fall schrieb er sich nach /tmp und führte sich von dort aus.
Zwei Dinge braucht so ein Angriff zwingend: einen Ort, an den er schreiben kann, und die Erlaubnis, das Geschriebene auszuführen. Nimm ihm eines von beiden, und der ganze Ablauf bricht ab, egal über welche Lücke er reingekommen ist. Genau da setzt die Regel an.
Die Regel
Ein Container muss zur Laufzeit fast nichts an sich selbst ändern. Die Anwendung ist beim Bauen fertig geworden; im Betrieb liest sie, rechnet, antwortet, aber sie schreibt sich nicht selbst um. Also darf sie das auch nicht. Konkret sieht das so aus:
services:
app:
read_only: true # Wurzel-Dateisystem unveränderbar
tmpfs:
- /tmp:noexec,nosuid,size=100m # beschreibbar, aber nicht ausführbar
security_opt:
- no-new-privileges:true # keine Rechte-Eskalation
cap_drop:
- ALL # keine Sonderrechte des Kernels
user: "10001" # niemals als root
mem_limit: 512m # Ausreißer wird gedeckelt, nicht der ganze Host
pids_limit: 512 # keine Fork-Lawine
Jede Zeile hat einen Grund, und die zwei entscheidenden sind die ersten beiden. read_only: true macht das Wurzel-Dateisystem des Containers unveränderbar. Es gibt keinen Ort mehr, an den sich ein nachgeladenes Programm dauerhaft schreiben könnte. /tmp als noexec-tmpfs lässt genau einen Arbeitsbereich beschreibbar, verbietet dort aber das Ausführen. Der Miner darf sich also theoretisch nach /tmp legen, aber der Versuch, ihn zu starten, wird vom System abgelehnt. Damit ist der Angriff, den wir gesehen haben, tot, bevor er beginnt: Er braucht Schreiben und Ausführen am selben Ort. Diese Kombination gibt es nicht mehr.
Der Rest sind die Sicherheitsnetze für den Fall, dass doch einmal Code läuft. no-new-privileges und cap_drop: ALL nehmen dem Prozess jede Möglichkeit, sich mehr Rechte zu holen, als er ohnehin hat. Der nicht-root-Benutzer sorgt dafür, dass ein kompromittierter Container nicht gleich einen Administrator im Container bedeutet. Und die Grenzen für Speicher und Prozesszahl stellen sicher, dass ein Ausreißer, ein Leck, ein Miner-Wächter, der sich tausendfach klont, gezielt gestoppt wird, statt den ganzen Host in die Knie zu zwingen und dabei fremde, gesunde Dienste mitzureißen.
Warum das eine Prüfregel ist und kein Einzel-Fix
Man hätte den einen Vorfall auch einfach wegräumen können: Lücke schließen, System säubern, weitermachen. Das haben wir auch getan. Aber ein Fix, der nur an einer Stelle greift, ist wenig wert, wenn nebenan zehn andere Container nach demselben alten Muster laufen. Der eigentliche Wert steckt darin, aus dem Einzelfall eine Regel zu machen, die überall gilt.
Also wurde die Härtung zur Baseline für jeden selbst betriebenen Container, gestaffelt ausgerollt und pro Container getestet, denn manche Anwendungen schreiben legitim Cache, Sitzungen oder Logs, und diese Pfade müssen als eigene, ebenfalls nicht ausführbare Bereiche nachgezogen werden, bevor man das Dateisystem zusperrt. Dazu kam die zweite Lehre aus dem Vorfall: die Zugriffs-Protokolle des vorgelagerten Routers flottenweit einschalten. Beim Vorfall fehlten uns zunächst genau diese Request-Logs, um den Weg des Angreifers lückenlos nachzuzeichnen. Seitdem schreibt jeder Router mit, als forensische Grundlage und als Frühwarnung.
Das ist der Kern, wie wir arbeiten: Unser Wissen liegt in Systemen, nicht in Köpfen. Ein Vorfall, den man nur behebt, ist eine verlorene Nacht. Ein Vorfall, aus dem eine Regel wird, die auf jedem Container läuft, ist eine Verteidigung, die von da an für jeden Kunden mitarbeitet, auch für den, dessen System nie angegriffen wurde.
Was das für einen Betrieb bedeutet
Keine Software ist frei von Lücken, und die nächste Schwachstelle in irgendeinem Framework kommt bestimmt. Die interessante Frage ist nicht, ob jemand an die Tür klopft, sondern was passiert, wenn er drin ist. Bei einem gehärteten Container ist die Antwort: erstaunlich wenig. Er findet keinen Ort zum Schreiben, keine Erlaubnis zum Ausführen, keine Rechte zum Eskalieren und keinen Weg, den Host mitzureißen. Der Angriff, der uns die Regel gelehrt hat, läuft heute auf jedem unserer Systeme ins Leere.
Wie die Härtungs-Baseline im Detail aussieht, steht auf koetting.io/de/betrieb/haertung. Wer den ganzen Betrieb zum Festpreis in eine Hand geben will, findet das Angebot unter koetting.io/de/betrieb.