14 KiB
Frühwarnsystem: Angriffsversuche erkennen und die Adresse sperren
Stand: 2026-08-03 · entworfen mit dem Betreiber
Das Problem
Heute merkt niemand, wenn jemand an einer Kundeninstanz Passwörter durchprobiert. Nextcloud bremst zwar von sich aus, aber es sagt es keinem, es hört nie auf zu antworten, und die Adresse darf es weiter versuchen — Monate lang, wenn es sein muss. Auf der Host-Seite ist es dasselbe: der Betreiber erfährt von einem Anlauf gar nichts.
Gebaut wird deshalb dreierlei, nicht mehr: erkennen, die Adresse aus dem Netz nehmen, den Betroffenen benachrichtigen und ihm den Schlüssel zum Aufsperren geben.
Was ausdrücklich NICHT gebaut wird
- Kein Sperren von Benutzerkonten. Ein Angreifer, der Kontonamen durchprobiert, könnte sonst der Reihe nach die ganze Firma aussperren.
- Keine dauerhafte Sperre. Jede läuft ab. Ein System, das jemanden für immer aussperrt, sperrt irgendwann den Falschen für immer aus.
- Keine Software im Gast. Kein Agent, kein fail2ban, nichts, was mitinstalliert und mitgepflegt werden müsste.
- Kein Eingriff in laufende Sitzungen. Niemand wird ausgeloggt.
Die Entscheidungen des Betreibers
| Frage | Entscheidung |
|---|---|
| Was wird gesperrt? | Die Adresse des Angreifers — nicht das Konto, nicht die Instanz |
| Ab wann? | 10 Fehlversuche in 10 Minuten von derselben Adresse |
| Wie lange? | 1 Stunde, danach löst sie sich von selbst. Wiederholung verdoppelt (max. 24 h) |
| Wer erfährt es? | Instanz: Mail an den Inhaber, Eintrag für den Betreiber (ohne Mail). Host: Mail an den Betreiber |
| Wer hebt auf? | Instanz: der Inhaber im Portal — und der Betreiber in der Konsole. Host: nur der Betreiber |
Mitarbeiter kommen nicht ins Portal; sie melden sich ausschließlich an der Nextcloud an. Sie sehen von alldem nichts.
1. Erkennen
Zwei Quellen, beide nur gelesen, beide über bestehende Wege — und es sind
zwei verschiedene: eine Kundeninstanz wird über den Proxmox-Gastagenten
angesprochen (guestExec, so wie jeder occ-Aufruf im Produkt), ein Host per
SSH durch den Tunnel.
Kundeninstanz — die Nextcloud-Protokolldatei
Nextcloud schreibt jede gescheiterte Anmeldung als JSON-Zeile mit remoteAddr
und time. Gelesen wird ab einem gemerkten Byte-Versatz, damit jeder Lauf nur
das Neue sieht:
tail -c +<offset> data/nextcloud.log
Der Versatz steht als security_log_offset (bigint) an der Instanz. Ist die
Datei kleiner als der Versatz, wurde sie rotiert — dann wird bei 0 begonnen.
Gezählt werden nur Zeilen, deren message mit Login failed: beginnt.
Warum die Datei und nicht Nextclouds eigene Sperrtabelle: das Protokollformat
ist über Versionen hinweg stabil, occ security:bruteforce:* ist es nicht — die
Unterbefehle kamen und wechselten über die letzten Hauptversionen.
Host — die fehlgeschlagenen SSH-Anmeldungen
journalctl -u ssh -u sshd --since '<zuletzt gesehen>' -o cat
Gezählt werden Failed password und Invalid user, die Adresse steht in
derselben Zeile. Der Zeitpunkt steht als security_log_seen_at am Host.
Ehrlich zur Reichweite: auf einem fertig übernommenen Host ist Port 22 nur
noch aus dem Tunnel erreichbar (SecureHostFirewall). Von außen kann dort
niemand probieren. Dieser Melder greift für genau drei Fälle: während der
Übernahme, nachdem jemand das Notfallskript benutzt hat, und für alles,
was über 80/443 hereinkommt. Das ist weniger, als „Host-Angriffe erkennen"
klingt — es steht hier, damit es später niemanden überrascht.
Zeitfenster und Schwelle
Ein gleitendes Fenster von 10 Minuten. Ab 10 Fehlversuchen derselben Adresse wird gesperrt. Die Zählung passiert in der Anwendung, nicht im Gast: was gelesen wurde, wandert in eine kurzlebige Liste je Subjekt und Adresse.
Wer NIEMALS gesperrt wird
Eine Sperrliste, die sich selbst aussperren kann, ist eine Falle. Ausgenommen sind daher, hart und ohne Schalter:
- das WireGuard-Verwaltungsnetz (
10.66.0.0/24) — darüber erreicht CluPilot den Host überhaupt 127.0.0.1und::1- die öffentliche Adresse des CluPilot-Servers selbst
2. Sperren
Nicht in Nextcloud, sondern in der Host-Firewall. SecureHostFirewall
bekommt zwei benannte Mengen mit Ablaufzeit und eine Regel:
set clupilot_blocked { type ipv4_addr; flags timeout; }
set clupilot_blocked6 { type ipv6_addr; flags timeout; }
chain input {
…
ct state established,related accept
ip saddr @clupilot_blocked drop
ip6 saddr @clupilot_blocked6 drop
…
}
Die Reihenfolge ist die eigentliche Zusage. Die Sperrregel steht unter
ct state established,related accept. Wer schon verbunden ist, bleibt
verbunden — auf Paketebene, nicht als Versprechen in unserem Code. Genau das
war die Anforderung: „die bereits drinnen sind, bleiben angemeldet und merken
davon nichts."
Sperren und Aufheben sind je ein Befehl:
nft add element inet clupilot_filter clupilot_blocked { 203.0.113.7 timeout 3600s }
nft delete element inet clupilot_filter clupilot_blocked { 203.0.113.7 }
Die Ablaufzeit verwaltet der Kernel. Die Stunde läuft also auch dann ab, wenn CluPilot gerade steht — eine Sperre kann nicht liegenbleiben, weil eine Warteschlange klemmt.
Nach einem Neustart des Hosts ist die Menge leer (sie lebt im Speicher). Derselbe Zeitplan, der erkennt, trägt bei jedem Lauf die noch nicht abgelaufenen Sperren erneut ein — mit der Restlaufzeit, nicht der ursprünglichen. Das ist idempotent und braucht keinen eigenen Mechanismus.
Verdopplung bei Wiederholung
Kommt dieselbe Adresse am selben Subjekt innerhalb von 24 Stunden erneut
über die Schwelle, verdoppelt sich die Dauer: 1 h → 2 h → 4 h → 8 h → 16 h →
24 h als Obergrenze. Der Zähler steht als strikes am Datensatz.
3. Wer es erfährt und wer aufsperrt
Datenmodell
security_blocks:
| Spalte | Bedeutung |
|---|---|
uuid |
für Adressen in der Oberfläche |
host_id / instance_id |
genau eines von beiden gesetzt |
ip |
die gesperrte Adresse |
reason |
instance_login oder host_ssh |
attempts |
wie viele Fehlversuche die Sperre ausgelöst haben |
strikes |
die wievielte Sperre dieser Adresse an diesem Subjekt |
blocked_at, expires_at |
wann, und bis wann |
released_at, released_by |
wer sie vorzeitig aufgehoben hat, wenn jemand |
Cursor: instances.security_log_offset (bigint), hosts.security_log_seen_at.
Benachrichtigung
SecurityBlockMail, als eigene Mailart im Katalog, mit dem Zweck
System als Vorgabe — nach dem Muster von NewDeviceSignInMail. Nicht
Support: das ist kein Gespräch, und eine Antwort darauf soll kein Ticket öffnen.
(Nebenbei umgeht das auch das bekannte Problem des Support-Postfachs, dem der
SMTP-Benutzername fehlt.)
Über die Wegwahl aus Abschnitt 3a kann der Betreiber sie jederzeit auf ein anderes Postfach legen, ohne dass eine andere Mail mitwandert.
- Instanz gesperrt → an die Kontoadresse des Inhabers, mit Link auf die Sperrliste im Portal.
- Host gesperrt → an den Betreiber.
- Instanz gesperrt, aus Sicht des Betreibers → kein Mail, nur der Eintrag an der Instanz. So wollte es der Betreiber ausdrücklich.
Höchstens eine Mail je Instanz und Stunde. Ein Angreifer, der Adressen durchwechselt, erzeugt sonst zwanzig Mails, und die zwanzigste liest niemand mehr. Die weiteren Sperren stehen vollständig in der Liste.
Portal (Inhaber)
Eigene Seite Sicherheit mit der Liste: Adresse, wann, wie viele Versuche, wann sie von selbst abläuft. Je Zeile ein Knopf Sperre aufheben — der ein Bestätigungs-Modal öffnet (R23), nicht direkt handelt.
Der Inhaber sieht nur die Sperren seiner eigenen Instanzen. Nie die eines anderen Kunden, nie die des Hosts.
Konsole (Betreiber)
- Auf der Instanz-Detailseite ein Abschnitt mit denselben Zeilen und demselben Knopf.
- Auf der Host-Detailseite dasselbe für Host-Sperren.
- Auf der Übersicht ein Hinweis, solange irgendwo eine Sperre aktiv ist.
3a. Aus welchem Postfach eine Mail geht — je Mailart einstellbar
Warum das hier steht: der Betreiber will die Meldungen dieses Systems aus
einem eigenen Postfach schicken (etwa info@ oder später ein alert@), ohne
dass dafür jede andere Mail mitwandert. Heute geht das nicht: die Zuordnung
kennt nur fünf Zwecke, und alles, was denselben Zweck hat, geht aus
demselben Postfach.
Was es schon gibt
Mailbox— Postfächer mit Adresse, Anzeigename, Zugangsdaten. Fünf sind vorhanden:no-reply,support,billing,office,info.MailPurpose— fünf Zwecke, je einem Postfach zugeordnet (Settings).MailPreviews::all()— der Katalog aller 16 Mailarten mit lesbaren Namen, bisher nur für die Vorschau benutzt.
Was dazukommt
Eine Ebene über den Zwecken, kein Ersatz für sie:
Settings: mail.route.<mailart> → Postfach-Schlüssel (leer = wie der Zweck)
SendsFromMailbox::mailboxEnvelope() fragt zuerst die Wegwahl der Mailart und
fällt auf den Zweck zurück, wenn keine gesetzt ist. Bestehendes Verhalten
ändert sich damit nicht — ohne Eintrag geht alles genau wie heute.
Damit eine Mailart ihren eigenen Weg kennen kann, braucht sie einen Schlüssel.
Der Katalog in MailPreviews wird dafür zur einen Liste: er wandert in ein
Register (MailCatalogue), das Schlüssel, lesbaren Namen und Vorgabe-Zweck je
Mailart führt. Die Vorschau liest daraus, die Wegwahl liest daraus, und es gibt
keine zweite Liste, die man vergessen kann.
Oberfläche
Auf der bestehenden Seite Mail in der Konsole eine Tabelle: eine Zeile je Mailart, links der lesbare Name, rechts ein Auswahlfeld mit allen aktiven Postfächern und der Vorgabe „wie der Zweck (Support)". Gespeichert wie die Zwecke darüber.
Was hier NICHT gebaut wird
Postfächer anlegen, ändern und löschen. Die Seite kann heute nur zuordnen
und testen; ein neues alert@ lässt sich dort nicht erzeugen. Das ist ein
eigenes Projekt mit eigenen Fallstricken — ein Postfach zu löschen, auf das noch
drei Mailarten zeigen, darf nicht still geschehen. Bis dahin stehen die fünf
vorhandenen Postfächer zur Wahl, und das genügt für dieses System.
Prüfungen
- Ohne Eintrag geht jede Mail aus demselben Postfach wie vorher.
- Mit Eintrag geht genau diese eine Mailart woanders hin, alle anderen nicht.
- Ein Eintrag, der auf ein gelöschtes oder abgeschaltetes Postfach zeigt, fällt auf den Zweck zurück, statt die Mail zu verlieren.
- Jede Mailart im Katalog hat einen Vorgabe-Zweck; keine steht ohne da.
4. Wann es läuft
Ein Auftrag ScanForIntrusions, jede Minute über den Zeitplan, auf der
Provisionierungs-Warteschlange — nur die steht im Tunnel und erreicht Hosts
und Gäste.
Je Lauf: über alle aktiven Hosts und alle aktiven Instanzen; lesen, zählen, sperren, wieder eintragen was noch gilt.
5. Fehlerbehandlung
- Ein Gast antwortet nicht. Übersprungen, Versatz unverändert, keine Meldung. Ein Gast, der gerade neu startet, ist kein Vorfall.
- Ein
nft-Aufruf scheitert. Der Datensatz wird trotzdem angelegt und beim nächsten Lauf erneut versucht — das Wiedereintragen für den Neustartfall erledigt das ohnehin. Eine Sperre, die nur in der Datenbank steht, ist sichtbar; eine, die nur in der Firewall steht, wäre unsichtbar. - Das Protokoll ist unlesbar oder leer. Kein Fehler. Kein Zählstand, keine Sperre.
- Die Mail scheitert. Die Sperre bleibt. Zustellung ist nicht die Bedingung für Schutz.
6. Prüfungen
- Zehn Fehlversuche im Fenster erzeugen eine Sperre; neun nicht.
- Zehn Fehlversuche über zwei Fenster verteilt erzeugen keine.
- Eine Adresse aus dem Verwaltungsnetz wird nie gesperrt — auch nicht bei hundert Versuchen.
- Wiederholung verdoppelt, und die Obergrenze von 24 Stunden hält.
- Der erzeugte
nft-Aufruf enthält die Restlaufzeit, nicht die ursprüngliche Dauer. - Die Sperrregel steht im erzeugten Regelwerk unter
ct state established,related accept— als Test über die gerenderte Konfiguration, nicht über den Quelltext. - Ein Inhaber sieht die Sperren eines fremden Kunden nicht und kann sie nicht aufheben (403).
- Höchstens eine Mail je Instanz und Stunde.
- Ein rotiertes Protokoll (Datei kleiner als der Versatz) beginnt bei 0, statt ins Leere zu lesen.
7. Bekannte Grenzen
Ein Aufruf je Instanz und Minute über den Proxmox-Gastagenten, dazu eine SSH-Verbindung je Host. Bei der heutigen Größe ist das nichts. Bei hundert Instanzen sind es hundert Aufrufe pro Minute — dann braucht es einen anderen Weg (ein Melder im Gast, der meldet, statt gefragt zu werden). Das steht hier als bekannte Grenze, nicht als Überraschung für später.
Geteilte Adressen. Hinter einer Adresse können unbeteiligte Menschen sitzen — Büro-NAT, Mobilfunk. Sie kommen dann eine Stunde lang nicht neu hinein. Abgefedert durch: die kurze Dauer, den Knopf des Inhabers, und dass Angemeldete unberührt bleiben.
Kein Schutz vor langsamem Durchprobieren. Wer neun Versuche pro zehn Minuten macht, läuft unter der Schwelle durch. Das ist der Preis dafür, dass ein vertippter Mitarbeiter nicht ausgesperrt wird.
8. Was danach kommt (nicht Teil dieser Spec)
Die Mitarbeiterverwaltung im Portal — Konten anlegen mit Einladungsmail, Rechte setzen — ist ein eigenes Projekt mit eigener Spec. Sie hängt nicht an diesem hier und dieses nicht an ihr. Offen ist dort vor allem die Frage, wie ein Mitarbeiter an sein Passwort kommt, ohne dass es durch CluPilots Hände geht.