CluPilotCloud/docs/superpowers/specs/2026-08-03-fruehwarnsystem-...

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.1 und ::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.