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

8.9 KiB
Raw Blame History

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.

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.