diff --git a/docs/superpowers/specs/2026-08-03-fruehwarnsystem-design.md b/docs/superpowers/specs/2026-08-03-fruehwarnsystem-design.md new file mode 100644 index 0000000..ede3670 --- /dev/null +++ b/docs/superpowers/specs/2026-08-03-fruehwarnsystem-design.md @@ -0,0 +1,256 @@ +# 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 den bestehenden SSH-Weg. + +### 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 + 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 '' -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`, aus dem **System**-Postfach — nach dem Muster von +`NewDeviceSignInMail`. Nicht aus 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.) + +- **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. + +## 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 + +**Eine SSH-Verbindung je Instanz und Minute.** Bei der heutigen Größe ist das +nichts. Bei hundert Instanzen sind es hundert Verbindungen 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.