Files
ApiCore/ARCHITECTURE.md
T
Nick SzaunigandClaude Opus 5.5 8b6f48b996 docs: API security model, audit results and operating notes
Auth flow and guard order as built now, new architecture decisions
(user-management escalation guard, SSO identity binding, sealed backups),
a section on API security with the remaining known gaps, and the audit
table with commits, new env variables and steps after the update.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-01 07:27:22 +02:00

24 KiB
Raw Blame History

ApiCore – Architektur

Basiert auf docs/lastenheft.md. Diese Datei beschreibt, WIE die fachlichen Anforderungen technisch umgesetzt werden.

1. Backend – Modulschnitt (NestJS)

Ein NestJS-Modul pro fachlichem Bereich, Reihenfolge entspricht Baureihenfolge in TASKS.md:

Modul Zweck Abhängig von
AuthModule Login, Refresh, Passwort-Reset, JWT-Strategien –
UsersModule Benutzerverwaltung, Rollen Auth
AuditModule Audit-Log-Grundstruktur (Service AuditLogService, wird von anderen Modulen injiziert) Users
ApiariesModule Bienenstände Auth
ColoniesModule Völker, Standortwechsel-Historie, Ableger/Vereinigung Apiaries
QueensModule Königinnen, Volk-Historie Colonies
InspectionsModule Durchsichten Colonies
TasksModule Aufgaben, polymorph verknüpft –
VarroaModule Varroamessungen Colonies
TreatmentsModule Behandlungen Colonies
FeedingsModule Fütterungen Colonies
HarvestsModule Ernten Apiaries, Colonies
ExtractionsModule Schleuderungen Harvests
HoneyBatchesModule Honigchargen, Honigmessungen Extractions
StorageModule Lagerbehälter, einfache Lagerverwaltung (StockItem/StockMovement) HoneyBatches
DashboardModule Aggregations-Endpunkte alle o.g.
SearchModule globale Suche, Filter-Bausteine alle o.g.
ExportModule CSV/JSON/PDF-Export alle o.g.

