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>
8.9 KiB
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.1ist dieselbe Adresse wie127.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, CSPdefault-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.