Der Cryptominer, der zweimal kam

Acht Tage unbemerktes Mining, ein Patch, der keiner war, und fünf Deploy-Regeln, die seitdem jedes Go-Live blockieren. Ein Incident-Bericht.

Der Cryptominer, der zweimal kam

Kurzfazit: Ein Cryptominer lief acht Tage unbemerkt in einem öffentlich erreichbaren Container — weil das Metrik-System null Alarm-Transporte hatte. Nach dem Rebuild auf die „aktuelle" Version kam er nach drei Tagen wieder: Der Patch hatte die Lücke nie geschlossen. Was aus dem Incident wurde: fünf harte Deploy-Regeln, die seitdem jedes Go-Live blockieren.

Praxisserie „Agentic Ops", Folge 2. Transparenz: Alle Folgen basieren auf realen Vorfällen aus meiner eigenen Infrastruktur; Details, die Rückschlüsse auf interne Systeme erlauben, sind verallgemeinert. CVE- und Malware-Details sind öffentliches Wissen.

Im Juni meldete mein Proxmox-Host ungewöhnliche CPU-Temperaturen. Tctl 76 °C, dauerhaft. Meine erste Vermutung: Sommer, Last, vielleicht ein Lüfterproblem. Die Realität: In einem Docker-Container auf meinem Cluster lief seit gut einer Woche ein XMRig-Cryptominer mit knapp 400 % CPU — getarnt als Prozess namens cpu-logind.

Das ist die Geschichte, wie er reinkam, warum er wiederkam, und welche fünf Dinge sich danach dauerhaft geändert haben.

Akt 1: Der Fund

Der Miner saß im Container eines Open-Source-Umfrage-Tools (Formbricks), das ich öffentlich erreichbar betrieb. Das Schadbild war Lehrbuch:

  • XMRig-Binary in /var/tmp, RandomX, Verbindung zu einem Mining-Pool über Port 443 mit TLS — für Firewalls, die nur Ports filtern, unsichtbar.
  • Persistenz im Container: ein @reboot-Cronjob, achtfach dupliziert.
  • Ein Backdoor-User namens pakchoi mit NOPASSWD-sudo — der von einem Cron alle 30 Minuten neu angelegt wurde, falls man ihn löscht.

Wichtig fürs Verständnis: Der Container-Host darunter war sauber. Kein Ausbruch, keine Host-Persistenz. Die Kompromittierung kam durch die App — eine Remote-Code-Execution in der öffentlich erreichbaren Anwendung selbst, nicht durch einen offenen Port daneben.

Warum acht Tage unbemerkt? Weil meine Monitoring-Kette an der letzten Meile riss: Das Metrik-System sah die CPU-Last die ganze Zeit — aber es hatte null konfigurierte Alarm-Transporte. Ein Dashboard, das niemand anschaut, ist kein Monitoring. Es ist Deko.

Akt 2: Der Patch, der keiner war

Aufräumen nach Standard-Drehbuch: Datenbank-Dump gesichert und geprüft (keine Rogue-Accounts, keine fremden API-Keys), Container komplett neu gebaut, Image von Version 3.6 auf die damals aktuellste 3.17 gehoben, sämtliche Secrets rotiert, App-Port per iptables so eingesperrt, dass nur noch der Reverse-Proxy ihn erreicht. Dienst wieder online, Häkchen dran.

Drei Tage später war der Miner wieder da. Gleicher Container, gleiches Binary, gleicher Pool, gleicher Account. Diesmal noch ohne Backdoor-User — er war frisch gedroppt, die Zombie-Prozess-Timeline zeigte mehrere neue Exploitation-Wellen.

Das war der eigentliche Lernmoment: Die Binary gehörte dem App-User (uid 1001). Der Angreifer kam nicht an meinem Port-Lockdown vorbei und nicht am Reverse-Proxy — er kam durch die App-Lücke, durch den Proxy hindurch, wie jeder legitime Besucher auch. Und die Lücke — ein JWT-Signatur-Bypass, CVSS 9.4 — war in Version 3.17 nicht geschlossen. Der Fix existierte erst in einem späteren Major-Release, dessen Upgrade-Pfad einen kompletten zusätzlichen Infrastruktur-Stack verlangt hätte.

„Wir haben auf die neueste Version gepatcht" und „die Lücke ist zu" sind zwei verschiedene Aussagen. Ich hatte die erste geprüft und die zweite geglaubt.

Akt 3: Die Entscheidung

Konsequenz: Das Produkt flog raus. Kein drittes Katz-und-Maus, kein Major-Upgrade mit neuem Stack für ein Tool, das mir zweimal Miner ins Haus geholt hat — ersetzt durch eine schlankere Alternative, verseuchte Images gelöscht, Malware-Sample mit Hash für die Akte gesichert.

