
🌐 Auch auf: English · Français · Español
Gleicher Abend, andere Schicht des Stacks. 🥱➡️🤓 Nach dem kube-bench-Beitrag kam eine sehr berechtigte Nachfrage aus der Kommentarspalte, die nur in meinem eigenen Kopf existiert: „Okay, aber was ist mit der eigentlichen Linux-Kiste unter dem ganzen Kubernetes-Kram?“ Guter Punkt. kube-bench schaut immer nur auf den Cluster – API-Server-Flags, RBAC, Pod Security. Der AlmaLinux-9-Host, auf dem das alles läuft, hat nie dieselbe Behandlung bekommen. Also noch ein spätabendlicher Abstecher. 🐧🔍🧐 OpenSCAP und der CIS AlmaLinux 9 Benchmark in aller Kürze
OpenSCAP ist für Linux-Distributionen das, was kube-bench für Kubernetes ist – nur älter, etablierter und gestützt auf einen echten offiziellen Standard (SCAP, Security Content Automation Protocol) statt auf die meinungsstarke Checkliste eines einzelnen Herstellers. Red Hat liefert ein Paket namensscap-security-guide aus, das CIS, DISA STIG, PCI-DSS, HIPAA und einen ganzen Stapel weiterer Compliance-Profile als maschinenlesbare XCCDF-Inhalte bündelt, und oscap xccdf eval geht jede Regel eines gewählten Profils durch und meldet PASS, FAIL oder NOTAPPLICABLE. Gleicher Geist wie kube-bench: Hunderte pingelige, sehr konkrete Prüfungen – Dateirechte, Kernel-Parameter, PAM-Konfiguration, Mount-Optionen. Kein Schwachstellenscanner, sondern ein Konfigurationsprüfer. 🛠️ Ich habe es gegen das Profil CIS AlmaLinux OS 9 Benchmark, Level 1 – Server laufen lassen, zuerst nur lesend (--profile, kein --remediate), denn anders als eine Wegwerf-Test-VM liefert diese Kiste gerade genau den Blog aus, auf dem du das hier liest, und ich wollte das ganze Bild sehen, bevor ich irgendetwas im Live-Betrieb anfasse. 🙏🚦 Runde eins: 151 bestanden, 113 durchgefallen
293 Prüfungen insgesamt: ✅ 151 PASS, ❌ 113 FAIL, ⚪ 27 N/A, ⚠️ 2 ERROR. Ein deutlich schlechteres Verhältnis als auf der Kubernetes-Ebene – logisch, auf genau diesen Host hatte noch nie jemand einen CIS Benchmark gerichtet, während die k3s-Seite schon einen kompletten Durchgang hinter sich hatte. 113 sind eine Menge zum Sortieren von Hand, also hier die Tour: was behoben wurde, was mit Begründung ein klares Nein bekam, und – weil meine Abende offenbar jetzt einfach so laufen – die zwei wirklich interessanten „Moment, warum hat mich das gerade angelogen?“-Momente unterwegs. 🕵️1️⃣ 🗃️ AIDE – Dateiintegritätsüberwachung, die ich irgendwie nie hatte
Ich habe rkhunter für Rootkits und ClamAV für Malware, aber nichts, was merken würde, wenn jemand still und leise/etc/passwd bearbeitet oder ein System-Binary austauscht. AIDE (Advanced Intrusion Detection Environment) füllt genau diese Lücke: Es erstellt einmal Fingerabdrücke des Dateisystems, vergleicht dann regelmäßig mit dieser Baseline und schlägt Alarm, wenn sich etwas Unerwartetes geändert hat. Installiert, die initiale Datenbank gebaut (ein kompletter Durchlauf durchs Dateisystem, ~30 Sekunden, ~51.000 Dateien erfasst), eine tägliche Prüfung per Cron eingeplant. 📅 Kleiner Haken beim sauberen Erstellen der Baseline: Meine ersten beiden Versuche meldeten beide 9.279 entfernte Einträge, alle im Modulverzeichnis einer bestimmten alten Kernel-Version – obwohl dieses Verzeichnis sehr wohl noch auf der Platte existierte. Die Erklärung, nachdem ich ihr hinterhergejagt war: Diese SSH-Sitzung läuft mit ProtectKernelModules=yes aus der systemd-Härtung von sshd selbst (ja, genau die Härtung, die ich vor Monaten für genau diesen Blog dokumentiert habe), und das lässt /usr/lib/modules innerhalb einer interaktiven SSH-Sitzung komplett leer aussehen – eine private, gesandboxte Illusion, nicht die Realität. Die Datenbank hatte ich korrekt gebaut (mit Ausbruch aus der Sandbox per systemd-run), sie dann aber mit einem simplen aide --check direkt in meiner SSH-Sitzung beäugt – und der sah die vorgetäuschte leere Version und geriet wegen „fehlender“ Dateien in Panik, die nie wirklich gefehlt hatten. 🙈 Als ich die Prüfung auf dieselbe ausgebrochene Weise wie die Initialisierung wiederholt habe, kam sie sauber zurück: null Unterschiede. Die Lehre gilt weit über AIDE hinaus: Wenn eine Prüfung über SSH je etwas Bizarres über Kernel-Module oder /proc/sys meldet, frag dich erst, ob du gerade die Vorstellung der Sandbox vom Dateisystem siehst, bevor du es glaubst.2️⃣ 🧱 Netzwerk-Härtung auf Kernel-Ebene – und der eine sysctl, den ich bewusst NICHT angefasst habe
Das übliche Standardpaket ergänzt: ICMP-Redirects ablehnen, Source-Routed-Pakete verweigern, „Martian“-Pakete mit unmöglichen Absenderadressen loggen, Broadcast-ICMP-Echos ignorieren (Schutz vor Smurf-Angriffen), TCP-SYN-Cookies aktivieren,ptrace auf die eigenen Nachkommen eines Prozesses beschränken, volles ASLR. Alles langweilig, alles sicher, alles ausgerollt. ✅ Was ich nicht angefasst habe, mit Absicht und nach vorheriger Prüfung: net.ipv4.ip_forward und Reverse-Path-Filtering (rp_filter). CIS will Forwarding aus und striktes Reverse-Path-Filtering an. Mein Cluster braucht exakt das Gegenteil – Flannel und Calico brauchen IP-Forwarding, um Pod-Traffic überhaupt routen zu können, und ich fand den rp_filter dieses Hosts bereits auf 0 statt auf dem Distro-Standard 1, ziemlich sicher, weil striktes Reverse-Path-Filtering ein gut dokumentierter Weg ist, einen Kubernetes-Node legitimen Pod-Traffic stillschweigend verwerfen zu lassen. Eins davon „für die Compliance“ umzulegen, wäre der schnellste Weg gewesen, für ein Häkchen den ganzen Cluster vom Netz zu nehmen. Beides in Ruhe gelassen, und zwar wirklich getestet und verifiziert in Ruhe gelassen: Nach dem Anwenden des restlichen sysctl-Pakets hat ein kompletter sysctl --system-Durchlauf die interfacebezogenen rp_filter-Werte der CNI-Interfaces kurzzeitig trotzdem auf 1 zurückgesetzt (eine Standarddatei der Distro, die sich wieder durchsetzt), also habe ich sofort einen echten Rundlauf Pod → Pod → Datenbank über einen laufenden CronJob angestoßen, um zu prüfen, dass nichts kaputt ist. Kam sauber zurück. Ich habe ein Auge darauf behalten; zurückdrehen musste ich nichts, aber ich wollte die Belege haben, bevor ich diesen Satz schreibe, nicht nur die Theorie. 🧪3️⃣ 🔒 /tmp, /var/tmp und /dev/shm abriegeln – und das Installer-Skript, das dabei kaputtging
Alle drei per Bind-Remount mitnodev,nosuid,noexec eingehängt – aus einem für alle beschreibbaren Scratch-Verzeichnis sollte nichts Ausführbares laufen. Bevor ich den Schalter umgelegt habe, habe ich in meinem eigenen Ansible-Code nach allem gesucht, was eine Datei in /tmp ausführt, denn genau so etwas macht noexec stillschweigend kaputt. Einen Treffer gefunden: Der k3s-Installer-Bootstrap lädt das Installationsskript von get.k3s.io nach /tmp/k3s-install.sh herunter und führt es dann direkt über den Pfad aus. Direktes Ausführen braucht das Exec-Bit auf dem Mount; dieselbe Datei als Argument an sh zu lesen, nicht. Einzeiliger Fix (sh /tmp/k3s-install.sh statt /tmp/k3s-install.sh), bevor ich noexec eingeschaltet habe, damit eine künftige Neuinstallation mit genau diesem Playbook nicht leise auf einer Maschine scheitert, auf die ich nicht mal schaue. Nach der Änderung verifiziert: Lesen/Schreiben in /tmp geht weiterhin, kubectl apply -f /tmp/whatever.yml geht weiterhin (YAML wird als Daten gelesen, nicht ausgeführt), und ein absichtlich platziertes ausführbares Testskript bekam ein sauberes, korrektes „Permission denied“. 🎯4️⃣ 🪵 journald, Core Dumps, cron/at-Rechte, sudo-Härtung, Bluetooth, rsync
Das unspektakuläre, aber einfache Paket: journald schreibt jetzt dauerhaft auf die Platte (komprimiert, gedeckelt bei 500MB, damit aus „persistent“ nicht unbemerkt „füllt die Platte“ wird), Core Dumps sind überall deaktiviert, wo sie entstehen könnten (ein Core Dump ist nur ein Speicherabbild, und auf einer Kiste mit einer Datenbank und einem Admin-Login ist das ein Speicherabbild, das mitten im Vorgang ein Klartext-Passwort enthalten könnte – so etwas will ich nicht herumliegen haben, auch nicht lokal), die cron/at-Verzeichnisse haben strengere Rechte und eine explizite Allow-Liste bekommen, sudo loggt jetzt in eine eigene Datei, verlangt ein PTY und fragt jedes einzelne Mal neu nach dem Passwort, statt es zwischenzuspeichern. Bluetooth wurde maskiert (ein VPS hat keine Bluetooth-Hardware, der Dienst lief einfach grundlos), undrsync wurde deinstalliert, nachdem ich tatsächlich nachgesehen hatte – kein einziges Skript, kein Cronjob und kein Ansible-Task auf diesem Host verweist darauf. Falls ich es je wieder brauche, ist es nur ein dnf install entfernt. 🧹5️⃣ 🎭 Die SSH-Befunde, die schon längst stimmten, und der eine, den systemd-run nicht überlebt hat
Jetzt wird’s lustig. Mehrere SSH-Prüfungen meldeten FAIL für Einstellungen, bei denen ich mir ziemlich sicher war, dass sie längst passen – Einschränkungen für den Root-Login, hostbasierte Authentifizierung, Umgebungsoptionen. Ich habe die tatsächliche Live-Konfiguration von sshd doppelt und dreifach übersshd -T geprüft (der Befehl zeigt, was sshd wirklich durchsetzen würde, nicht nur, was irgendwo aufgeschrieben steht), sowohl aus einer normalen SSH-Sitzung als auch aus einer per systemd-run ausgebrochenen, um eine weitere Sandbox-Illusion wie die bei AIDE oben auszuschließen. Beide Male identische, korrekte Ergebnisse. Das war also keine Lüge der Sandbox – es stellte sich als echte Lücke anderer Art heraus: OpenSSH verhält sich bei einigen dieser Punkte standardmäßig schon sicher, aber ich hatte die Direktive nie explizit hingeschrieben, und ein strenger Compliance-Scanner prüft die Konfigurationsdatei wörtlich, nicht „na ja, der Standard ist zufällig in Ordnung“. Die drei fehlenden Zeilen direkt ergänzt – doppelt hält besser, und ein künftiges OpenSSH-Update, das einen Standard ändert, würde meine Absicherung nicht mehr unbemerkt verschieben. Einen habe ich bewusst genau so gelassen: CIS will PermitRootLogin no – Root-Zugriff per SSH komplett gesperrt, Punkt. Ich fahre PermitRootLogin without-password – nur mit Schlüssel, aber eben doch root. Auf dieser Kiste gibt es keinen separaten Admin-Account mit sudo-Rechten; Ansible verbindet sich direkt als root. Den Root-Login komplett abzuschalten, ohne vorher einen eigenen Admin-User einzuführen, wäre ein äußerst effizienter Weg gewesen, mich vom einzigen Zugang zu einem Server auszusperren, den nur ich administriere. Im Code vermerkt, in Ruhe gelassen. Und die wirklich technische Fußnote für alle, die diesen Ablauf nachbauen wollen: Den kompletten OpenSCAP-Scan selbst in systemd-run --wait --pipe zu verpacken (um der SSH-Sandbox auszuweichen, derselbe Trick wie beim AIDE-Fix), scheiterte zuverlässig mit „Remote peer disconnected“, sobald ich den SSH-Befehl, der ihn gestartet hatte, in den Hintergrund geschickt habe. Beste Vermutung: Die DBus-Sitzung von systemd-run hing an der Login-Sitzung, die PAM für diese SSH-Verbindung angelegt hatte, und sobald ich mich davon gelöst habe (selbst mit schlichtem nohup + disown auf Shell-Ebene), starb die DBus-Verbindung mit. Lang laufende systemd-run-Jobs und „über SSH anwerfen und weggehen“ vertragen sich offenbar nicht. Umgangen habe ich das, indem ich die Handvoll betroffener Regeln einzeln mit kurzlebigen systemd-run-Aufrufen geprüft habe, statt den ganzen minutenlangen Scan da durchzuzwängen.🤷 Was ich bewusst in Ruhe gelassen habe
Gleiches Prinzip wie im kube-bench-Beitrag: Ein rotes Ergebnis ist nicht automatisch ein To-do, und nichts von Folgendem gibt einem Angreifer irgendetwas, das er nicht ohnehin schon hätte:- 🔐 PAM / authselect / Passwortqualität / Kontosperr-Richtlinie – der größte Block verbleibender FAILs und der, den ich am bewusstesten noch nicht anfasse.
authselectmeldete auf diesem Host „no existing configuration detected“, PAM wurde hier also noch nie unter seine Verwaltung gestellt. Es jetzt zu aktivieren, heißt/etc/pam.d/system-authundpassword-authstrukturell umzuschreiben – geht das schief, könnte ich mich bei jedem lokalen Login, beisuund beisudoaussperren, auf einem Server, für den ich keinen bestätigten Zugang zu einer Rettungskonsole habe. SSH selbst läuft bereits nur mit Schlüssel, der tatsächliche Sicherheitsgewinn ist also bescheiden im Vergleich zu einem wirklich katastrophalen Risiko. Das bekommt ein eigenes, sorgfältig getestetes Wartungsfenster, keine Hauruckaktion am Donnerstagabend. - 🔑 GRUB2-Bootloader-Passwort – eine Maßnahme gegen physischen Konsolenzugriff, die bei einem gemieteten VPS größtenteils nicht greift und sich mit den eigenen Rettungskonsolen-Werkzeugen des Hosters beißen könnte.
- 🔏 Eigene systemweite Crypto-Policy für CIS – ändert, welche TLS-/SSH-Cipher die ganze Kiste akzeptiert. Echtes Potenzial, ohne eigene Tests die Kompatibilität mit irgendetwas zu zerschießen (ein alter Client, der den Blog aufruft, meine eigenen Werkzeuge). Ebenfalls keine Hauruckaktion am Donnerstagabend.
- 📡 systemd-journal-remote – CIS will es für zentrales Log-Shipping installiert haben. Ich habe nichts eingerichtet, was diese Logs empfangen würde. Einen Daemon zu installieren, den nichts nutzt, härtet gar nichts, er vergrößert nur die Angriffsfläche, ohne jeden Nutzen.