Jedes Modul: *.controller.ts, *.service.ts, dto/, *.module.ts, *.spec.ts (Unit), test/*.e2e-spec.ts (Integration). Rollenprüfung über @Roles()-Decorator + RolesGuard, angewendet pro Endpunkt – niemals nur UI-seitig.

Alle Routen unter /api/v1/..., versioniert über NestJS URI Versioning. OpenAPI-Doku automatisch über @nestjs/swagger, erreichbar unter /api/v1/docs.

2. Frontend – Seitenstruktur (Next.js App Router)

app/
├── (auth)/login/
├── (app)/                       geschütztes Layout mit Bottom-Nav (mobil) / Sidebar (Desktop)
│   ├── dashboard/
│   ├── apiaries/[id]/
│   ├── colonies/[id]/
│   ├── queens/[id]/
│   ├── inspections/new/
│   ├── tasks/
│   ├── varroa/new/
│   ├── treatments/new/
│   ├── feedings/new/
│   ├── harvests/new/
│   ├── honey-batches/[id]/
│   ├── storage/
│   ├── search/
│   └── settings/users/          nur Administrator

Zentrale Schnellaktion "+ Neu" (Abschnitt 22): permanent sichtbarer Floating-Button in der mobilen Bottom-Nav, öffnet Auswahl-Sheet (Durchsicht, Fütterung, Behandlung, Varroamessung, Aufgabe, Ernte, Volk, Königin) → direkt zum jeweiligen Schnellerfassungs-Formular, mobil möglichst 1-2 Taps bis zum ersten Eingabefeld.

Datenzugriff ausschließlich über TanStack Query gegen die Backend-API (kein direkter DB-Zugriff, kein Next.js-eigenes Datenbank-Layer) – erzwingt API-first auch praktisch.

3. Auth-Flow

  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

Kernkette (Abschnitt 16, 29) relational über Fremdschlüssel, nicht über Textfelder:

Apiary → Colony → (Inspection | Treatment | Feeding | VarroaMeasurement)
Apiary/Colony → Harvest → Extraction → HoneyBatch → StorageContainer

HoneyBatch referenziert Extraction, Extraction referenziert n Harvests (m:n über ExtractionHarvest), Harvest referenziert n Colonys (m:n über HarvestColony) + eine Apiary. Damit ist "von welchem Volk stammt Charge X" eine reine Join-Abfrage, kein Freitext.

Historie statt Löschen: alle fachlichen Haupttabellen haben status/deactivatedAt statt Hard-Delete. Verschiebungen (Volk↔Stand) und Königinnenwechsel werden als eigene Historientabellen geführt (ColonyApiaryHistory, QueenColonyHistory), nicht durch Überschreiben des aktuellen Zustands.

5. Technische Leitentscheidungen (bestätigt, keine Abweichung)

  • Validierung: class-validator/class-transformer (nicht Zod) – konsistent mit NestJS-Idiomatik und @nestjs/swagger-Decorators, ein Schema-Ort statt zwei.
  • Ein gemeinsames Git-Repo im ApiCore/-Root für Backend + Frontend (Monorepo-artig, aber zwei unabhängige package.json, kein Turborepo/Nx-Overhead im MVP).
  • Package-Manager: npm (in beiden Projekten, keine Vermischung mit pnpm/yarn).
  • Backend-Tests: Jest (NestJS-Standard). Frontend-Tests: Vitest + React Testing Library.

6. Annahmen & offene Fragen

Fachliche Annahmen, getroffen weil das Lastenheft an der Stelle keine eindeutige Vorgabe macht. Bei Bedarf mit dem Fachbereich klären, aktuell aber Grundlage der Umsetzung:

  1. HoneyMeasurement als MVP-Entität: Abschnitt 26 MUSS-Liste nennt Honigmessungen nicht explizit, Abschnitt 4.17 aber als eigenen Unterpunkt und Abschnitt 30 verlangt Wassergehalt schnell abrufbar. Da sie klein und für die Chargen-Kette relevant ist, wird sie als Teil des HoneyBatchesModule im MVP mitgebaut statt auf v1.x verschoben.

  2. Treatment auf Einzelvolk statt "Völkergruppe": Abschnitt 4.11 erlaubt "Volk oder Völkergruppe". MVP modelliert Treatment pro Volk (colonyId Pflichtfeld); eine Gruppenbehandlung wird im MVP als mehrere Treatment-Datensätze mit gleicher batchNumber/Notiz erfasst statt eigener Gruppen-Entität. Spätere Migration kann eine TreatmentGroup-Entität nachziehen, falls das in der Praxis zu unhandlich ist.

  3. Lagerverwaltung MVP-Umfang: "grundlegende Lagerverwaltung" (Abschnitt 26 Punkt 12) wird als generisches StockItem + StockMovement (Kategorie Honig/Verbrauchsmaterial/Imkereimaterial) umgesetzt. Das detaillierte Inventar mit Wartungsdaten (Abschnitt 4.22) bleibt v1.x/"SOLL", wie im Lastenheft vorgesehen.

  4. API-Client-Rolle im MVP: kein vollwertiger OAuth2/Client-Credentials-Server. Ein API-Client ist ein User mit Rolle API_CLIENT und einem langlebigen Service-Refresh-Token; vollwertiges Scopes-/Client-Management ist SOLL/KANN.

  5. Backup-fähige Struktur: kein eigenes Backup-Tooling im Code. Umgesetzt als: reproduzierbare Prisma-Migrationen (kein manuelles Schema-Drift), dokumentierte pg_dump/PITR-Strategie in diesem Dokument (Betrieb obliegt dem bestehenden Cluster/DBA), /health-Endpoint für Betriebsüberwachung.

  6. Deviations vom Leitentscheidungs-Rahmen: bisher keine. Wird ergänzt, falls während der Umsetzung ein konkreter Grund für eine Abweichung von NestJS/Prisma/Next.js/Tailwind/shadcn/TanStack Query auftritt.

  7. Dokumentenspeicher als lokales Volume (Modul 29): DocumentsModule speichert Uploads unterhalb von DOCUMENTS_STORAGE_PATH auf dem lokalen Dateisystem des Backend-Pods, keine Objektspeicher-Anbindung (S3-kompatibel o.ä.) im MVP/v1.x. Der Pfad ist über die ConfigMap austauschbar, wie in Abschnitt 27 des Lastenhefts vorgesehen. Wichtig für den Betrieb: das aktuelle deploy/k8s-Deployment läuft mit replicas: 2 ohne geteiltes Volume – für konsistente Uploads über beide Pods hinweg muss vor Produktivbetrieb mit Dokumenten-Feature ein ReadWriteMany-PVC (z.B. NFS- oder EFS-basiert) gemountet und DOCUMENTS_STORAGE_PATH darauf gesetzt werden, sonst sieht nur der Pod, der die Datei entgegengenommen hat, sie auch beim Download wieder.

  8. Entra ID SSO ohne Auto-Provisioning: POST/GET /api/v1/auth/sso/* implementiert Authorization Code + PKCE gegen Entra ID über openid-client. Der E-Mail-Claim aus dem ID-Token wird ausschließlich gegen einen bereits existierenden, aktiven User-Datensatz gematcht – es wird kein neuer Benutzer automatisch angelegt (Just-in-Time-Provisioning), passend zum bestehenden Modell, dass Admins Benutzer manuell anlegen. Eine erfolgreiche SSO-Anmeldung überspringt die App-eigene TOTP-MFA-Abfrage, da der Identity Provider bereits eine eigene (typischerweise stärkere) Authentifizierung durchgesetzt hat. Der PKCE-code_verifier wird kurzlebig (5 min) in einem httpOnly-SameSite=Lax-Cookie zwischengespeichert statt serverseitig, damit der Flow ohne Sticky-Sessions über mehrere Backend-Replicas hinweg funktioniert; der Rückweg vom Callback zum Frontend nutzt ein kurzlebiges (60 s), zweckgebundenes JWT statt des echten Access-Tokens in der Redirect-URL. OIDC_ISSUER_URL/OIDC_CLIENT_ID/OIDC_CLIENT_SECRET/OIDC_REDIRECT_URI kommen ausschließlich aus Environment-Variablen; fehlen sie, meldet GET /api/v1/auth/sso/status enabled: false und die Login-Seite blendet den "Mit Microsoft anmelden"-Button aus, statt beim Start zu scheitern.

  9. M365-Kalender-Synchronisation als einseitiger, asynchroner Push: Abschnitt 4.25 verlangt nur einen internen Betriebskalender, keine externe Anbindung – diese Funktion ist eine reine Erweiterung ohne Lastenheft-Vorgabe. Umgesetzt als CalendarSyncModule (ApiCore-Backend/src/calendar-sync/): App-only Graph-API-Zugriff per Client-Credentials-Flow (GraphClientService) statt delegierter Anmeldung, da die Synchronisation unbeaufsichtigt im Hintergrund laufen muss und nicht an eine interaktive Benutzersitzung gebunden sein darf. Nutzt bewusst dieselbe Entra-App-Registrierung wie das SSO-Login (OIDC_ISSUER_URL/OIDC_CLIENT_ID/OIDC_CLIENT_SECRET, ausschließlich Env-Variablen) statt einer zweiten, separaten App-Registrierung – ein Enterprise-App-Objekt im Tenant genügt, sofern es zusätzlich zu den delegierten Login-Scopes auch die Application-Permission „Calendars.ReadWrite“ (Admin-Consent) erhält; die Tenant-ID wird dafür aus dem Pfad von OIDC_ISSUER_URL abgeleitet, statt eine weitere Konfigurationsvariable zu benötigen. Die Übertragung ist bewusst nur einseitig (ApiCore → M365-Gruppenkalender, adressiert über groupId); Änderungen, die direkt im M365-Kalender vorgenommen werden, fließen nicht zurück. Statt eines synchronen Push im Request-Pfad (wie beim Webhooks-Modul) nutzt CalendarSyncOutboxEntry ein Outbox-Muster mit minütlichem @Cron-Worker: Graph-API-Aufrufe sind zu langsam/fehleranfällig (Token-Refresh, Drosselung, transiente 5xx), um eine Durchsicht-/Aufgaben-Erstellung darauf warten zu lassen; fehlgeschlagene Versuche werden bis zu 5-mal wiederholt, danach bleibt der Eintrag FAILED bis zu einem manuellen "Jetzt synchronisieren" oder einer erneuten Änderung des Quelldatensatzes. Pro Quelltyp (Aufgabe/Durchsicht/Behandlung/Fütterung/Ernte/Königinnen-Zuordnung) ist die Synchronisation einzeln über die GUI (Verwaltung > Automatisierung > Kalender-Sync) abschaltbar; Kalendereinträge werden als ganztägige Graph-Events (isAllDay: true) angelegt, um Zeitzonen-/Uhrzeit-Mehrdeutigkeiten zu vermeiden, da die zugrundeliegenden Fachdaten selbst keine Uhrzeit führen.

  10. Wartezeit manuell statt aus Wirkstoff-Datenbank abgeleitet: Treatment.withdrawalPeriodDays wird pro Behandlung von Hand eingetragen (laut Packungsbeilage des verwendeten Mittels), statt automatisch aus Produktname/Wirkstoff hergeleitet zu werden. Eine hartkodierte Zuordnung Produkt→Wartezeit wäre eine faktische, rechtlich relevante Behauptung über Tierarzneimittel-Zulassungen, die im Code weder aktuell gehalten noch verlässlich verifiziert werden kann; die Verantwortung für den korrekten Wert bleibt beim Imker. Die App warnt in der Ernte-Erfassung nur, wenn ein Wert hinterlegt wurde und dessen Fenster (endDate ?? date + withdrawalPeriodDays) noch nicht abgelaufen ist – sie blockiert die Ernte nicht hart, da Ernte-Datensätze auch nachträglich für bereits erfolgte, reale Ernten erfasst werden.

  11. Idempotenz per Opt-in: Anlege-Endpunkte, die Formulare und die Offline-Warteschlange wiederholen können (Durchsicht, Behandlung, Fütterung, Varroa, Schwarmkontrolle, Ernte, Aufgabe, Abfüllung, Lagerbewegung), tragen @Idempotent(). Der Client sendet je Vorgang einen Idempotency-Key; eine Wiederholung liefert die gespeicherte erste Antwort (IdempotencyRecord, 90 Tage). Bewusst kein globaler Interceptor, da gespeicherte Antworten sonst Geheimnisse (MFA-Setup, Tokens) enthalten könnten. Die Tabelle ist flüchtig und nicht Teil des Backups.

  12. Automatische Aufgaben sind Vorschläge: Task.sourceAutomationTrigger kennzeichnet von Regeln erzeugte Aufgaben. Je Regel und Objekt existiert höchstens eine offene Aufgabe. Keine voreingestellte Frist (fachlich nicht belastbar ableitbar); optional config.dueInDays je Regel. Die Varroa-Schwelle (Milben/Tag) wird nur mit Tagesraten des natürlichen Milbenfalls verglichen.

  13. API-Schlüssel für Maschinen-Zugriffe: Integrationen (Wabenhain-Shop) nutzen einen Schlüssel ack_… im Authorization: Bearer-Header statt eines Service-Kontos mit Passwort. Gespeichert wird nur der SHA-256-Hash (32 Zufallsbytes, daher kein langsamer KDF nötig); der Klartext erscheint einmal bei der Erstellung. Ein Schlüssel ist kein Benutzer: JwtAuthGuard setzt request.apiKey statt request.user, PermissionsGuard lässt ihn nur auf Routen, deren Berechtigung in seinen Scopes steht – @AnyAuthenticatedUser-Routen nie. Erlaubte Scopes sind eine kurze Allowlist (api-keys/api-key-scopes.ts, derzeit nur shop-sync:read). Verwaltung mit api-keys:manage. Schlüssel sind bewusst nicht Teil des Backups: nach einer Wiederherstellung wird ein neuer Schlüssel ausgestellt.

  14. Lebende vs. beendete Völker: Nur DISSOLVED, SOLD, DECEASED, MERGED sperren Umsetzen/Teilen/Vereinigen/Königinzuweisung (colonies/colony-status.ts). WEAK, CONSPICUOUS, QUEENLESS bleiben bearbeitbar.

  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 "<iss>|<oid>" (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').

deploy/k8s/postgres/ enthält das PostgreSQL-StatefulSet (ein Replica, volumeClaimTemplates für persistente Daten, postgres:18-alpine), den zugehörigen headless Service (apicore-postgres, DNS-Name fürs Backend) sowie secret.example.yaml für POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB. deploy/k8s/backend/secret.example.yamls DATABASE_URL zeigt auf diesen Service.

8. Backup & Restore

Schema-Migrationen (Reproduzierbarkeit): Ausschließlich prisma/migrations/ als Quelle der Wahrheit. Lokal/Entwicklung: npx prisma migrate dev. Gegen jede Produktions- oder Staging-Datenbank ausschließlich npx prisma migrate deploy (idempotent, wendet nur fehlende Migrationen an, kein Schema-Drift). prisma db push wird an keiner Stelle im Code, in Skripten oder im Dockerfile verwendet und ist für Produktions-/Staging-Umgebungen untersagt. Der Backend-Container führt migrate deploy und danach npm run seed selbst beim Start aus, bevor der Server Verbindungen annimmt (Dockerfile-CMD: npm run migrate:deploy && npm run seed && node dist/main.js) – kein separater Kubernetes-Job. Bei replicas: 2 starten mehrere Pods potenziell gleichzeitig; das ist unproblematisch, da Prisma für migrate deploy einen datenbankseitigen Advisory-Lock nutzt und prisma/seed.ts per Unique-Constraint auf User.email idempotent ist (verlierender Pod bekommt einen P2002-Fehler, den seed.ts abfängt und als No-op behandelt statt den Container abstürzen zu lassen) – eine Instanz wendet die Migrationen/den Seed an, die anderen finden danach nichts mehr zu tun. deploy/k8s/backend/deployment.yaml hat dafür eine startupProbe mit großzügigem failureThreshold, damit liveness/readiness den Container nicht neu starten, während die (beim allerersten Deploy ggf. länger laufende) Migration noch durchläuft.

Erster Admin-Zugang: prisma/seed.ts legt beim allerersten Start (leere User-Tabelle) einen Administrator an (SEED_ADMIN_EMAIL/SEED_ADMIN_PASSWORD, Default [email protected]/ChangeMe123!) und gibt URL (FRONTEND_ORIGIN), Benutzername und Passwort einmalig im Container-Log aus. Existiert der User schon (auch deaktiviert), passiert nichts – kein erneutes Log, kein erneutes Anlegen, damit spätere Passwortänderungen nicht durch veraltete Logs verwirrend überschrieben wirken und Zugangsdaten nicht bei jedem Neustart in die Logs wandern. Ersetzen: neuen Admin über /settings/users anlegen, dann den alten deaktivieren (Löschen gibt es nicht, siehe "Historie statt Löschen") – die Lockout-Sperre in UsersService.assertNotLastSystemRoleUser verhindert ohnehin, den letzten aktiven Admin zu deaktivieren.

Datensicherung: Seit der Entscheidung, PostgreSQL selbst im Cluster zu betreiben (siehe CLAUDE.md), liegt die Backup-Verantwortung bei diesem Projekt, nicht mehr bei einem externen Cluster-/DBA-Betrieb. Umgesetzt als anwendungsseitiges BackupModule (ApiCore-Backend/src/backup/) statt der ursprünglich hier vorgesehenen reinen pg_dump/PITR-Strategie:

  • Export/Import: BackupService.exportAll()/.importAll() sichert wirklich alle Modelle (Betriebsdaten, Benutzer/Rollen/Rechte, Audit-Log, hochgeladene Dokumente inkl. Dateiinhalt als Base64) als eine versionierte JSON-Datei; Import ist upsert-basiert (Rollen/Berechtigungen über key/resource+action statt roher id, damit ein Restore auch in ein anderes Deployment funktioniert) und läuft in einer einzigen $transaction.
  • Automatischer Zeitplan: BackupSchedulerService registriert einen @nestjs/schedule-Cron-Job, der die Bundle-Datei periodisch auf BACKUP_STORAGE_PATH schreibt und alte Stände nach BACKUP_RETENTION_COUNT rotiert. Pfad/Zeitplan/Aufbewahrung sind sowohl per Env-Variable als auch über die GUI (Verwaltung > Backup) konfigurierbar – eine gesetzte Env-Variable hat dabei immer Vorrang vor dem GUI-Wert. Bei replicas: 2 verhindert ein Datei-Mtime-Guard (kein DB-Lock nötig), dass beide Pods im selben Tick doppelt sichern; ohne gesetzten Pfad bleibt der Zeitplan inaktiv, manueller Export/Import über die UI funktioniert davon unabhängig immer.
  • 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.