324 lines
14 KiB
Markdown
324 lines
14 KiB
Markdown
# 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.
|