Das ist keine Niederlage, sondern Portfolio-Hygiene: Ein Dienst, dessen Sicherheitsgeschichte man nicht versteht, ist kein Dienst, sondern eine Verbindlichkeit.

Welche Regeln sind aus dem Incident entstanden?

Der Vorfall hat bei mir fünf Regeln hinterlassen, die seitdem für jeden öffentlich erreichbaren Dienst gelten — als Checkliste, die ein Deployment blockiert, nicht als fromme Absicht:

  • App-Services laufen nie als root. Eine App-RCE darf ein Ärgernis sein, nie ein Host-Totalschaden. (Der Miner blieb im Container — genau deshalb.)
  • Kein Go-Live ohne komplette Alarm-Kette. Monitoring-Systeme sind Pflicht — aber seit dem Incident wird auch geprüft, dass am Ende ein Mensch benachrichtigt wird. Der CPU-Alarm mit Telegram-Transport kam als Erstes.
  • Runtime-Detection auf allen Hosts. Falco meldet heute verdächtige Prozessmuster, neue Login-User werden fleet-weit überwacht — ein pakchoi würde jetzt Minuten leben, nicht Tage.
  • Versionsstand heißt: Lücke nachweislich geschlossen. Dependency- und Image-Scans laufen automatisiert; bei kritischen CVEs wird der Fix-Release verifiziert, nicht die Versionsnummer gefeiert.
  • App-Ports sind nur vom Front-Proxy erreichbar, Egress ist verdächtig. Der Pool-Traffic über 443 hat mir gezeigt: Port-Filter reichen nicht. Ausgehende Verbindungen von Servern, die nichts ausgehend zu tun haben, sind heute ein Alarmsignal.

Die Meta-Lektion

Ein Solo-Betreiber kann sich keinen Security-Analysten leisten, der acht Stunden Timeline-Forensik macht. Aber genau diese Arbeit — Prozessbäume lesen, Crontabs vergleichen, Ownership der Binaries prüfen, die „Patch ≠ Fix"-Recherche in den Release-Notes — ist heute mit einem KI-Agenten in einer Session machbar, inklusive sauber dokumentierter Beweiskette.

Was der Agent nicht abnimmt: die Entscheidung, ein Produkt rauszuwerfen. Und die Demut, dass die eigene Alarm-Kette acht Tage lang ein Dashboard ohne Zuschauer war.

Häufige Fragen

Wie merkt man, dass ein Server einen Cryptominer hat?

Typische Signale: dauerhaft hohe CPU-Last ohne erklärbare Ursache, erhöhte Temperaturen oder Stromverbrauch, unbekannte Prozesse mit harmlosen Namen (oft an Systemprozesse angelehnt), ausgehende TLS-Verbindungen zu unbekannten IPs, und Crontab-Einträge, die man nie angelegt hat. Der zuverlässigste Weg ist Runtime-Detection (z. B. Falco), die verdächtige Prozessstarts meldet, statt auf Symptome zu warten.

Reicht es, nach einem Befall das Image neu zu ziehen?

Nur, wenn der Einstiegsvektor geschlossen ist. Ein frischer Container mit derselben verwundbaren App-Version wird — wie in diesem Fall — binnen Tagen erneut kompromittiert, weil automatisierte Scanner das Internet permanent nach bekannten Lücken absuchen. Erst Vektor identifizieren und nachweislich schließen, dann neu bauen, dann Secrets rotieren.

Warum lief der Miner nicht als root gleich auf dem Host?

Weil der App-Prozess im Container als unprivilegierter User lief und der Container selbst unprivilegiert war. Der Angreifer bekam genau die Rechte der verwundbaren App — genug zum Minen, zu wenig für Host-Persistenz. Das ist der praktische Wert der Regel „nie als root": Sie verwandelt einen Totalschaden in ein ärgerliches, aber begrenztes Aufräumprojekt.


In eigener Sache

Folge 1 der Serie: Fünf Monitoring-Systeme, und keins hat gemerkt, dass die Datenbank leer ist. Folge 3: Wie ich einer KI beigebracht habe, meine Infrastruktur zu betreiben.

Die Go-Live-Checkliste aus diesem Artikel gibt es jetzt — zusammen mit drei weiteren Vorlagen — im Selfhost-Standards Pack (ab 79 €). Abonniere die KI-Rundschau, wenn du Updates zuerst erfahren willst. Und wenn du KI-Agenten in deinem Unternehmen produktiv einsetzen willst: ai.solavia.at.

× 📊 KI-Reality-Check
Wie nutzt der Mittelstand wirklich KI? 2 Min, anonym. Jetzt mitmachen →