# Fixes & Hardening TODO Kritisches Review-Backlog vor Produktiveinsatz und OSS-Release. Sortiert nach Risikoklasse. Datei-/Zeilenreferenzen beziehen sich auf den Stand bei Review-Erstellung. --- ## 🔴 Kritisch — vor Produktion blockierend - [x] **1. Default-Admin `admin/admin` entfernen** ✅ erledigt - `management/src/db.js:83` — wirft jetzt Exception bei leerem `AUTH_PASSWORD`. Kein Silent-Fallback mehr. - README/`docs/deployment.md` entsprechend anpassen (noch offen). - [x] **2. `API_TOKEN` im Node-Agent zur Pflicht machen** ✅ erledigt - `agent/src/config.js:44` — Start schlägt fehl wenn Token fehlt oder kürzer als 32 Zeichen. - `agent/src/index.js` — `if (!config.apiToken) next()` entfernt; Token-Prüfung immer aktiv. - [x] **3. HTTPS zwischen Management und Agent erzwingen** ✅ erledigt - `management/src/routes/nodes.js:102-112` — `validateBaseUrl` erzwingt `https://`. Escape-Hatch `ALLOW_INSECURE_AGENT_HTTP=true` nur für Dev. - `management/src/agentClient.js` — eigene CA über `AGENT_CA_FILE` konfigurierbar. - Agent-Server unterstützt TLS via `HTTPS_ENABLED`, `TLS_CERT_FILE`, `TLS_KEY_FILE`. - [x] **4. CSRF-Risiko durch reflektierendes CORS schließen** ✅ erledigt - `management/src/index.js:18-27` — Nur Origins aus `CORS_ORIGINS`-Env zugelassen. - `management/src/auth.js:57` — Cookie auf `SameSite=Strict` gesetzt. - [x] **5. Brute-Force-Schutz beim Login** ✅ erledigt - `management/src/routes/auth.js:7-9` — In-Memory Rate-Limit: 5 Versuche / 15 min / IP+User. - Audit-Event `login_blocked` und `login_failed` implementiert. - Hinweis: Kein externer `express-rate-limit` nötig, eigenständige Implementierung ausreichend. - [x] **6. Restore atomarisieren — kein direktes `dd` auf Produktiv-Volume** ✅ staged Restore umgesetzt - Datei: `agent/src/executor.js:92` (`streamResticDumpToDd`), `agent/src/routes/restore.js` - Umsetzung: Restore schreibt zuerst in ein temporäres ZFS-Volume, prüft die Größe vorab und tauscht danach per `zfs rename` gegen das Produktiv-Volume. - Das alte Volume bleibt als `*.pre-restore-*` Rollback-Kopie erhalten. - Bei Fehlern vor dem Swap wird nur das temporäre Volume entfernt; bei Fehlern nach dem Swap versucht der Agent den alten Volume-Namen wiederherzustellen. - [ ] **7. Backup-Verifikation einbauen** - Datei: `agent/src/routes/backup.js` (VM-Pfad ~Z.71, Container-Pfad ~Z.137) - Aktuell: Bei Pipe-Fehler (`zfs send` !=0, `createReadStream`-Abbruch) committed restic ggf. einen unvollständigen Snapshot mit Status „success". - Fix: - Vor Start erwartete Größe ermitteln (`zfs get volsize` / `used`). - Nach Restic-Commit `restic stats ` vergleichen. - Bei Mismatch oder Pipe-Fehler: Restic-Snapshot per ID `forget`+`prune`en, Job auf `failed`. - [x] **8. Bearer-Token aus Frontend-`localStorage` entfernen** ✅ erledigt - `frontend/src/api.js` — nur noch Cookie-Auth (`withCredentials: true`). `localStorage`-Token und `VITE_API_TOKEN` entfernt. - [x] **45. Secrets in `GET /settings` maskieren (Write-only)** ✅ erledigt - Datei: `agent/src/config.js` (`readEnvSettings`/`writeEnvSettings`), `frontend/src/components/Settings.jsx` - Problem: `readEnvSettings` gab den Wert *aller* Felder zurück — inkl. `RESTIC_PASSWORD`, `AWS_SECRET_ACCESS_KEY`, `API_TOKEN`. Über den Management-Proxy konnte damit jeder eingeloggte UI-User das Restic-Verschlüsselungspasswort und die S3-Credentials enthüllen (= alle Backups entschlüsseln und löschen). - Fix: Secret-Felder liefern beim Lesen keinen Wert mehr, nur `hasValue: true|false`. Beim Schreiben gilt ein leeres Secret-Feld als „unverändert" (bestehender Wert bleibt erhalten). Frontend zeigt Platzhalter „gesetzt – leer lassen zum Beibehalten". --- ## 🟠 Hoch - [~] **9. Session-Cookie `Secure`-Flag** ⚠️ teilweise erledigt - `management/src/auth.js:56` — `Secure`-Flag via `SESSION_COOKIE_SECURE` konfigurierbar, auto-aktiviert bei `NODE_ENV=production`. - Offen: `management/.env.example` hat `SESSION_COOKIE_SECURE=false` — in Deployment-Doku explizit als "in Produktion auf `true` setzen" dokumentieren. - [ ] **10. Race beim Sichtbarmachen des Snapshot-Devices** - Datei: `agent/src/routes/backup.js:57` - Aktuell: 2 s `sleep` reicht nicht garantiert. - Fix: Polling-Schleife auf `fs.access(snapshotDevice)` mit Timeout; zusätzlich `udevadm trigger && udevadm settle`. - [x] **11. Persistenter Job- und Lock-Store im Agent** ✅ erledigt - Datei: `agent/src/jobs.js` - Umsetzung: Agent speichert Jobs in SQLite (`AGENT_DATABASE_PATH`). - Beim Start werden `running`/`queued` Jobs als `failed` markiert, damit Management einen finalen Zustand pollen kann und VM-Locks nicht hängen bleiben. - Offen für später: ressourcenspezifische Cleanup-Recovery für abgebrochene Host-Operationen. - [ ] **12. Toten `SESSION_SECRET` aufräumen** - Datei: `management/src/config.js:8` - Aktuell: `SESSION_SECRET` wird geladen, aber nirgendwo genutzt — toter Code. - Fix: Variable und zugehörigen `.env.example`-Eintrag entfernen, oder Session-IDs HMAC-signieren und Variable dann sinnvoll nutzen. - [ ] **13. Snapshot-ID-Prefix-Matching eindeutig machen** - Datei: `agent/src/validators.js:66` - Aktuell: `startsWith` — bei Prefix-Kollision wird stillschweigend der erste Treffer genommen. - Fix: Bei >1 Treffer 409 zurückgeben; UI auf 12-Hex-Prefix umstellen. - [ ] **14. RBAC einführen** - Aktuell: ein User, alle Rechte, kein Read-only. - Fix: Mindestens Rollen `admin` / `operator` / `viewer`. Restore nur für `admin`. - [ ] **15. ENV-Escaping in `writeEnvSettings` verbessern** - Datei: `agent/src/config.js:105` - Aktuell: Nur `\` und `"` escaped; `$`, Backticks, Newlines nicht. - Fix: Eigener Serializer mit korrektem Escaping aller Sonderzeichen. - [ ] **16. Settings-Endpunkt sperren bis `API_TOKEN` initial gesetzt ist** - Datei: `agent/src/routes/settings.js` - Hängt mit Fix #2 zusammen — nach #2 automatisch erfüllt, hier zur Sicherheit dokumentieren/testen. - [x] **46. Agent-Token-Vergleich timing-safe machen** ✅ erledigt - Datei: `agent/src/index.js:28` - Problem: `header === \`Bearer ${config.apiToken}\`` — normaler String-Vergleich am root-Agent (das wertvollste Ziel), während das Login-Passwort bereits `timingSafeEqual` nutzte. - Fix: Vergleich über `crypto.timingSafeEqual` mit Längen-Guard. Leerer/fehlender konfigurierter Token verweigert weiterhin (`config.apiToken &&`). - [ ] **47. Pre-Restore-Volumes aufräumen (Disk-Space-GC)** - Datei: `agent/src/routes/restore.js` - Aktuell: Jeder erfolgreiche Restore behält das alte Volume als `*.pre-restore-*` Rollback-Kopie — es gibt aber keinen Cleanup. Jeder Restore verdoppelt den Plattenbedarf der VM dauerhaft; bei mehreren Restores läuft der ZFS-Pool voll. - Fix: Aufbewahrungsregel (z.B. „keep last N pre-restore/failed-restore Volumes pro VM") oder expliziter Cleanup-Schritt/UI-Aktion. Mindestens im Health/Operations-View sichtbar machen. - [ ] **48. Agent-Tokens im Management-SQLite nicht im Klartext speichern** - Datei: `management/src/store.js` (`nodes.token`), `management/src/db.js` - Aktuell: `nodes.token` liegt im Klartext in der Management-DB. DB-Diebstahl = alle Agent-Tokens = root auf allen Hosts. - Fix: Verschlüsselung at-rest mit einem Management-Key (z.B. aus `SESSION_SECRET`/dediziertem Key abgeleitet), oder zumindest DB-Dateipermissions/Disk-Encryption-Anforderung im Deployment-Doc verbindlich dokumentieren. --- ## 🟡 Mittel - [ ] **17. `Math.random()` durch `crypto.randomBytes` ersetzen** - Datei: `management/src/db.js:93` (`cryptoId`). - Noch vorhanden: `Math.random().toString(16)` für DB-interne User-IDs — kein akutes Sicherheitsproblem, aber nicht kryptografisch sicher. - [ ] **18. Systemd-Hardening für den Agent** - Datei: `deploy/systemd/incus-backup-agent.service` - Hinzufügen: `NoNewPrivileges=true`, `ProtectSystem=strict`, `ProtectHome=true`, `PrivateTmp=true`, `ReadWritePaths=/dev/zvol /var/lib/incus /opt/incus-backup-ui/agent`, `CapabilityBoundingSet=...`, eingeschränkte `AmbientCapabilities`. - [ ] **19. `npm start` durch direkten `node`-Aufruf ersetzen** - Datei: beide `deploy/systemd/*.service` - `ExecStart=/usr/bin/node src/index.js` — kein npm-Wrapper-Prozess, kein PATH-Risiko. - [ ] **20. CORS am Agent entfernen** - Datei: `agent/src/index.js:16` - Agent wird nie aus dem Browser angesprochen → Angriffsfläche raus. - [ ] **21. `schedules.json`-Pfad explizit konfigurierbar** - Datei: `agent/src/scheduler.js:6` - Aktuell: `process.cwd()`-abhängig. - Fix: über Env (`SCHEDULES_PATH`) absolut konfigurieren, Default unter `/var/lib/incus-backup-agent/`. - [ ] **22. Audit-Log-Integrität** - Datei: `management/src/store.js:71` - Append-only Constraint + Hash-Chain (jeder Event hat `prev_hash`). Optional Off-Site-Versand (Webhook). - [ ] **23. Passwort-Hashing auf argon2id umstellen** - Datei: `management/src/crypto.js` - Aktuell: `scryptSync` mit Node-Defaults, ohne explizite Parameter. - Fix: `@node-rs/argon2` oder explizite scrypt-Parameter dokumentieren + Migration-Pfad. - [ ] **24. Rate-Limit / Quoten für Backups** - Pro VM und global (z.B. max N parallele Streams nach S3). Disk-Space-/Quota-Check vor Start. - [ ] **25. `incus snapshot delete` Retry verallgemeinern** - Datei: `agent/src/routes/backup.js:303` - Aktuell: Substring-Match auf englische Stderr — bricht bei lokalisierten Builds. - Fix: Generischer Retry (n Versuche, Backoff) bei nicht-0 Exit-Code. - [ ] **26. Restic-`ls` streamen statt vollständig in RAM laden** - Datei: `agent/src/routes/snapshots.js:19` - Für Container-Backups mit vielen Files relevant. - [ ] **27. Container-Restore implementieren oder Container-Backup deaktivieren** - Datei: `agent/src/routes/restore.js:21` - Aktuell: 501. Backups laufen, aber nicht wiederherstellbar = Backup-Theater. - Fix: Sicheren `zfs receive`-Workflow umsetzen oder Container-Backup im UI/API ausschalten bis fertig. - [ ] **28. Pre-Backup VM-Zustand prüfen** - Live-Migration, laufende interne Snapshots, fehlende Berechtigungen → klare Fehler statt halb durchgeführter Pipeline. - [ ] **49. Abgelaufene Sessions serverseitig löschen + Rotation** - Datei: `management/src/store.js:12` (`getUserBySession`), `createSession` - Aktuell: Abgelaufene Sessions werden beim Lesen nur gefiltert, nie aus der DB entfernt → unbegrenztes Tabellenwachstum. Außerdem keine Session-Rotation nach erfolgreichem Login. - Fix: Periodischer Cleanup (`DELETE FROM sessions WHERE expires_at <= now`) und neue Session-ID nach Login ausstellen. --- ## 🟢 Niedrig / Aufräumen - [ ] **29. `.DS_Store` aus Repo entfernen und in `.gitignore` aufnehmen** - [ ] **30. `frontend/dist/` ist eingecheckt** — ignorieren und löschen. - [ ] **31. `incus-backup-ui-plan.md` auf Secrets/Bucket-Namen prüfen** bevor OSS. - [ ] **32. Container-Limit (Restore fehlt) prominent in README dokumentieren.** - [ ] **33. Scheduler-Jitter einbauen** (`management/src/scheduler.js`, `agent/src/scheduler.js`) — sonst belasten viele Nodes synchron S3. - [ ] **34. `formatBytes` deduplizieren** (`agent/src/routes/backup.js`, `restore.js`) → `executor.js` oder `utils.js`. - [ ] **35. `.env`-Dateipermissions dokumentieren** — `chmod 600` im Deployment-Doc verlangen. - [ ] **36. Schema-Versionierung statt `addColumnIfMissing`** — z.B. `schema_version`-Tabelle + nummerierte Migrationen. - [ ] **37. `audit_events.details` Größenlimit** oder JSON-Spalte (SQLite hat JSON1). --- ## 📦 OSS-Release-Voraussetzungen - [ ] **38. Tests einführen** — aktuell keinerlei Tests im Repo. Mindestens: - `validators.js` (vmName-/snapshot-Regex, Pfad-Validation). - `crypto.js` (hash/verify Roundtrip, Timing-Safe). - `jobs.js` (Locking, Trimming). - Pipeline-Smoke-Tests mit gemockten Befehlen. - [ ] **39. CI-Pipeline** (GitHub Actions): Lint, Tests, `npm audit`, Lockfile-Check. - [ ] **40. `SECURITY.md`** mit Disclosure-Adresse + GPG-Key. - [ ] **41. Threat-Model dokumentieren** — explizit machen, was außerhalb des Schutzbereichs liegt (root-Agent, vertrauenswürdiges Netz, etc.). - [ ] **42. `LICENSE`-Datei ergänzen** (z.B. MIT/Apache-2.0). - [ ] **43. `CONTRIBUTING.md`** + Code-of-Conduct. - [ ] **44. Beispiel-`compose.yaml` oder Ansible-Rolle** für reproduzierbares Deployment. --- ## Status-Übersicht kritische Punkte (Stand 2026-06-04) | # | Titel | Status | |---|-------|--------| | 1 | Default-Admin `admin/admin` entfernen | ✅ erledigt | | 2 | `API_TOKEN` Pflicht | ✅ erledigt | | 3 | HTTPS Management↔Agent | ✅ erledigt | | 4 | CORS-Whitelist + SameSite=Strict | ✅ erledigt | | 5 | Brute-Force-Schutz Login | ✅ erledigt | | 6 | Restore atomarisieren | ✅ staged Restore umgesetzt | | 7 | Backup-Verifikation | ⬜ offen | | 8 | Bearer-Token aus localStorage | ✅ erledigt | | 9 | Session-Cookie `Secure`-Flag | ⚠️ teilweise | | 45 | Secrets in `GET /settings` maskieren | ✅ erledigt | | 46 | Agent-Token-Vergleich timing-safe | ✅ erledigt | | 47 | Pre-Restore-Volume-GC | ⬜ offen | | 48 | Agent-Tokens in DB verschlüsseln | ⬜ offen | | 49 | Session-Cleanup serverseitig | ⬜ offen | ## Nächste Prioritäten 1. **#7** Backup-Verifikation weiter härten — Pipeline-/Stream-Fehler testen und vollständiger absichern. 2. **#47** Pre-Restore-Volume-GC — sonst läuft der ZFS-Pool bei wiederholten Restores voll. 3. **#9** `SESSION_COOKIE_SECURE=true` in Deployment-Doku festschreiben. 4. **#12** Toten `SESSION_SECRET` entfernen. 5. **#15** ENV-Escaping vervollständigen (`$`, Backticks, Newlines). 6. **#11 Folgearbeit** ressourcenspezifische Cleanup-Recovery nach Agent-Crash definieren.