12 KiB
MPM – Architekturdokumentation
1. Leitentscheidung
Die Management-Plattform ist ein Application Host und Gateway für Module – nicht selbst die Fachanwendung.
Management-Plattform Module (ab Phase 3)
├── Identität & Login ├── eigene Oberfläche
├── Benutzer & Rollen ├── eigene Business-Logik
├── Rechte (plattformweit) ├── eigene API
├── Modul-Verwaltung ├── eigene Daten (eigenes Schema)
├── Routing / Gateway ├── eigene Migrationen
├── API & Administration └── optional eigene Modulrechte
└── Audit & Monitoring
Die Plattform darf niemals von einem einzelnen Modul abhängig sein. Wird ein Modul entfernt, bleiben Login, Benutzerverwaltung, Administration, Rechteverwaltung und alle anderen Module voll funktionsfähig.
2. Komponenten & Container-Layout
Internet
│
▼ HTTPS (Produktion; lokal :8080)
Docker Container "mpm-platform" (Supervisor mit engen Capabilities)
┌──────────────────────────────────────────────┐
│ Supervisor (Prozessmanager, Crash-Recovery) │
│ │ │
│ ├── nginx Reverse Proxy :8080 │
│ ├── platform-backend NestJS API 127.0.0.1:3000│
│ └── [ab Phase 3: Modul-Prozesse, │
│ z. B. Kalender 127.0.0.1:41001] │
└──────────────────────────────────────────────┘
│
▼
PostgreSQL "mpm-postgres" (eigener Container,
persistentes Volume "postgres-data")
Grundsätze:
- MPM bleibt der Managementcontainer. Neue Module laufen in eigenen Compose-Stacks mit optionalen Datenbankservices.
- Nur der MPM-Backendprozess erhält Zugriff auf den Docker-Socket; Modulcontainer erhalten ihn nie.
- Docker-Socket-Zugriff entspricht weitreichender Kontrolle über den Docker-Host. Installationsrechte müssen deshalb vertrauenswürdigen Administratoren vorbehalten bleiben.
- Interne Ports (z. B. 3000, 41001+) werden nie nach außen veröffentlicht; nur Nginx lauscht auf 8080.
- PostgreSQL liegt außerhalb des Applikationscontainers → Container-Neustarts/Updates zerstören keine Nutzdaten.
- Supervisor und Nginx-Master erhalten nur die Container-Capabilities
SETUID,SETGIDundKILL. - Backend und Nginx-Worker laufen als
app; Modulservices werden mitno-new-privilegesgestartet und intern über ein eigenes Gateway-Netz geroutet.
3. Prozessmodell
Supervisor verwaltet Start/Stop/Restart, Crash-Recovery und Logging aller Prozesse:
| Prozess | Adresse | Aufgabe |
|---|---|---|
| nginx | 0.0.0.0:8080 | Reverse Proxy, statisches Frontend, Security-Header |
| platform-backend | 127.0.0.1:3000 | Management-API /api/v1 |
| Modul-Prozesse (ab Phase 3) | 127.0.0.1:41001+ | Fachanwendungen |
Stürzt ein Modulprozess ab, startet Supervisor ihn automatisch neu – die Management-Plattform läuft weiter.
4. Request-Flow
Login
Browser ──POST /api/v1/auth/login──▶ Nginx ──▶ NestJS
│ Validierung (Zod) + IP-Rate-Limit + Argon2id
│ Session in PostgreSQL anlegen (Token nur gehasht gespeichert)
◀── Set-Cookie: mpm_session (HttpOnly, SameSite=Lax)
Set-Cookie: mpm_csrf (lesbar für CSRF-Doppel-Submit)
Authentifizierter Request
Browser ──▶ Nginx ──▶ SessionGuard (Session gültig? User aktiv?)
├── CsrfGuard (bei POST/PATCH/DELETE: X-CSRF-Token)
├── RolesGuard (RBAC: ADMIN/USER)
└── Controller/Service (Business-Logik)
Modul-Routing (ab Phase 4, geplant)
Browser ──▶ MPM-Host (/api/v1/auth/module-open/slug)
└── Einmal-Ticket ──▶ eigener Modul-Host (/<slug>) ──▶ Management-Gateway
├── User identifizieren (Session)
├── Permission Check (user_module_permissions)
├── DENIED → 403
└── ALLOWED → Modul-Gateway → Modulprozess
Ein Modul vertraut niemals allein auf die URL; die Plattform übergibt die Identität sicher an das Modul (Modul-API-Vertrag, Phase 6). Der Modulhost bedient keine Management-API. Ein kurzlebiges, einmalig verwendbares Ticket stellt dort eine an die Plattformsession gebundene Modulsession aus. Modulpfade für Assets und API-Aufrufe müssen unter /<slug>/ liegen; root-relative /api/ ist auf dem Modulhost gesperrt. In Produktion verwenden Plattform-Cookies den __Host--Präfix.
5. Datenmodell (Phase 1)
Alle Plattform-Tabellen liegen im Standard-Schema (ab Phase 3 Auslagerung in ein management-Schema und pro Modul ein eigenes Schema).
| Tabelle | Zweck |
|---|---|
roles |
Globale Rollen: ADMIN, USER |
users |
Benutzer mit Argon2id-Hash, Rollen-FK und Fehlversuchs-Zähler |
sessions |
Serverseitige Sessions (Token SHA-256-gehasht, CSRF-Token, Gleitende Verlängerung) |
audit_logs |
Zentrales Audit (LOGIN_SUCCESS, LOGIN_FAILED, LOGOUT, …) |
schema_migrations |
Angewandte Migrationen (eigener Runner mit Advisory-Lock) |
Geplant (Phase 3+): modules, user_module_permissions, module_settings, system_settings, api_clients.
6. Authentifizierung & Sessions
- Argon2id (19 MiB, t=2, p=1 – OWASP-Empfehlung) für Passwort-Hashing; niemals Klartextpasswörter.
- Serverseitige Sessions: 256-Bit-Zufalls-Token im
HttpOnly-Cookie; in der DB wird nur der SHA-256-Hash gespeichert.Secure(produktiv),SameSite=Lax, kurze Laufzeit (SESSION_TTL_MINUTES), gleitende Verlängerung. - CSRF-Schutz: Doppel-Submit – Server speichert pro Session ein CSRF-Token; bei jedem zustandsändernden Request muss der Header
X-CSRF-Token(konstantzeitvergleich) übereinstimmen. - Rate Limiting: In-Memory-Fenster pro IP für Login (Standard: 10 Versuche / 5 Minuten).
- Kein Account Lockout: Fehlversuche sperren das Zielkonto nicht, damit Dritte keine gezielte Konto-DoS auslösen können.
- Keine User-Enumeration: Unbekannter Benutzer, inaktiver Benutzer und falsches Passwort liefern dieselbe generische 401-Meldung.
- Audit-Log: Jeder Login-Versuch (Erfolg/Misserfolg mit Grund), Logout.
7. Sicherheitsmaßnahmen (Phase 1)
- Security-Header via Helmet (Backend) und Nginx (Frontend): CSP,
X-Frame-Options: DENY,X-Content-Type-Options,Referrer-Policy. - Zod-Validierung aller Eingaben (serverseitig, clientseitige Validierung ist nur UX).
- SQL nur parametrisiert (
pg), keine String-Konkatenation. - Strukturierte Fehlermeldungen ohne Stack-Traces nach außen.
- Secrets ausschließlich über Umgebungsvariablen (
.envist gitignored). - Interne Ports nicht veröffentlicht; PostgreSQL nur auf Loopback des Hosts gemappt.
- Unprivilegierter Container-Benutzer; kein Root in der Laufzeit.
Security Hardening (Penetrationstests, Dependency/Container-Scans, HSTS, Backup/Restore) ist gemäß Arbeitsplan Phase 10.
8. API-Übersicht (Phase 1)
| Methode | Pfad | Auth | Beschreibung |
|---|---|---|---|
| POST | /api/v1/auth/login |
Public | Anmeldung, setzt Session-Cookies |
| POST | /api/v1/auth/logout |
Session | Beendet die aktuelle Session |
| GET | /api/v1/auth/me |
Session | Angemeldeter Benutzer (Id, Rolle, …) |
| GET | /api/v1/health |
Public | Systemstatus (Backend, DB-Latenz, Version, Uptime) |
| GET | /api/v1/users |
Admin | Alle Benutzer |
| POST | /api/v1/users |
Admin | Benutzer anlegen (409 bei Duplikat) |
| GET | /api/v1/users/:id |
Admin | Einzelner Benutzer |
| PATCH | /api/v1/users/:id |
Admin | Bearbeiten / Aktivieren / Deaktivieren |
| PATCH | /api/v1/users/:id/password |
Admin | Passwort zurücksetzen |
| DELETE | /api/v1/users/:id |
Admin | Benutzer löschen |
| GET | /api/v1/profile |
Session | Eigenes Profil |
| PATCH | /api/v1/profile/password |
Session | Eigenes Passwort ändern |
| GET | /api/v1/modules |
Admin | Alle Module |
| POST | /api/v1/modules/install |
Admin | ZIP-Paket installieren (multipart, Feld package) |
| GET | /api/v1/modules/:id |
Admin | Einzelnes Modul |
| POST | /api/v1/modules/:id/start |
Admin | Modul starten |
| POST | /api/v1/modules/:id/stop |
Admin | Modul stoppen |
| POST | /api/v1/modules/:id/restart |
Admin | Modul neu starten |
| PATCH | /api/v1/modules/:id/enabled |
Admin | Modul aktivieren/deaktivieren |
| GET | /api/v1/modules/:id/health |
Admin | Healthcheck ausführen |
| DELETE | /api/v1/modules/:id |
Admin | Modul entfernen |
OpenAPI/Swagger UI: /api/docs · Ab Phase 4: dynamisches Routing /slug.
Schutzregeln der Benutzerverwaltung
- Keine Selbst-Deaktivierung und keine Selbst-Löschung (400)
- Der letzte aktive Administrator kann nicht herabgestuft, deaktiviert oder gelöscht werden (400)
- Deaktivierte Benutzer werden sofort von allen Sessions abgemeldet
- Passwort-Reset (Admin) meldet den Benutzer von allen Sessions ab
- Eigenes Passwort ändern meldet alle übrigen Sessions ab (aktuelle bleibt aktiv)
9. Modul-System (Phase 3)
Manifest-Vertrag
Jedes Modul-Paket (ZIP, max. 10 MB) enthält ein module.json:
{
"id": "calendar",
"name": "Kalender",
"version": "1.0.0",
"slug": "kalender-tool",
"description": "Kalenderverwaltung",
"author": "…",
"runtime": "node",
"entrypoint": "server.js",
"port": 41001,
"healthcheck": "/health",
"apiVersion": "v1"
}
Validierung per Zod: ID/Slug-Muster, SemVer, Port 41000–41999, kein Pfad-Traversal im Entrypoint, Runtime node, apiVersion v1.
Lifecycle
INSTALLED → STARTING → RUNNING → STOPPING → STOPPED
↓ ↓
ERROR ERROR DISABLED (via Enable/Disable)
Sicherheitsmaßnahmen
- Zip-Slip-Schutz: Alle ZIP-Einträge müssen im Zielverzeichnis bleiben
- Minimale Prozess-ENV: Module erhalten nur
PATH,NODE_ENV,PORT– keine Plattform-Secrets - Spawn ohne Shell: keine Command-Injection möglich
- Eigene Log-Dateien pro Modul (
/app/data/logs/module-<id>.log) - Startup-Grace: Healthcheck mit 10 Retries, bevor ein Modul als ERROR gilt
- Isolation: Ein abgestürztes Modul (ERROR) beeinträchtigt weder Plattform noch andere Module
- Persistente Volumes: Modul-Dateien und Logs überleben Container-Updates
Minimal-API je Modul (Phase 6): GET /health, GET /api/manifest, GET /api/me. Details zum Paketformat: modules/README.md.
10. Architektur-Entscheidungen (ADR-Kurzform)
| # | Entscheidung | Begründung |
|---|---|---|
| 1 | Serverseitige Sessions statt reiner JWTs | Widerrufbar (Logout, Deaktivierung); Modulidentität wird über kurzlebige, signierte Gateway-Assertions übergeben |
| 2 | Opaque 256-Bit-Token, DB speichert nur SHA-256-Hash | DB-Leck kompromittiert keine Sessions |
| 3 | PostgreSQL außerhalb des Applikationscontainers | Persistenz bei Container-Updates, klare Trennung von Zustand und Compute |
| 4 | Supervisor statt systemd im Container | Restricted-capability supervisor starts least-privilege services and monitors crashes |
| 5 | Nginx im Container als einziger öffentlicher Endpunkt | Zentrales Routing, Security-Header, interne Ports bleiben verborgen |
| 6 | pg + eigener Migration-Runner statt ORM |
Wenig Abhängigkeiten, volle SQL-Kontrolle, Migrationen transaktionssicher mit Advisory-Lock |
| 7 | Zod statt class-validator | Ein Validierungs-Framework für Frontend und Backend, TypeScript-Inferenz |
| 8 | Monorepo mit unabhängigen Apps (kein Workspace-Tooling) | Einfache, cache-freundliche Docker-Builds, keine Tool-Lock-in |
11. Frontend-Architektur
- Schichten:
components/ui(Design-System) →components/layout(App-Shell) →features/*(Fachseiten) →pages(Fehlerseiten). - Datenzugriff ausschließlich über
lib/api-client(CSRF-Header, 401-Behandlung) und TanStack Query. - Routing: öffentliche Login-Seite, geschützter Bereich (
RequireAuth), Admin-Bereich (RequireAdmin, RBAC im Frontend nur UX – erzwungen wird serverseitig). - Responsiv: Sidebar wird auf Mobilgeräten zur Overlay-Navigation.