Enterprise-Backup-, Recovery-, Verification-, Security- und Monitoring-Plattform fuer Proxmox VE, Windows, Linux und Dateisysteme. Der Leitsatz, der fast jede Entscheidung erklaert: Ein Backup gilt erst als vertrauenswuerdig, wenn Integritaet geprueft und Wiederherstellbarkeit nachgewiesen wurde. Deshalb steigt ein Wiederherstellungspunkt erst nach einem tatsaechlich durchgefuehrten Restore-Test auf "recoverable", und Unbekanntes geht in keine Bewertung als "gut" ein. Umfang (Phasen 0-23): - Repository Engine: inhaltsadressierte Bloecke, atomares Commit-Protokoll, Katalogaufbau allein aus den Manifesten — ohne Datenbank - Backup Engine: inhaltsabhaengiges Chunking, Deduplizierung trotz Verschluesselung, zstd, AES-256-GCM, Streaming mit Gegendruck - Agenten fuer Windows und Linux mit Auftragsabholung (Pull-Modell) - Proxmox-Provider mit beiden Zugriffswegen auf die Sicherungsarchive - Scheduler, Recovery Engine mit Pruefpunkt, Verification, Unveraenderlichkeit - Weboberflaeche, Kennzahlen, Meldungen, Berichte, Security Center, Ransomware-Heuristik (meldet, handelt nie) - Disaster Recovery, Haertung, Leistungsmessung, Chaos Testing - Eingefrorene Vertraege fuer API, Migrationen, Backup-Format und Repository - Auslieferungspaket fuer linux/amd64, linux/arm64 und windows/amd64 Nicht enthalten und als solches gekennzeichnet: Kapazitaetsprognose, Backup Copy, Changed Block Tracking bei Proxmox, erweiterte Attribute und ACLs. Gebaut, aber nie auf echter Hardware gefahren: der Windows-Dienst, die systemd-Einheit und der verpflichtende Proxmox-Meilenstein — ob eine wiederhergestellte VM startet, ist ungeprueft. Einzelheiten in CHANGELOG.md und docs/release-candidate.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
180 lines
8.9 KiB
Markdown
180 lines
8.9 KiB
Markdown
# Härtung (Phase 19)
|
||
|
||
Elf Prüfarten, sieben Härtungsbereiche — und die Regel, die für die ganze Phase
|
||
gilt:
|
||
|
||
> Sicherheit wird nicht behauptet, sondern angegriffen.
|
||
|
||
Jeder Angriff wurde gegen die laufende Anlage geführt. Was abgewehrt wurde, ist
|
||
als Test festgeschrieben; was durchkam, ist behoben und hat einen
|
||
Regressionstest.
|
||
|
||
## Ergebnis der Angriffe
|
||
|
||
| Angriff | Ergebnis |
|
||
| --- | --- |
|
||
| SQL Injection | ✓ abgewehrt — pgx nutzt durchgehend Parameter, `users` blieb unversehrt |
|
||
| RBAC-Umgehung | ✓ abgewehrt — sechs schreibende Zugriffe als Viewer, alle 403 |
|
||
| Rechteausweitung | ✓ abgewehrt — sich selbst die Administratorrolle geben: 404/403 |
|
||
| Authentifizierung | ✓ abgewehrt — leeres, erfundenes und fremdes Token: 401 |
|
||
| Sofortiger Widerruf | ✓ nach Kontosperre sofort 401 (opake Tokens, Phase 1) |
|
||
| XSS | ✓ abgewehrt — Go maskiert `<` in JSON, dazu `nosniff` und CSP `default-src 'none'` |
|
||
| Path Traversal (Manifest) | ✓ abgewehrt — `validateManifestPath` aus Phase 5 |
|
||
| **Wiederherstellungsziel** | ✗ `/etc` galt als durchführbar |
|
||
| **SSRF** | ✗ Webhook auf Metadatendienst, localhost, privates Netz |
|
||
| **Rate Limit** | ✗ nur der Login war begrenzt |
|
||
| **TLS** | ✗ nicht vorhanden |
|
||
|
||
## Die vier Funde
|
||
|
||
### Wiederherstellung nach /etc
|
||
|
||
Der Angriff braucht keine Lücke im Code: Wer Backups zurückschreiben darf, gibt
|
||
`/etc` als Ziel an. Der Lauf schreibt dann eine Datei nach `/etc/cron.d/` oder
|
||
eine `authorized_keys` in ein fremdes Benutzerverzeichnis — aus einem
|
||
Anwendungsrecht ist ein Systemzugang geworden.
|
||
|
||
`recovery.TargetGuard` sperrt die Systemverzeichnisse des **laufenden** Systems.
|
||
Wer ein fremdes `/etc` zurückschreiben will, legt es woanders ab und übernimmt es
|
||
von dort — ein Zwischenschritt, der einen Blick erzwingt. Über
|
||
`SYNCOVA_RESTORE_ALLOWED_ROOTS` lässt sich die Erlaubnis auf bestimmte
|
||
Verzeichnisse begrenzen; dann gilt ausschließlich diese Liste.
|
||
|
||
Der Vergleich läuft über die Pfadtrennung, nicht über `strings.HasPrefix`: Sonst
|
||
gälte `/etchen` als Teil von `/etc` und `/var/library` als Teil von `/var/lib`.
|
||
|
||
### SSRF über Benachrichtigungswege
|
||
|
||
Der schwerste Fund. Ein Webhook ließ sich auf `http://169.254.169.254/` richten —
|
||
den Metadatendienst der Cloud, der mit Zugangsdaten antwortet. Ebenso auf
|
||
`127.0.0.1:5432` (die eigene Datenbank), auf die eigene API und ins private Netz.
|
||
|
||
`platform/netguard` sperrt Rückschleife, verbindungslokale Adressen, private
|
||
Netze, CGNAT, Multicast und die Dokumentationsbereiche. Drei Einzelheiten
|
||
entscheiden über die Wirksamkeit:
|
||
|
||
- **Die Prüfung sitzt an zwei Stellen.** Beim Anlegen des Kanals, damit der
|
||
Betreiber die Meldung sofort sieht — und erneut unmittelbar vor dem
|
||
Verbindungsaufbau, weil ein Name zwischen beiden Zeitpunkten auf eine andere
|
||
Adresse zeigen kann. Ein DNS-Eintrag, der beim Prüfen öffentlich und beim
|
||
Zustellen intern auflöst, ist der übliche Weg um eine einmalige Prüfung herum.
|
||
- **Eingebettete IPv4-Adressen werden entpackt.** `::ffff:127.0.0.1` ist dieselbe
|
||
Adresse wie `127.0.0.1`, nur anders geschrieben — und wer eine Sperrliste
|
||
umgehen will, probiert genau das als Erstes.
|
||
- **Jede aufgelöste Adresse wird geprüft**, nicht nur die erste. Ein Name kann auf
|
||
mehrere zeigen, und der Verbindungsaufbau nimmt nicht zwangsläufig dieselbe.
|
||
|
||
Auch der SMTP-Server ist ein Ziel: Wer ihn auf `127.0.0.1:11211` richtet, spricht
|
||
mit einem Zwischenspeicher statt mit einem Mailserver — und bekommt dessen
|
||
Antwort über die Fehlermeldung zurück.
|
||
|
||
`SYNCOVA_ALLOW_INTERNAL_NOTIFICATION_TARGETS=true` ist der ausdrückliche Ausweg
|
||
für einen Meldedienst im eigenen Netz.
|
||
|
||
### Rate Limit nur beim Login
|
||
|
||
100 von 100 Anfragen gegen `/jobs` liefen durch. Ein angemeldeter Benutzer — oder
|
||
ein entwendetes Token — konnte die Anlage überfluten. Besonders teuer sind die
|
||
Endpunkte, die im Hintergrund arbeiten: Ein Bericht erzeugt ein PDF, eine
|
||
Vorabprüfung liest jeden Block eines Backups, das Security Center stellt zehn
|
||
Abfragen.
|
||
|
||
Der allgemeine Begrenzer liegt jetzt in der Middleware-Kette
|
||
(`SYNCOVA_HTTP_REQUESTS_PER_MINUTE`, Vorgabe 600). Der Login behält seine eigene,
|
||
strengere Grenze. Nachgewiesen: 700 Anfragen → 594 × 200, 106 × 429.
|
||
|
||
### Kein TLS
|
||
|
||
Der Dienst sprach ausschließlich Klartext. Jetzt lädt er ein Zertifikat über
|
||
`SYNCOVA_HTTP_TLS_CERT_FILE` und `SYNCOVA_HTTP_TLS_KEY_FILE`; nachgewiesen mit
|
||
TLS 1.3.
|
||
|
||
Zwei Entscheidungen dabei:
|
||
|
||
- **Eine halbe TLS-Konfiguration wird beim Start abgelehnt.** Nur ein Zertifikat
|
||
ohne Schlüssel ließe den Dienst im Klartext starten, obwohl der Betreiber
|
||
Verschlüsselung eingerichtet zu haben glaubt.
|
||
- **Ohne TLS sagt der Dienst das bei jedem Start.** Lauscht er nur auf
|
||
`127.0.0.1`, ist das eine Information — der Betrieb hinter einem Reverse Proxy
|
||
ist der Normalfall. Lauscht er auf allen Schnittstellen, ist es eine Warnung in
|
||
Großbuchstaben: Dann wandern Anmeldedaten im Klartext durch das Netz.
|
||
|
||
### Nebenbefund: Rechte des Repository-Wurzelverzeichnisses
|
||
|
||
`MkdirAll` legt Elternverzeichnisse mit der umask des Aufrufers an. Die Wurzel
|
||
eines frisch erzeugten Repositorys war dadurch weltlesbar, während jedes
|
||
Unterverzeichnis `0700` trug. Ein Fremder kam in keines hinein, sah aber, dass
|
||
hier ein Repository liegt und wie viele Backups es führt.
|
||
|
||
## Werkzeuge
|
||
|
||
Sie laufen im Projekt, nicht in einer Anleitung — was nur in einer Anleitung
|
||
steht, wird nach zwei Wochen nicht mehr ausgeführt.
|
||
|
||
```bash
|
||
make lint # gofmt, go vet, staticcheck
|
||
make security-scan # govulncheck + npm audit
|
||
make secret-scan # eigener Scanner
|
||
make test # der Secret-Scan läuft hier ohnehin mit
|
||
```
|
||
|
||
**govulncheck fand neun Schwachstellen in der Go-Standardbibliothek.** Die
|
||
Toolchain stand auf 1.26.1; behoben sind sie in 1.26.5. Nach dem Anheben: null.
|
||
|
||
**staticcheck ist bis auf eine Regel vollständig aktiv.** ST1005 verlangt
|
||
Fehlertexte in Kleinschreibung und ohne Satzzeichen — eine englische Konvention.
|
||
Ein Teil der Fehlertexte dieser Anlage geht unverändert an einen Anwender: in die
|
||
API-Antwort, in den Integritätsbericht, in die Oberfläche. Ein deutscher Satz,
|
||
den ein Mensch liest, beginnt mit einem Großbuchstaben. Die Regel zu befolgen
|
||
hieße, Anwendermeldungen zu verstümmeln, damit ein Werkzeug schweigt.
|
||
|
||
**Der Secret-Scanner ist selbst geschrieben** und läuft als Test mit. Er ersetzt
|
||
kein Werkzeug wie gitleaks, das die gesamte Historie durchsucht; er beantwortet
|
||
die Frage, die vor jedem Commit zählt: Liegt jetzt gerade ein Geheimnis im
|
||
Arbeitsverzeichnis? Zwei Eigenschaften machen ihn brauchbar:
|
||
|
||
- **Er kennt Platzhalter.** Ohne diese Ausnahme meldete er jede
|
||
Beispielkonfiguration — und ein Prüfwerkzeug, das grundlos Alarm schlägt, wird
|
||
bald nicht mehr ernst genommen.
|
||
- **Er gibt einen Fund nie vollständig aus.** Ein Scanner, der das gefundene
|
||
Geheimnis in voller Länge in ein Prüfprotokoll schreibt, hat es soeben ein
|
||
zweites Mal veröffentlicht — und Prüfprotokolle landen in Logdateien,
|
||
Ticketsystemen und Chats.
|
||
|
||
Im Bestand fand er 14 Stellen, alle erfundene Testwerte. Sie tragen jetzt den
|
||
Vermerk `secretscan:erlaubt` — bewusst umständlich benannt, damit er sich in
|
||
einer Durchsicht wiederfinden lässt.
|
||
|
||
Ein zweiter Test prüft, dass der Scanner überhaupt anschlägt: Ein Scanner, der
|
||
nichts findet, weil seine Muster nicht greifen, ist von einem sauberen
|
||
Quellbestand nicht zu unterscheiden.
|
||
|
||
## Was bereits stand
|
||
|
||
Nicht alles war offen. Aus früheren Phasen greifen:
|
||
|
||
- **Sicherheitskopfzeilen** (`nosniff`, `X-Frame-Options: DENY`,
|
||
`Referrer-Policy: no-referrer`, CSP `default-src 'none'`) — Phase 0
|
||
- **CORS mit Erlaubnisliste**, Wildcard wird beim Start abgelehnt — Phase 0
|
||
- **Opake Tokens statt JWT** — eine Kontosperre wirkt sofort, nachgewiesen — Phase 1
|
||
- **Argon2id, Schein-Passwortprüfung bei unbekanntem Konto, Brute-Force-Schutz** — Phase 1
|
||
- **Append-only-Audit per Datenbank-Trigger** — Phase 1
|
||
- **Pfadprüfung beim Wiederherstellen aus einem Manifest** — Phase 5
|
||
- **Weitergeleitete IP-Header werden ignoriert** — Phase 1
|
||
|
||
## Bekannte Grenzen
|
||
|
||
- **Keine Prüfung der gesamten Versionsgeschichte.** Der Secret-Scanner sieht das
|
||
Arbeitsverzeichnis. Für die Historie braucht es gitleaks oder trufflehog.
|
||
- **Keine Entropieprüfung im Secret-Scan.** Sie wäre der naheliegende Zusatz und
|
||
erzeugte hier vor allem Fehlalarme: Dieses Projekt ist voller Prüfsummen,
|
||
UUIDs und Testvektoren, die von einem Geheimnis nicht zu unterscheiden sind.
|
||
- **Der Rate Limiter zählt je Absenderadresse im Arbeitsspeicher.** Bei mehreren
|
||
Control-Servern zählt jeder für sich; die wirksame Grenze ist dann ein
|
||
Vielfaches. Für eine gemeinsame Zählung bräuchte es einen geteilten Speicher.
|
||
- **Kein automatisches Zertifikat.** ACME ist nicht umgesetzt; das Zertifikat
|
||
kommt aus Dateien.
|
||
- **Die SSRF-Sperrliste ist statisch.** Sie kennt die üblichen internen Bereiche.
|
||
Ein Betreiber mit öffentlich geroutetem, aber internem Adressbereich muss ihn
|
||
selbst absichern — die Anlage kann ihn nicht erkennen.
|