
🌐 Auch auf: English · Français · Español
Ein Werkzeug zur Integritätsprüfung ist nur so gut wie die Mails, die es schickt. Meldet es jeden Morgen hunderte Änderungen, liest sie nach einer Woche niemand mehr – und dann geht auch der eine echte Fund unter. Genau das war mein Problem mit AIDE und rkhunter auf den drei K3s-Hosts: Beide funktionierten, aber sie meldeten vor allem Dinge, die ich selbst verursacht hatte.
Dieser Deep Dive zeigt, was die beiden Werkzeuge tun, wo auf einem Kubernetes-Host der Lärm herkommt und wie die Prüfbasis mit jeder gewollten Änderung mitwandert.
🧾 Was AIDE macht
AIDE (Advanced Intrusion Detection Environment) legt eine Datenbank an: Für jede überwachte Datei speichert es Prüfsummen, Rechte, Besitzer, Größe und Zeitstempel. Beim täglichen Check vergleicht es das Dateisystem mit dieser Basis. Ändert jemand ein Programm in /usr/bin oder eine Konfiguration in /etc, fällt das auf – auch wenn der Angreifer seine Spuren im Log verwischt hat.
Bei mir läuft der Check einmal täglich und schickt nur dann eine Mail, wenn AIDE tatsächlich Unterschiede findet. Zwei Details machen ihn alltagstauglich:
- Mehrere Threads: AIDE 0.19 kann parallel hashen. Mit einem Thread dauerte ein Durchlauf 38 Sekunden bis anderthalb Minuten, jetzt läuft ein Thread pro vCPU, mit niedriger Priorität.
- Der Exit-Code ist eine Bitmaske: 1 = Dateien hinzugekommen, 2 = entfernt, 4 = geändert. Werte von 1 bis 7 bedeuten also „Unterschiede gefunden“, erst ab 14 liegt ein Fehler von AIDE selbst vor. Wer das wie einen normalen Fehlercode behandelt, bricht genau dann ab, wenn AIDE seine Arbeit gemacht hat – so ist es mir in der ersten Version passiert.
Was man mitnimmt: AIDE sagt nicht „gut“ oder „böse“, sondern „anders als gestern“. Ob das ein Befund ist, hängt davon ab, ob die Basis aktuell ist.
🐳 Woher auf einem K3s-Host der Lärm kommt
Ein Kubernetes-Host verändert sich ständig, ohne dass etwas Verdächtiges passiert. Drei Stellen haben bei mir regelmäßig Fehlalarme erzeugt:
/var/log/pods: K3s legt für jeden Pod-Start und jeden CronJob-Lauf ein Unterverzeichnis an. Das ändert die Linkanzahl des Elternverzeichnisses – AIDE meldete das an praktisch jedem Tag./var/log/containers: dort liegt pro Container ein Symlink in/var/log/pods. Wird ein CronJob-Pod während des Checks aufgeräumt, zeigt der Link ins Leere, und die Mail begann mit „failed to access …“./boot/efi: ein FAT-Dateisystem, das AIDE unter SELinux gar nicht lesen darf. Übersprungen hat es die Partition ohnehin – aber jeder Lauf erzeugte eine Reihe von SELinux-Ablehnungen, 13 pro Lauf auf einem Host.
Die ersten beiden sind kein Ziel für Manipulationen: Es sind flüchtige Mitschnitte der Container-Ausgaben, keine Programme und keine Konfiguration. Sie fliegen deshalb aus der Überwachung, jeweils mit Begründung im Code. Bei /boot/efi reicht das Ausschließen allein nicht: AIDE fragt trotzdem per statfs() die Eigenschaften des Dateisystems ab. Ein kleines lokales SELinux-Modul erlaubt genau diesen einen Aufruf – Metadaten des Dateisystems lesen, keine einzige Datei darauf. Wer die EFI-Dateien prüfen will, kann das per rpm -V tun, denn sie stammen aus Paketen.
Was man mitnimmt: Ausnahmen sind in Ordnung, wenn jede eine Begründung hat. „Das nervt“ ist keine – „das ist flüchtig und kein Angriffsziel“ schon.
🔄 Die Basis muss mitwandern
Der größere Teil der Fehlalarme kam aber von mir selbst. Jede gewollte Änderung – ein Paket-Update, eine neue Firewall-Regel, ein neues K3s-Binary – ist für AIDE zunächst eine Abweichung. Auf meinen Hosts gibt es drei Wege, auf denen sich überwachte Dateien ändern, und jeder braucht seinen eigenen Abgleich:
- Ansible-Läufe: Die letzte Rolle jedes Playbooks heißt
common_aide_refresh. Sie ruftaide --updateauf, prüft und schreibt die neue Datenbank in einem einzigen Durchgang. Früher waren es ein Check und ein Update hintereinander, also zwei komplette Scans pro Lauf. - Nächtliche Sicherheitsupdates: dnf-automatic installiert nachts Updates, ganz ohne Ansible. Nach einem Kernel-Wechsel meldete AIDE am nächsten Morgen 3.273 hinzugefügte, 3.273 entfernte und 364 geänderte Dateien – alter Kernel raus, neuer Kernel rein. Seitdem läuft um 04:00 ein eigener Abgleich, nach dem Update-Fenster und nach dem möglichen Neustart um 03:30.
- Änderungen von Hand: Wer außerhalb dieser beiden Wege etwas ändert, muss den Abgleich selbst anstoßen. Bei mir lautet die Regel: nach jedem Playbook-Lauf mit
--tagszusätzlich--tags common_rkhunter,common_aide_refresh– denn ein Lauf mit Tags überspringt die letzte Rolle.
Der dritte Weg ist der heikelste. Als ich zuletzt einen Kernel nachmittags von Hand eingespielt habe, hätte der Check um Mitternacht – vor dem Abgleich um 04:00 – genau diesen Kernel gemeldet.
Was man mitnimmt: Für jeden Weg, auf dem sich ein System ändert, braucht die Prüfbasis einen passenden Abgleich. Fehlt einer, kommt der Fehlalarm zuverlässig.
🦠 rkhunter: zweite Meinung mit eigenen Eigenheiten
rkhunter (Rootkit Hunter) arbeitet ähnlich, aber mit anderem Schwerpunkt: Er vergleicht wichtige Systemprogramme mit einer eigenen Eigenschaftsdatenbank und sucht nach Spuren bekannter Rootkits. Auch er braucht eine aktuelle Basis, die er mit rkhunter --propupd erzeugt.
Drei Dinge waren nötig, bis er Ruhe gab:
- Ein dnf-Plugin statt eines Hooks im Timer: Nach jeder dnf-Transaktion, die tatsächlich Pakete geändert hat, ruft ein kleines Plugin
rkhunter --propupdauf. Die erste Lösung hing am Dienst von dnf-automatic und griff deshalb nur bei nächtlichen Updates, nicht bei einemdnf upgradevon Hand. - Nur ein Scan pro Nacht: Das EPEL-Paket bringt ein eigenes Skript in
/etc/cron.dailymit. Zusammen mit meinem eigenen Cron-Eintrag liefen jede Nacht zwei vollständige Scans – und bei einer Warnung kamen zwei Mails. Das Paketskript ist weg, und eine rpm-Einstellung (%_netsharedpath) verhindert, dass das nächste rkhunter-Update es zurückbringt. - Abgeschaltete Tests mit Grund: Einige Tests passen nicht zu AlmaLinux 9 oder zu K3s – etwa der SSH-Konfigurationstest mit einem bekannten Fehler in rkhunter 1.4.6 oder die Modultests, die nach einem Kernel-Wechsel am falschen Pfad suchen. Jeder abgeschaltete Test steht mit Begründung in der Rolle.
Ein Detail fürs Drift-Prinzip: --propupd erneuert eine abgeleitete Datenbank, keine Konfiguration. Die Aufgabe meldet deshalb nie „changed“ – sonst sähe jeder Playbook-Lauf wie eine Abweichung aus.
Was man mitnimmt: Ein Paket bringt manchmal seine eigene Vorstellung vom Betrieb mit. Wer selbst plant, muss die mitgelieferte Planung bewusst abschalten – und dafür sorgen, dass sie nicht zurückkommt.
💓 Wer prüft den Prüfer?
Ein stiller Integritäts-Check ist nur dann gut, wenn er wirklich läuft. „Keine Mail“ kann zwei Dinge bedeuten: alles in Ordnung – oder der Job läuft gar nicht mehr. Deshalb schreibt jeder dieser Jobs am Ende einen Zeitstempel (einen Herzschlag) für Prometheus, und eine Alarmregel meldet sich, wenn einer davon zu alt wird.
Beim Einrichten der Herzschläge kamen zwei alte Fehler ans Licht:
- Auf einem Host lief eine von Hand angelegte zweite AIDE-Prüfung um 05:00, die in keiner Rolle und in keinem Repository stand – einfädig und doppelt.
- Mails an den nackten Empfänger
rootkamen nie an. Der Mail-Relay nahm sie an, aber msmtp kennt keine lokalen Aliase, und ohne Domain landete die Mail nirgends. Seitdem geht jede Meldung an eine ausgeschriebene Adresse.
Was man mitnimmt: Stille ist kein Beweis. Ein Prüfwerkzeug braucht selbst eine Prüfung, dass es läuft und dass seine Mails ankommen.
🎯 Die Lehren
- Ein Alarm muss etwas bedeuten – sonst wird er nicht mehr gelesen.
- Ausnahmen nur mit Begründung – flüchtige Container-Logs ja, „nervt“ nein.
- Jeder Änderungsweg braucht seinen Abgleich – Ansible, nächtliche Updates und Handarbeit.
- Mitgelieferte Zeitpläne prüfen – ein Paket kann einen zweiten Scan mitbringen.
- Den Prüfer überwachen – Herzschlag pro Job, Mails an echte Adressen.
Weitere Hintergründe zu SELinux auf diesen Hosts stehen im Deep Dive zu SELinux, die Überwachung der Jobs im Teil 6 der Ansible-Tour.

