From 9deb5eca6062e85a4f638a9e9c27f8c6e3ff60e2 Mon Sep 17 00:00:00 2001 From: nexxo Date: Mon, 3 Aug 2026 12:12:38 +0200 Subject: [PATCH] Spec: Fruehwarnsystem gegen Anmeldeversuche MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Entworfen mit dem Betreiber. Vier Entscheidungen stehen fest: gesperrt wird die ADRESSE des Angreifers (nicht das Konto, nicht die Instanz), ab 10 Fehlversuchen in 10 Minuten, fuer eine Stunde mit Verdopplung bei Wiederholung, und aufgehoben wird von dem, den es angeht — der Inhaber im Portal, der Betreiber in der Konsole. Der Kern des Entwurfs ist eine nftables-Menge mit Ablaufzeit, deren Regel UNTER 'ct state established,related accept' steht. Damit ist 'wer drin ist, bleibt drin' eine Eigenschaft des Netzes und kein Versprechen in unserem Code. Und die Stunde laeuft im Kernel ab — eine Sperre kann nicht liegenbleiben, weil eine Warteschlange klemmt. Ehrlich benannt: auf einem fertig uebernommenen Host gibt es kein oeffentliches SSH mehr, der Host-Melder greift also enger als sein Name klingt. Und eine SSH-Verbindung je Instanz und Minute traegt heute, aber nicht bei hundert. Co-Authored-By: Claude Opus 5 --- .../2026-08-03-fruehwarnsystem-design.md | 256 ++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-03-fruehwarnsystem-design.md 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.