syncova-backup/docs/hardening.md
Jerrit Fritzsche 610719c316
Some checks failed
CI / Backend (Go) (push) Failing after 3m7s
CI / Frontend (React/TypeScript) (push) Successful in 37s
CI / Sicherheitsprüfungen (push) Successful in 44s
Syncova Backups V1
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>
2026-08-17 09:10:54 +02:00

180 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.