diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a12a3fd..fd9f4e3 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -59,11 +59,11 @@ Datenzugriff ausschließlich über TanStack Query gegen die Backend-API (kein di ## 3. Auth-Flow -1. Login (`POST /api/v1/auth/login`) mit E-Mail/Passwort → Access-Token (kurzlebig, ~15 min, JWT) + Refresh-Token (langlebig, ~7 Tage, in DB als Hash gespeichert, rotierend). -2. Frontend hält Access-Token im Memory/React-Context, Refresh-Token als httpOnly-Cookie. -3. Bei 401 automatischer Refresh über `POST /api/v1/auth/refresh`, danach Retry. -4. Jeder geschützte Endpunkt: `JwtAuthGuard` (Authentifizierung) + `RolesGuard` (Autorisierung nach Rolle aus Abschnitt 3). -5. API-Client-Rolle nutzt dieselben JWT-Endpunkte mit client-credentials-artigem Service-Account (kein OAuth2-Server im MVP, siehe Annahmen). +1. Login (`POST /api/v1/auth/login`) mit E-Mail/Passwort → Access-Token (15 min, JWT HS256, `iss=apicore`, `aud=apicore:access`) + Refresh-Token (in DB als Hash, rotierend). Bei aktiver MFA zuerst ein Zwischen-Token (`aud=apicore:mfa-challenge`), das nie als Access-Token gilt. +2. Frontend hält Access-Token im Memory/React-Context, Refresh-Token als httpOnly-Cookie (`Secure`, `SameSite=Lax`, Pfad `/api/v1/auth`). +3. Bei 401 automatischer Refresh über `POST /api/v1/auth/refresh`, danach Retry. Rotation atomar; Wiederverwendung eines alten Tokens (nach 10 s Gnadenfrist für parallele Tabs) beendet alle Sitzungen. Spätestens `SESSION_MAX_AGE` (7 Tage) nach dem Login ist neu anzumelden. +4. Guards in dieser Reihenfolge: `ThrottlerGuard` → `JwtAuthGuard` (Authentifizierung, Gültigkeit frisch aus der DB je Anfrage) → `PermissionsGuard` (Berechtigung, fail-closed). +5. Maschinen-Zugriff (Wabenhain) nur per API-Schlüssel mit Scope-Allowlist und optionaler IP-Allowlist, siehe Annahme 14 und Abschnitt 9. ## 4. Datenfluss / Rückverfolgbarkeit @@ -107,6 +107,10 @@ Fachliche Annahmen, getroffen weil das Lastenheft an der Stelle keine eindeutige 15. **Bestandsbuch aus Behandlungen, nicht als eigenes Register**: Das Bestandsbuch nach Art. 108 VO (EU) 2019/6 wird aus `Treatment` erzeugt (`export/treatment-log.ts`); Lieferant, Erwerbsbeleg und verschreibender Tierarzt sind Freitextfelder der Behandlung, Name/Anschrift des Halters liegen in `SiteSettings`. Der Stand am Behandlungstag wird beim Anlegen als `Treatment.apiaryId` festgehalten (Umsetzen ändert ihn nicht nachträglich); für Altbestand wird er aus `ColonyApiaryHistory` abgeleitet und im PDF bei Unsicherheit markiert. Fehlende Pflichtangaben werden als „FEHLT“ gedruckt statt still weggelassen. Keine Wartezeit-Automatik (siehe 10). 16. **Überwinterung als eigene Einträge statt Volksstatus**: `WinterCheck` (eindeutig je Volk, `winterYear`, Saison) hält Ein- und Auswinterung; ab Juli gehört ein Datum zum Winter desselben Jahres. Verlustursachen sind bewusst nur die COLOSS-Kategorien, keine Diagnosen. Die Übersicht gruppiert nach dem aktuellen Stand des Volkes. Ein Verlust ändert den Volksstatus nur auf ausdrücklichen Wunsch (`markColonyDeceased`). Neue Rechte vergibt die Migration an Nicht-System-Rollen, weil der Katalog-Sync nur System-Rollen berechtigt. +17. **Schutz gegen Rechteausweitung in der Benutzerverwaltung**: Wer Benutzer pflegt, darf nur Konten verwalten (Passwort/MFA zurücksetzen, E-Mail ändern, (de)aktivieren) und Rollen vergeben, deren Rechte er selbst vollständig hat (`UsersService.assertWithinCallerPermissions`), analog zur Rollenbearbeitung. +18. **SSO-Bindung an die IdP-Identität**: Die erste Microsoft-Anmeldung bindet das Konto an `"|"` (`User.ssoSubject`); danach zählt nur noch diese Identität, nicht der unverifizierte E-Mail-Claim. Mandantenübergreifende Issuer (`common`/`organizations`/`consumers`) schalten SSO ab. E-Mails werden ohne Groß-/Kleinschreibung verglichen. +19. **Backups versiegelt**: AES-256-GCM über das gzip-Bundle, Schlüssel `BACKUP_ENCRYPTION_KEY` (sonst per HKDF aus `JWT_ACCESS_SECRET`). Nur versiegelte Dateien stellen Benutzer, Rollen, Webhooks und Audit-Log wieder her; Import verlangt Passwort (+ TOTP). Webhook-Secrets liegen verschlüsselt in der DB (Schlüssel aus `JWT_ACCESS_SECRET`) – für eine Wiederherstellung in einem anderen Deployment mit anderem `JWT_ACCESS_SECRET` müssen Webhook-Secrets und MFA neu eingerichtet werden. + ## 7. Kubernetes `deploy/k8s/` enthält je Projekt: `deployment.yaml`, `service.yaml`, `ingress.yaml`, sowie `configmap.example.yaml` und `secret.example.yaml` (Platzhalter, keine echten Werte). Backend exponiert `GET /health` (liveness) und `GET /health/ready` (readiness, prüft DB-Verbindung). Beide Projekte je ein schlankes Multi-Stage-Dockerfile (Build-Stage mit vollem `node_modules`, Runtime-Stage mit `npm ci --omit=dev` bzw. Next.js `output: 'standalone'`). @@ -126,3 +130,25 @@ Fachliche Annahmen, getroffen weil das Lastenheft an der Stelle keine eindeutige - **Restore:** Über `POST /backup/import` (bzw. den Import-Dialog unter Verwaltung > Backup) eine zuvor exportierte Bundle-Datei einspielen. Ein rohes `pg_dump`/PITR-Backup auf Infrastrukturebene bleibt als zusätzliche, komplementäre Absicherung für den Katastrophenfall (z. B. korrupte Datenbankdateien, nicht nur fachliche Daten) sinnvoll, ist aber nicht mehr der primäre, hier dokumentierte Mechanismus – die in-App-Lösung deckt den fachlichen Anwendungsfall "Daten sichern/übertragen" bereits vollständig ab. + +## 9. API-Sicherheit (Stand Oktober 2026) + +Grundlage: OWASP API Security Top 10 (2023) und OWASP ASVS Level 2. Umsetzung und Begründung je Punkt in `docs/pruefung-2026-09.md`, Abschnitt „API-Sicherheit“. + +| Bereich | Maßnahme | +|---|---| +| Authentifizierung | JWT mit festem Algorithmus, Issuer und Audience je Zweck; MFA (TOTP) mit Replay-Schutz; Kontosperre 15 min nach 5 Fehlversuchen; gleiche Antwort für unbekannte/gesperrte Konten; bcrypt auf jedem Pfad | +| Passwörter | mind. 12 Zeichen, max. 72 Byte, Abgleich mit Have I Been Pwned (k-Anonymität, abschaltbar per `PASSWORD_BREACH_CHECK=false`) | +| Sitzungen | Refresh-Rotation atomar, Diebstahlerkennung, max. 7 Tage; Einmal-Tokens für SSO-Abschluss und MFA-Schritt | +| Step-up | Backup-Import, MFA ein/aus verlangen aktuelles Passwort (+ TOTP) | +| Autorisierung | fail-closed `PermissionsGuard`; keine Rechteausweitung über Benutzer-/Rollenpflege; Suche nur über lesbare Objektarten | +| Maschinen-Zugriff | API-Schlüssel nur als Hash, Scope-Allowlist (`shop-sync:read`), IP-Allowlist, widerrufbar, befristbar; Ersteller kann nur eigene Rechte vergeben | +| Ausgehende Aufrufe | Webhooks: Ziel-IP beim Verbindungsaufbau geprüft (kein DNS-Rebinding), nur Port 80/443, https in Produktion, 5 s Timeout, Signatur über Zeitstempel+ID+Body | +| Transport/Antworten | `Cache-Control: no-store` (außer öffentliche Chargen-Seite), CSP `default-src 'none'`, `frame-ancestors 'none'`, keine ETags; Produktion startet nur mit https-`FRONTEND_ORIGIN` und sicheren Cookies | +| Rate-Limits | Throttler vor der Authentifizierung (100/min, Login/MFA/Refresh/Logout 10/min, öffentliche Chargen 30/min); Client-IP über `TRUST_PROXY` | +| Daten | Backups verschlüsselt und signiert, Dateien 0600; CSV-Export ohne Formel-Injection; Logs ohne Query-Strings | + +**Bekannte Restpunkte (bewusst nicht umgesetzt):** + +- Rate-Limits zählen je Pod im Speicher; bei 2 Replikas verdoppelt sich das Limit. Brute-Force-Schutz hängt nicht daran, sondern an der Kontosperre in der DB. Ein gemeinsamer Speicher (Redis) wäre der nächste Schritt, falls Lastspitzen ein Thema werden. +- Eine E-Mail-Änderung im eigenen Profil meldet „bereits vergeben“, wenn die Adresse existiert. Das erfordert eine Anmeldung plus aktuelles Passwort; eine generische Meldung würde Nutzer nur verwirren. diff --git a/docs/pruefung-2026-09.md b/docs/pruefung-2026-09.md index 553259e..6390549 100644 --- a/docs/pruefung-2026-09.md +++ b/docs/pruefung-2026-09.md @@ -106,3 +106,29 @@ Aufbewahrung 5 Jahre. - Rechte `winter-checks:read/create/update`: per Migration an Imker und Mitarbeiter wie Durchsichten, Nur-Lesen-Rollen nur lesen. Einträge sind im Backup enthalten. - Quelle Verlustkategorien: COLOSS-Monitoring (z. B. Gray et al. 2020, J. Apic. Res., doi:10.1080/00218839.2020.1797272). + +## API-Sicherheit (Oktober 2026) + +Audit des gesamten Backends nach OWASP API Top 10 / ASVS L2 (25 Befunde), alle umgesetzt außer den zwei in ARCHITECTURE.md §9 begründeten Restpunkten. Verifikation: AT (Unit + E2E gegen Postgres 17). + +| Befund | Schwere | Commit | +|---|---|---| +| MFA-Zwischen-Token galt als Access-Token (MFA-Bypass) | Kritisch | `e0bd7b2` | +| `users:update` konnte Admin-Konten übernehmen | Hoch | `c4a11b4` | +| SSO-Konto über unverifizierten E-Mail-Claim gewählt | Hoch | `a25265b` | +| Backup-Import überschreibt Benutzer; Export enthält Hashes/Secrets; Pfad-Traversal im Import | Hoch | `bd592ad` | +| Backup-Pfad/Zeitplan frei wählbar | Mittel | `ebeb499` | +| Webhooks: DNS-Rebinding, kein Timeout, Signatur ohne Zeitstempel | Mittel | `79d20ee` | +| TOTP-Replay, keine Kontosperre, MFA-Aus nur mit Passwort, Timing-Enumeration | Mittel | `5c666fe` | +| Refresh-Rotation nicht atomar, keine Höchstdauer | Mittel | `2de93bc` | +| Throttler nach Auth, Proxy-Vertrauen fest, Cache/CSP, Logs mit Query | Mittel | `9ae1d96` | +| API-Schlüssel ohne IP-Bindung, Scope über eigene Rechte hinaus | Mittel | `f1dc0de` | +| CSV-Formel-Injection | Mittel | `61e3247` | +| Passwortregel 8 Zeichen, kein Leak-Abgleich | Mittel | `0ba8996` | +| Suche umgeht Leserechte | Niedrig–Mittel | `eafe925` | +| Öffentliche Chargen-Seite, SSO-Callback-URL, Logout-Limit, 4xx-Mapping | Niedrig | `0899576` | +| SSO-/MFA-Token mehrfach nutzbar; Rechte-Cache je Replika | Niedrig–Mittel | `45efb08` | + +Neue Umgebungsvariablen: `BACKUP_ENCRYPTION_KEY`, `BACKUP_ROOT`, `SESSION_MAX_AGE`, `TRUST_PROXY`, `PASSWORD_BREACH_CHECK` (siehe `.env.example`). + +Betrieb nach dem Update: Alle Benutzer melden sich einmal neu an (Access-Tokens ohne Audience gelten nicht mehr). Webhook-Empfänger prüfen die neue Signatur (`X-ApiCore-Timestamp`.`X-ApiCore-Delivery`.Body). Im Wabenhain-Schlüssel die Server-IP als erlaubte Adresse eintragen.