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