docs(quick-260923-dhh): Proxmox-Modul PVE/PBS/PMG - Research
This commit is contained in:
+358
@@ -0,0 +1,358 @@
|
|||||||
|
# Quick-Aufgabe 260923-dhh: Proxmox-Modul (PVE/PBS/PMG) — Research
|
||||||
|
|
||||||
|
**Researched:** 2026-09-23
|
||||||
|
**Domain:** Proxmox VE/PBS/PMG REST-API (nur lesend), NestJS-Hintergrunddienst mit Zertifikatsausnahme, Mandantentrennung (Prisma/RLS), Modul-/Kachel-Registrierung im Bestand
|
||||||
|
**Confidence:** MEDIUM — Proxmox-API-Formen (Auth-Header, `cluster/resources`, PBS-Datastore, PMG-Statistik) sind aus offizieller Doku UND Foren-Diskussion zusammengetragen (offizielle API-Viewer sind reine JS-Apps und liefern beim Abruf keinen Text); Bestandsmuster (Verschlüsselung, Scheduler, RLS, Modul-Registrierung) sind HIGH, weil aus tatsächlich gelesenem Code dieses Repos zitiert.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Das Proxmox-Modul ist reine Beobachtung (kein Schreibzugriff) auf bis zu drei Produkttypen — PVE, PBS, PMG —, die derselbe Mandant in beliebiger Zahl in den Einstellungen einträgt (Adresse + Zugang, wahlweise API-Token oder Benutzer/Passwort). Alle drei Produkte teilen dieselbe API-Familie (REST, `/api2/json/...`), aber mit produktspezifischem Token-Präfix (`PVEAPIToken`/`PBSAPIToken`) — PMG hat laut aktueller Foren- und Roadmap-Lage **keine** API-Token-Unterstützung, nur Ticket-Login, weshalb der Zugang für PMG-Server ausschließlich Benutzer/Passwort sein kann (Konsequenz für die Einstellungs-UI: das Token-Feld ist bei Typ „PMG" auszublenden). Für reine Leseabfragen ist ein CSRF-Token nie nötig — weder bei Token- noch bei Ticket-Auth —, weil CSRF nur GET-fremde Schreiboperationen betrifft; das vereinfacht die Ticket-Variante erheblich (Cookie genügt).
|
||||||
|
|
||||||
|
Der Bestand liefert für jeden Baustein bereits ein direktes Vorbild: `CalendarSource` ist die richtige Schema-Vorlage (mehrere verschlüsselte Fremdsystem-Zugänge pro Mandant, nicht ein Singleton wie `DkvModuleConfig`); `CryptoService`/`LdapConfig.tlsRejectUnauthorized` zeigen sowohl die Verschlüsselung als auch den **admin-gesteuerten, pro Zeile umschaltbaren** Zertifikats-Bypass — das ist die bessere Vorlage als die pauschale, immer-an-Ausnahme in `icon-discovery.service.ts`, weil hier echte Zugangsdaten über die Leitung gehen, nicht nur ein Favicon; und `TenderSchedulerService` (kombiniert mit `DkvSchedulerService`) zeigt exakt das Timing-Problem, das ein neuer Hintergrunddienst vermeiden muss: `onModuleInit`-Reihenfolge ist zwischen NestJS-Modulen nicht garantiert, `onApplicationBootstrap` läuft dagegen nachweislich nach jedem `onModuleInit` und ist deshalb für einen Proxmox-Planer, der die Modul-Seed-Daten voraussetzt, die richtige Lebenszyklus-Stufe — nicht die von `DkvSchedulerService` tatsächlich verwendete `onModuleInit`.
|
||||||
|
|
||||||
|
Für die Frage „live abfragen oder zwischenlagern" gibt der Bestand eine eindeutige Antwort: sowohl DKV (`DkvInvoiceHistory`) als auch Tender-Radar (`Tender`) schreiben Hintergrund-Polling-Ergebnisse in eine eigene Tabelle und die Seite liest ausschließlich daraus — kein Modul in diesem Projekt holt Fremddaten live bei Seitenaufruf. Für Proxmox ist das erst recht richtig: ein Dashboard-Widget, das bei jedem Öffnen drei bis N Server live abfragt, wäre spürbar langsam und bei nicht erreichbarem Server sogar blockierend. Empfehlung: ein Cron-Auftrag pro Mandant (DKV-Muster) mit `onApplicationBootstrap`-Timing (Tender-Muster) schreibt die zuletzt gemessenen Werte (Knoten/VM/Container-Zustand, PBS-Datastore-Belegung + letzter Backup-/Verify-Lauf, PMG-Tageszahlen) in eine Zwischenlagertabelle je Server; das Dashboard und die Modulseite lesen ausschließlich diese Tabelle.
|
||||||
|
|
||||||
|
**Primary recommendation:** `ProxmoxServer`-Modell nach `CalendarSource`-Vorbild (mehrere Zeilen je Mandant, `encryptedTokenSecret`/`encryptedPassword` über `CryptoService`, `tlsRejectUnauthorized Boolean @default(true)` pro Zeile); ein `ProxmoxSchedulerService` nach `DkvSchedulerService`-Vorbild (ein Cron-Auftrag je Mandant) aber mit `OnApplicationBootstrap` statt `OnModuleInit`; ein `ProxmoxSnapshot`/`ProxmoxServerStatus`-Cache-Modell, das der Planer beschreibt und Widget/Modulseite lesen; für Zertifikatsausnahmen ein pro Aufruf gebauter `undici.Agent({ connect: { rejectUnauthorized: false } })`, **nur** wenn `tlsRejectUnauthorized === false` auf genau diesem Server steht — kein modulweiter, kein globaler Bypass.
|
||||||
|
|
||||||
|
## Architectural Responsibility Map
|
||||||
|
|
||||||
|
| Capability | Primary Tier | Secondary Tier | Rationale |
|
||||||
|
|------------|-------------|----------------|-----------|
|
||||||
|
| Proxmox-Server-Verwaltung (CRUD Adresse+Zugang) | API / Backend | Frontend Server (Formulare) | Verschlüsselung und RLS-Bindung müssen serverseitig passieren, wie bei `LdapConfig`/`CalendarSource` |
|
||||||
|
| Periodische Abfrage PVE/PBS/PMG | API / Backend (Hintergrunddienst) | — | Kein Nutzer-Trigger; Cron-Auftrag wie DKV/Tender, kein Browser-Bezug |
|
||||||
|
| Zwischenlagerung der Messwerte | Database / Storage | API / Backend (Schreiber) | Dashboard-Geschwindigkeit verlangt Cache-Tabelle statt Live-Fetch (siehe Summary) |
|
||||||
|
| Dashboard-Kachel „Proxmox" | Browser (Rendering) | API / Backend (liefert Cache-Daten) | Folgt dem in `docs/anleitung-entwicklung.md` beschriebenen Drei-Stellen-Muster |
|
||||||
|
| Modulseite (Server-Übersicht, Details) | Frontend Server (SSR-Gate) | API / Backend | `ModuleAccessGate` + eigenes `layout.tsx`, wie bei den vier bestehenden fest verdrahteten Modulverzeichnissen |
|
||||||
|
| Zugriffskontrolle auf Proxmox-Endpunkte | API / Backend | — | `@UseModule('proxmox')` auf dem Controller, unabhängig vom Frontend-Gate |
|
||||||
|
| TLS-Ausnahme für selbstsigniertes Zertifikat | API / Backend (pro Aufruf) | — | Muss am Ort des Fetch-Aufrufs entschieden werden, nicht global (Prozessumgebung bleibt streng) |
|
||||||
|
|
||||||
|
## 1. Proxmox-API konkret
|
||||||
|
|
||||||
|
### Anmeldung — API-Token
|
||||||
|
|
||||||
|
Alle drei Produkte senden den Token im `Authorization`-Header, aber mit unterschiedlichem Schema-Namen und leicht unterschiedlicher Werteform:
|
||||||
|
|
||||||
|
| Produkt | Header-Form | Quelle |
|
||||||
|
|---|---|---|
|
||||||
|
| PVE | `Authorization: PVEAPIToken=USER@REALM!TOKENID=SECRET` (ein `=` vor dem Secret) | `[CITED: pve.proxmox.com/pve-docs/pveum-plain.html]` |
|
||||||
|
| PBS | `Authorization: PBSAPIToken=USER@REALM!TOKENID:SECRET` (ein `:` vor dem Secret — **anderes Trennzeichen als PVE**) | `[CITED: pbs.proxmox.com/docs/user-management.html]` |
|
||||||
|
| PMG | **kein Token-Schema.** Foren-Aussage (proxmox.com-Forum, 2024/2025): „PMG doesn't have API tokens, only Tickets." Kein Gegenbeleg in der aktuellen `pmg-admin-guide` gefunden. | `[CITED: forum.proxmox.com/threads/why-are-there-no-api-tokens.156802]` — Forenaussage, nicht offizielle Referenzdoku; als `[ASSUMED]` in die Planung übernehmen und vor dem Bau am echten PMG-Server verifizieren (`checkpoint:human-verify`) |
|
||||||
|
|
||||||
|
**Konsequenz für die Einstellungs-UI:** Server-Typ „PMG" darf die Auswahl „API-Token" nicht anbieten (oder muss sie beim Speichern ablehnen) — sonst legt der Admin einen Zugang an, der nie funktioniert.
|
||||||
|
|
||||||
|
### Anmeldung — Ticket (Benutzer/Passwort)
|
||||||
|
|
||||||
|
Identischer Mechanismus für alle drei Produkte (PMG: „funktioniert exakt wie bei PVE, PVE durch PMG ersetzen", Foren-Zitat):
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api2/json/access/ticket
|
||||||
|
Body: username=<user>@<realm>&password=<pw>
|
||||||
|
```
|
||||||
|
|
||||||
|
Antwort (JSON, `data`-Objekt): `ticket` (signierter Wert, Form `PVE:user@realm:...`), `CSRFPreventionToken`, `username`. `[CITED: pve.proxmox.com/wiki/Proxmox_VE_API]`
|
||||||
|
|
||||||
|
Folgeanfragen senden das Ticket als Cookie: `Cookie: PVEAuthCookie=<ticket>` (bei PBS/PMG vermutlich `PBSAuthCookie`/`PMGAuthCookie` — **nicht in der Doku bestätigt gefunden, `[ASSUMED]`**, vor Bau verifizieren). Ticket-Lebensdauer 2 Stunden bei PVE `[CITED: pve.proxmox.com/wiki/Proxmox_VE_API]`; ein Forumsbeitrag nennt abweichend 40 Sekunden für den kurzlebigen VNC-Ticket-Typ — **nicht derselbe Tickettyp**, für den hier verwendeten Auth-Ticket gilt die 2-Stunden-Angabe aus der offiziellen Wiki-Seite.
|
||||||
|
|
||||||
|
**CSRF — die zentrale Vereinfachung für dieses Modul:** `CSRFPreventionToken` ist laut offizieller Doku **nur für schreibende Anfragen (POST/PUT/DELETE)** nötig; „GET requests do not require this token" `[CITED: pve.proxmox.com/wiki/Proxmox_VE_API]`. Da dieses Modul ausschließlich liest (Auftrag: „NUR BEOBACHTEN"), entfällt die CSRF-Handhabung vollständig — auch bei Ticket-Auth genügt das Cookie. Bei Token-Auth ist CSRF ohnehin nie nötig, für keine Methode `[CITED: gleiche Quelle]`.
|
||||||
|
|
||||||
|
### PVE: Knoten/VMs/Container in einer Abfrage
|
||||||
|
|
||||||
|
`GET /api2/json/cluster/resources` liefert **alle** Objekttypen (`vm`, `node`, `storage`, weitere) in einer einzigen Anfrage, optional gefiltert per `?type=vm`. Für VM/Container-Zeilen kommen laut mehreren Forenbelegen die Felder `cpu`, `maxcpu`, `mem`, `maxmem`, `disk`, `maxdisk`, `netin`, `netout`, `diskread`, `diskwrite`, `node`, `vmid`, `status`, `uptime`, `type` zurück; für Storage-Zeilen `content`, `disk`, `maxdisk`, `node`, `plugintype`, `shared`, `status`, `storage`, `type`. `[CITED: mehrere forum.proxmox.com-Threads, keine Feldliste in der offiziellen API-Referenz gefunden — API-Viewer ist eine reine Vue-App und liefert per Abruf keinen Text]`
|
||||||
|
|
||||||
|
Gegenüber `/nodes/{node}/qemu` + `/nodes/{node}/lxc` (je Knoten zwei Aufrufe) ist `cluster/resources` der klare Gewinner für ein Übersichts-Dashboard: **eine** Anfrage liefert Knoten, VMs, Container und Storage über den gesamten (Multi-Node-)Cluster hinweg. Für Detailansichten einer einzelnen VM (z. B. Konfiguration) bleibt der gezielte `/nodes/{node}/qemu/{vmid}/...`-Pfad nötig — `cluster/resources` liefert nur die Übersichtsfelder, keine volle Konfiguration.
|
||||||
|
|
||||||
|
### PBS: Datastores, Backups, Verify
|
||||||
|
|
||||||
|
Aus Forenbelegen (keine vollständige Feldliste aus offizieller Referenz erreichbar):
|
||||||
|
- `GET /api2/json/status/datastore-usage` — Belegung aller Datastores in einer Abfrage (Gesamt/Belegt/Frei). `[CITED: forum.proxmox.com/threads/inquiry-about-the-proxmox-backup-api.166986]`
|
||||||
|
- `GET /api2/json/admin/datastore/{store}/status` — Status eines einzelnen Datastores.
|
||||||
|
- `GET /api2/json/admin/datastore/{store}/snapshots` — Liste der Sicherungen; enthält laut Community-Doku ein `verification`/`verify-state`-Feld je Snapshot (Ergebnis der letzten Prüfung) sowie `backup-time`, `size`. **Exakte Feldnamen nicht aus Primärquelle bestätigt — `[ASSUMED]`, vor Bau gegen einen echten PBS-Server oder den API-Viewer im Browser verifizieren.**
|
||||||
|
|
||||||
|
### PMG: Tageszahlen
|
||||||
|
|
||||||
|
`GET /api2/json/statistics/mail` (optional `starttime`/`endtime`) liefert laut `pmgsh`-Community-Beleg `count`, `count_in`, `count_out`, `spamcount_in`, `spamcount_out`, `viruscount_in`, `viruscount_out`. `[CITED: forum.proxmox.com, Centreon-Plugin-Doku]` Ein Quarantäne-Zähler steht vermutlich unter einem separaten `/quarantine/...`-Pfad — nicht recherchiert, für die erste Fassung ggf. entbehrlich (siehe Fallstricke).
|
||||||
|
|
||||||
|
### Nur-Lese-Rollen
|
||||||
|
|
||||||
|
| Produkt | Rolle | Beleg |
|
||||||
|
|---|---|---|
|
||||||
|
| PVE | `PVEAuditor` — „read only access" | `[CITED: pve.proxmox.com/pve-docs/pveum-plain.html]` |
|
||||||
|
| PBS | `Audit` (global) bzw. feiner `DatastoreAudit` — „Can view datastore metrics, settings and list content. But is not allowed to read the actual data." | `[CITED: pbs.proxmox.com/docs/user-management.html]` |
|
||||||
|
| PMG | `Auditor` — „read-only access to the whole configuration, can access logs and view statistics" | `[CITED: mehrere Foren-/Datasheet-Quellen, keine Primärquelle mit exaktem Wortlaut erreicht]` |
|
||||||
|
|
||||||
|
Empfehlung an den Admin-Helptext in den Einstellungen: für den API-Token/Benutzer, den Tessera nutzt, jeweils NUR diese Rolle zuweisen — ein Schreibrecht wird von diesem Modul nie gebraucht (deckt sich mit „NUR BEOBACHTEN").
|
||||||
|
|
||||||
|
### Fehlerverhalten
|
||||||
|
|
||||||
|
- **Falscher Zugang (Token/Passwort falsch):** HTTP 401. PVE-Foren-Belege zeigen 401 auch für andere Auth-Fehlklassen (abgelaufenes Ticket, falsches CSRF-Token) — Proxmox scheint 401 breiter zu verwenden als die übliche REST-Konvention 401=nicht authentifiziert/403=nicht berechtigt. **Nicht aus Primärquelle mit expliziter Statuscode-Tabelle bestätigt — `[ASSUMED]`.** Für die Fehlermeldung im UI heißt das: einen expliziten 403-Sonderfall separat von 401 zu behandeln lohnt sich vermutlich nicht; „Zugang abgelehnt (401)" als eine gemeinsame Meldung ist robuster als eine Unterscheidung, die die API evtl. gar nicht liefert.
|
||||||
|
- **Abgelaufenes Ticket:** 401, Meldung enthält meist „invalid ticket"/„permission denied" im Klartext-Body — für eine bessere Fehlermeldung lohnt sich das Parsen des `errors`-Feldes der JSON-Antwort.
|
||||||
|
- **Server nicht erreichbar (falsche Adresse, Netzwerk, Port zu):** **kein HTTP-Status** — der Fetch-Aufruf selbst schlägt fehl (`ECONNREFUSED`, `ETIMEDOUT`, `ENOTFOUND`/DNS-Fehler; bei `undici`/nativem `fetch` als geworfener `TypeError`/`FetchError`, nicht als Response mit Statuscode). Die Proxmox-Serviceklasse muss also zwei getrennte Fehlerpfade behandeln: HTTP-Antwort mit Statuscode ≠ 2xx (Zugang/Berechtigung) versus geworfene Exception ohne Response (Erreichbarkeit) — dieselbe Unterscheidung, die `icon-discovery.service.ts` mit seinem AbortController-Timeout + try/catch bereits trifft (`fetchWithRedirectGuard`, Zeilen 227–271: `catch { return null; }` fängt genau diesen Fall).
|
||||||
|
|
||||||
|
## 2. Selbstsignierte Zertifikate
|
||||||
|
|
||||||
|
**Vorlage 1 (Mechanik):** `apps/api/src/favorites/icon-discovery.service.ts:33–37` — Node 24s **globales** `fetch` ignoriert einen `Agent`/Dispatcher aus dem `undici`-Paket (andere Klasse als das intern gebündelte undici); nur `undiciFetch(url, { dispatcher })` (expliziter Import aus dem `undici`-Modul) respektiert einen eigenen Dispatcher. Gemessen und im Kommentar dokumentiert:
|
||||||
|
> „`undiciFetch(url, { dispatcher: new Agent(...) })` -> Status 200; `globalThis.fetch` derselben URL -> DEPTH_ZERO_SELF_SIGNED_CERT." `[VERIFIED: apps/api/src/favorites/icon-discovery.service.ts:33-37]`
|
||||||
|
|
||||||
|
`undici` ist bereits direkte Abhängigkeit von `apps/api` — `"undici": "7.28.0"` `[VERIFIED: apps/api/package.json:52]` — **kein neues Paket nötig**.
|
||||||
|
|
||||||
|
**Vorlage 2 (Steuerung — besser geeignet als icon-discovery's Immer-an-Ausnahme):** `LdapConfig.tlsRejectUnauthorized Boolean @default(true)` `[VERIFIED: apps/api/prisma/schema.prisma:65-83, Feld "tlsRejectUnauthorized Boolean @default(true)" in Zeile 76]` — ein **pro Zeile umschaltbares** Feld, vom Admin beim Anlegen/Bearbeiten des Zugangs gesetzt, Default „prüfen" (sicherer Default). `ldap.service.ts` baut daraus die Client-Optionen:
|
||||||
|
> „skip TLS verification" flag (`tlsRejectUnauthorized === false`)" `[VERIFIED: apps/api/src/ldap/ldap.service.ts:168]`
|
||||||
|
|
||||||
|
**Für Proxmox kombinieren:** `ProxmoxServer` bekommt dasselbe Feld `tlsRejectUnauthorized Boolean @default(true)`. Der Fetch-Aufruf für genau diesen Server baut **conditional** einen `undici.Agent({ connect: { rejectUnauthorized: false } })` nur wenn diese eine Zeile das Feld auf `false` gesetzt hat — nicht wie in `icon-discovery.service.ts` eine für die ganze Datei geltende Modul-Konstante `LENIENT_TLS_AGENT`, sondern je Aufruf aus dem gelesenen Serverdatensatz konstruiert. Das erfüllt exakt die Vorgabe „ausdrücklich nur für die vom Administrator eingetragenen Adressen, nicht global": kein prozessweiter Bypass, keine `NODE_TLS_REJECT_UNAUTHORIZED`-Umgebungsvariable (dieses Muster ist im Kommentar von `icon-discovery.service.ts` bereits ausdrücklich als verboten markiert, Zeile 30: „insbesondere NICHT ueber die Node-Umgebungsvariable, die mit NODE_TLS_ beginnt" `[VERIFIED: apps/api/src/favorites/icon-discovery.service.ts:30]`).
|
||||||
|
|
||||||
|
Standardmäßig Proxmox-Zertifikate akzeptieren zu **verweigern** (Default `true`) ist hier die richtige Entscheidung, anders als bei `icon-discovery.service.ts` (dort werden nur Favicons geholt, keine Zugangsdaten übertragen) — bei Proxmox gehen Token/Passwort über dieselbe Verbindung, ein blindes „immer tolerant" würde einen Site-in-the-Middle-Angriff auf die Zugangsdaten erleichtern.
|
||||||
|
|
||||||
|
## 3. Anschlussstellen im Bestand
|
||||||
|
|
||||||
|
### Verschlüsselte Zugangsdaten
|
||||||
|
|
||||||
|
`CryptoService` (`apps/api/src/crypto/crypto.service.ts`) ist die einzige Verschlüsselungsschicht im Projekt — AES-256-GCM, Schlüssel aus `TESSERA_ENCRYPTION_KEY`, Format `iv:authTag:ciphertext` (hex, `:`-getrennt) `[VERIFIED: apps/api/src/crypto/crypto.service.ts:70-84]`. `LdapConfigService` zeigt das vollständige Muster: verschlüsseln beim Schreiben (`this.crypto.encrypt(dto.bindPassword)`), entschlüsseln zentral in EINER privaten Methode (`decryptBindPassword`), API-Antworten maskieren das Feld ('********') im Controller, nicht im Service `[VERIFIED: apps/api/src/ldap/ldap-config.service.ts:117-133]`. Für Proxmox: `encryptedTokenSecret`/`encryptedPassword` genauso behandeln — zwei Felder, weil Token-Secret und Passwort unterschiedliche Auth-Methoden sind, beide nullable (nur eines pro Zeile gesetzt, je nach gewähltem `authMethod`).
|
||||||
|
|
||||||
|
**Migrationsbedarf beachten:** eine Spalte, die vor Verschlüsselung bereits Klartext trug, braucht einen einmaligen Nachzieh-Backfill wie in `ldap-config.service.ts` (`onApplicationBootstrap`, Regex `ENCRYPTED_VALUE_SHAPE` unterscheidet verschlüsselt/Klartext) `[VERIFIED: apps/api/src/ldap/ldap-config.service.ts:39, 66-101]` — für Proxmox als **neues** Feature ab Tag 1 irrelevant (keine Altdaten), nur als Muster relevant, falls später ein Feld umbenannt/neu verschlüsselt wird.
|
||||||
|
|
||||||
|
### Hintergrundabfrage je Mandant
|
||||||
|
|
||||||
|
**Zwei bestehende Muster, keins davon 1:1 übertragbar — kombinieren:**
|
||||||
|
|
||||||
|
`DkvSchedulerService` zeigt das **Mandanten-Fan-out**: EIN Cron-Auftrag *je aktivem Mandant*, Registry-Name `dkv-inbox-poll:<tenantId>`, damit ein zweiter Mandant den ersten nicht verdrängt (behobener Fehler WINDOWS #21) `[VERIFIED: apps/api/src/dkv/dkv-scheduler.service.ts:16-46]`. Proxmox-Server sind aber (anders als DKV) potenziell **mehrere pro Mandant** — der Cron-Tick eines Mandanten muss also intern über dessen `ProxmoxServer`-Zeilen iterieren, nicht 1:1 wie bei DKV (1 Config = 1 Mandant).
|
||||||
|
|
||||||
|
`DkvSchedulerService` hängt aber an `OnModuleInit`, nicht `OnApplicationBootstrap` `[VERIFIED: apps/api/src/dkv/dkv-scheduler.service.ts:1, "implements OnModuleInit"]` — **das ist NICHT das empfohlene Muster für einen neuen Dienst**. `TenderSchedulerService` erklärt im Kopfkommentar explizit, warum `OnApplicationBootstrap` die richtige Wahl ist:
|
||||||
|
> „`onModuleInit` hooks run in an unspecified order relative to one another, so on a FRESH database the scheduler could read the config before it is seeded → see it absent/inactive → never register the ... cron ... → the platform ingests NOTHING until a second restart. `onApplicationBootstrap` runs after EVERY module's `onModuleInit`, so the seed is guaranteed complete before this reads." `[VERIFIED: apps/api/src/tenders/tender-scheduler.service.ts:29-38]`
|
||||||
|
|
||||||
|
Dasselbe Risiko gilt für Proxmox: die `Module`-Seed-Zeile (Modulregistrierung) entsteht in `onModuleInit` des Proxmox-Moduls selbst; ein Scheduler, der beim Start die aktiven `ProxmoxServer`-Zeilen lädt, sollte dieses Risiko nicht eingehen, auch wenn hier keine Modul-Seed-Abhängigkeit vorliegt wie bei Tender — sicherer Standard ist trotzdem `OnApplicationBootstrap`, nicht das (mit einer dokumentierten, hier nicht zutreffenden Ausnahme begründete) `OnModuleInit` von DKV. Auch das nutzerseitige Erlebnis „frische Installation, erster Proxmox-Server angelegt, kein Neustart nötig" verlangt denselben `setInterval()`-Nachzieh-Aufruf wie bei DKV/Tender nach jedem Speichern in der Verwaltungsroute — nicht nur beim Boot.
|
||||||
|
|
||||||
|
`Tender-Cron Bootstrap`-Erfahrung aus dem Projektgedächtnis bestätigt das Risiko real: „frische Prod-DB ohne Fix ingestiert nichts" — genau das Szenario, das `OnApplicationBootstrap` verhindert.
|
||||||
|
|
||||||
|
### Modul-Registrierung
|
||||||
|
|
||||||
|
Vollständiges Muster in `docs/anleitung-entwicklung.md`, Abschnitt „So entsteht ein neues Modul", am Beispiel Domaincheck — sechs Backend-Dateien, sechs Frontend-Dateien, siehe Code-Beispiele unten. Zusätzlich als Dashboard-Kachel: `WIDGET_TYPES`/`WIDGET_MODULE_SLUGS` in `packages/shared/src/index.ts` (aktuell leer, `[VERIFIED: packages/shared/src/index.ts:97-121]`) — Proxmox wäre die **erste** Kachel, die `WIDGET_MODULE_SLUGS['proxmox'] = 'proxmox'` tatsächlich befüllt.
|
||||||
|
|
||||||
|
### Mandantentrennung
|
||||||
|
|
||||||
|
`ProxmoxServer` braucht eine eigene `tenantId`-Spalte (mehrere Server je Mandant, klar `muss-mandantengebunden`, analog `CalendarSource`) — RLS-Migration mit `ENABLE ROW LEVEL SECURITY` + `CREATE POLICY` ist **Pflicht**, sonst schlägt `rls-coverage.spec.ts` Test 1 fehl (jedes Modell mit `tenantId` muss RLS haben) `[VERIFIED: apps/api/src/prisma/rls-coverage.spec.ts:102-106]`. Jeder Service-Zugriff muss über `forTenant(this.prisma, tenantId)` laufen (Konvention: lokale Konstante `const tenantPrisma = forTenant(...)`, keine andere Form), sonst schlägt `rls-access-inventory.spec.ts` fehl — UND jede (Datei, Modell)-Fundstelle muss in `docs/mandantentrennung-zugriffsklassifikation.md` als Tabellenzeile eingetragen werden, sonst schlägt derselbe Test ebenfalls fehl (`[VERIFIED: apps/api/src/prisma/rls-access-inventory.spec.ts:718-723]`, Test „jede im Quelltext gefundene (Datei, Modell)-Fundstelle ist im Dokument eingetragen"). Der Scheduler-Startpfad (liest ALLE Mandanten vor dem ersten `forTenant()`-Aufruf) braucht denselben `forSystem()`-Systemkontext wie `DkvSchedulerService`/`TenderSchedulerService` — und muss in `FORSYSTEM_ALLOWED_CALL_SITES` in `rls-access-inventory.spec.ts` eingetragen werden `[VERIFIED: apps/api/src/prisma/rls-access-inventory.spec.ts:169-175]`, sonst schlägt der Wachhund-Test „ein Anfrageweg darf den Systemkontext nie rufen" fehl.
|
||||||
|
|
||||||
|
**Diese drei Testdateien sind harte Gates, keine Empfehlung** — ein Plan, der `ProxmoxServer`/`ProxmoxSnapshot` einführt, MUSS die Migration, die Klassifikationstabelle UND die Erlaubnisliste in derselben Aufgabe pflegen, sonst ist `pnpm --filter @tessera/api test` rot.
|
||||||
|
|
||||||
|
### Zwischenlagerung vs. Live-Abfrage
|
||||||
|
|
||||||
|
Siehe Summary — DKV (`DkvInvoiceHistory` `[VERIFIED: apps/api/prisma/schema.prisma:384-397]`) und Tender (`Tender` `[VERIFIED: apps/api/prisma/schema.prisma:435-480]`) schreiben beide Hintergrund-Polling-Resultate in eine eigene Tabelle; keine Seite in diesem Projekt holt Fremddaten live beim Rendern. Für Proxmox: ein `ProxmoxServerStatus`-Modell (1:1 oder 1:n je `ProxmoxServer`, mit `lastPolledAt`, `lastError`, und je nach Servertyp unterschiedlichen JSONB-Feldern für die Messwerte — PVE-Knoten/VM-Liste, PBS-Datastore-Liste, PMG-Tageszahlen) wird vom Scheduler beschrieben, Widget und Modulseite lesen ausschließlich daraus. Ein „Jetzt aktualisieren"-Knopf auf der Modulseite kann optional einen sofortigen Einzel-Poll auslösen (Vorbild: `DkvController` ruft nach Config-Speicherung `schedulerService.setInterval()` — derselbe Sofort-Trigger-Gedanke), sollte aber NICHT das Dashboard-Widget selbst live abfragen lassen.
|
||||||
|
|
||||||
|
## 4. Fallstricke
|
||||||
|
|
||||||
|
**Antwortgröße bei vielen VMs:** `cluster/resources` liefert bei einem größeren Cluster (zweistellige VM-Zahl je Knoten) potenziell hunderte Zeilen in einer JSON-Antwort — für die Zwischenlagertabelle unproblematisch (einmal je Poll-Intervall), aber falls die Modulseite später live filtert/sortiert, sollte serverseitig nicht bei jedem Klick neu gegen Proxmox gefragt werden, sondern gegen den Cache.
|
||||||
|
|
||||||
|
**`/rrddata` für die erste Fassung: NEIN.** RRD-Zeitreihen (Verlaufsgraphen über Zeit) sind ein separates, aufwändigeres API-Segment (mehrere Zeitraster: hour/day/week/month/year, je Objekt ein eigener Aufruf) und für eine reine Beobachtungs-Übersicht („Zustand jetzt") nicht nötig — erst relevant, wenn später Verlaufsgraphen gewünscht werden.
|
||||||
|
|
||||||
|
**Zähler sind Bytes/Ereignisse seit Start, nicht Bytes/Sekunde:** `netin`/`netout`/`diskread`/`diskwrite` in `cluster/resources` sind als COUNTER-Datenquellen definiert — kumulative Werte seit VM-Start, keine Rate `[CITED: mehrere Foren-Quellen, RRD-Datenquellen-Liste]`. Ein UI, das „aktueller Netzwerkdurchsatz" anzeigen will, muss selbst zwei aufeinanderfolgende Messungen differenzieren (Δ Wert / Δ Zeit) — eine einzelne Momentaufnahme zeigt nur „seit wann läuft die VM, wie viel kam insgesamt rein", was für eine erste Fassung ohnehin ausreicht, aber in der UI klar beschriftet werden sollte („gesamt seit Start", nicht „aktuell").
|
||||||
|
|
||||||
|
**PMG-API-Token-Lücke ist ein echtes Bau-Risiko:** wenn der Admin für einen PMG-Server versehentlich „API-Token" wählt (falls die UI das nicht verhindert), scheitert jede Anfrage mit einer für den Nutzer unverständlichen Fehlermeldung. Muss in der Einstellungs-UI hart verhindert werden (Auswahl abhängig vom Servertyp), nicht nur dokumentiert.
|
||||||
|
|
||||||
|
**Node 24 + `undici`-Dispatcher — dieselbe Falle wie in `icon-discovery.service.ts` dokumentiert:** wer aus Gewohnheit `fetch(...)` (globales, natives Fetch) statt `import { fetch as undiciFetch } from 'undici'` verwendet, bekommt bei einem `Agent`-Dispatcher **keinen Fehler beim Kompilieren**, sondern eine zur Laufzeit ignorierte Option — das selbstsignierte Zertifikat eines Proxmox-Testservers wird dann trotz `tlsRejectUnauthorized: false` weiterhin abgelehnt, was beim ersten Test verwirrend aussieht, als sei die Datenbank-Einstellung falsch gelesen worden.
|
||||||
|
|
||||||
|
**CSRF-Falle vermieden, nicht vergessen:** weil dieses Modul nur liest, entfällt CSRF komplett (siehe Block 1) — ein künftiger Ausbau mit Schreibzugriffen (nicht Teil dieses Auftrags) müsste CSRF bei Ticket-Auth nachrüsten; das jetzt schon vorzusehen wäre verfrühte Komplexität.
|
||||||
|
|
||||||
|
**Ticket-Lebensdauer 2 h bei Cron-Intervallen < 2 h kein Problem, aber Neu-Login-Logik nicht vergessen:** bei Benutzer/Passwort-Zugang muss der Scheduler bei 401 einmal automatisch neu einloggen (neues Ticket holen) und den Poll wiederholen, bevor er den Server als „nicht erreichbar" markiert — sonst erzeugt ein normaler Ticket-Ablauf alle zwei Stunden einen falschen Fehlalarm.
|
||||||
|
|
||||||
|
## Standard Stack
|
||||||
|
|
||||||
|
Keine neuen npm-Pakete. Alles Nötige ist bereits installiert:
|
||||||
|
|
||||||
|
| Baustein | Bereits vorhanden | Verwendung für Proxmox |
|
||||||
|
|---|---|---|
|
||||||
|
| `undici` 7.28.0 | `[VERIFIED: apps/api/package.json:52]` | `undiciFetch` mit bedingtem Dispatcher, Vorbild `icon-discovery.service.ts` |
|
||||||
|
| `@nestjs/schedule` (Cron) | bereits Basis von `DkvSchedulerService`/`TenderSchedulerService` | `ProxmoxSchedulerService` |
|
||||||
|
| `class-validator`/`class-transformer` | bereits DTO-Standard im Projekt (`CheckDomainDto`, `CreateLdapConfigDto`, ...) | DTOs für Server-Anlegen/-Bearbeiten |
|
||||||
|
| `CryptoService` (projekteigen) | `apps/api/src/crypto/crypto.service.ts` | Token-Secret/Passwort-Verschlüsselung |
|
||||||
|
| Prisma 6.19.3 | bereits ORM-Standard | `ProxmoxServer`/`ProxmoxServerStatus`-Modelle |
|
||||||
|
|
||||||
|
## Package Legitimacy Audit
|
||||||
|
|
||||||
|
Nicht anwendbar — dieser Auftrag installiert keine externen Pakete (weder npm noch sonst). Die Recherche bestätigt ausdrücklich, dass `undici`/natives `fetch` für alle benötigten HTTP-Aufrufe genügen; keine Proxmox-Client-Bibliothek wird eingeführt, wie vom Auftrag verlangt.
|
||||||
|
|
||||||
|
## Don't Hand-Roll
|
||||||
|
|
||||||
|
| Problem | Nicht selbst bauen | Stattdessen | Warum |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Verschlüsselung von Token-Secret/Passwort | eigenes Crypto-Schema | `CryptoService` (bestehend) | Einzige Verschlüsselungsschicht im Projekt, bereits geprüft (T-05-10), Schlüsselverwaltung über `TESSERA_ENCRYPTION_KEY` schon gelöst |
|
||||||
|
| Selbstsigniertes Zertifikat tolerieren | eigener HTTPS-Agent/eigene TLS-Logik | `undici.Agent({ connect: { rejectUnauthorized } })`, bedingt pro Server | Bereits einmal im Projekt gemessen (icon-discovery), inkl. der Node-24-Falle |
|
||||||
|
| Cron-Auftrag je Mandant | eigener Intervall-Mechanismus (`setInterval` global) | `SchedulerRegistry.addCronJob()` (DKV/Tender-Muster) | Bereits zweimal im Projekt gelöst, inkl. der Verdrängungs-Falle (WINDOWS #21) |
|
||||||
|
|
||||||
|
## Code Examples
|
||||||
|
|
||||||
|
### API-Token-Aufruf mit bedingtem TLS-Bypass (PVE)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Muster: apps/api/src/favorites/icon-discovery.service.ts (Dispatcher-Mechanik)
|
||||||
|
// + apps/api/src/ldap/ldap.service.ts:168 (bedingtes tlsRejectUnauthorized)
|
||||||
|
import { Agent, fetch as undiciFetch } from 'undici';
|
||||||
|
|
||||||
|
async function fetchPveResources(server: {
|
||||||
|
baseUrl: string; // z.B. https://pve.example.internal:8006
|
||||||
|
tokenId: string; // user@realm!tokenname
|
||||||
|
tokenSecret: string; // entschluesselt, nur im Speicher
|
||||||
|
tlsRejectUnauthorized: boolean;
|
||||||
|
}) {
|
||||||
|
const dispatcher = server.tlsRejectUnauthorized
|
||||||
|
? undefined // Standardpfad: echte Zertifikatspruefung, kein Sonderfall
|
||||||
|
: new Agent({ connect: { rejectUnauthorized: false } }); // NUR fuer diesen einen Server
|
||||||
|
|
||||||
|
const response = await undiciFetch(
|
||||||
|
`${server.baseUrl}/api2/json/cluster/resources`,
|
||||||
|
{
|
||||||
|
dispatcher,
|
||||||
|
headers: {
|
||||||
|
Authorization: `PVEAPIToken=${server.tokenId}=${server.tokenSecret}`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`PVE-Antwort ${response.status}`); // 401 = Zugang/Ticket ungueltig
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.json(); // { data: [...] } — type vm|node|storage gemischt
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Modul-Registrierung (Vorlage Domaincheck)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// apps/api/src/domaincheck/domaincheck.seed.ts — VERIFIED, so gelesen
|
||||||
|
export async function seedDomaincheckModule(
|
||||||
|
moduleRegistryService: ModuleRegistryService,
|
||||||
|
): Promise<void> {
|
||||||
|
await moduleRegistryService.seedModule({
|
||||||
|
slug: 'domaincheck',
|
||||||
|
name: 'Domaincheck',
|
||||||
|
version: '1.0.0',
|
||||||
|
category: 'domain-tools',
|
||||||
|
description: { de: '...', en: '...' },
|
||||||
|
isSystem: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Für Proxmox: `slug: 'proxmox'`, eigene `category` (z.B. `'infrastructure'`), Controller mit `@Controller('modules/proxmox')` + `@UseModule('proxmox')` auf Klassenebene — exaktes Muster in `apps/api/src/domaincheck/domaincheck.controller.ts:1-8` `[VERIFIED]`.
|
||||||
|
|
||||||
|
### Scheduler-Kombination (DKV-Mandanten-Fan-out + Tender-Bootstrap-Timing)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Kombiniert: apps/api/src/dkv/dkv-scheduler.service.ts (Mandanten-Fan-out)
|
||||||
|
// + apps/api/src/tenders/tender-scheduler.service.ts (OnApplicationBootstrap)
|
||||||
|
@Injectable()
|
||||||
|
export class ProxmoxSchedulerService implements OnApplicationBootstrap {
|
||||||
|
// NICHT OnModuleInit — siehe tender-scheduler.service.ts Kopfkommentar:
|
||||||
|
// onModuleInit-Reihenfolge zwischen Modulen ist nicht garantiert.
|
||||||
|
async onApplicationBootstrap(): Promise<void> {
|
||||||
|
const systemPrisma = forSystem(this.prisma); // alle Mandanten sehen, vor Mandantenkontext
|
||||||
|
const servers = await systemPrisma.proxmoxServer.findMany({ where: { isActive: true } });
|
||||||
|
const byTenant = groupBy(servers, (s) => s.tenantId);
|
||||||
|
for (const [tenantId, tenantServers] of byTenant) {
|
||||||
|
this.setInterval(tenantId, tenantServers); // ein Cron-Auftrag je Mandant, wie DKV
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Assumptions Log
|
||||||
|
|
||||||
|
| # | Claim | Abschnitt | Risiko falls falsch |
|
||||||
|
|---|---|---|---|
|
||||||
|
| A1 | PMG unterstützt keine API-Token, nur Ticket-Login (Forenbeleg, keine Primärquelle mit explizitem Gegenteil-Zitat) | Block 1, Anmeldung — API-Token | Falls doch unterstützt: UI verbietet unnötig eine gültige Option. Falls nicht: ohne diese Prüfung entsteht ein PMG-Zugang, der nie funktioniert |
|
||||||
|
| A2 | PBS/PMG-Ticket-Cookie heißt `PBSAuthCookie`/`PMGAuthCookie` (analog PVE) | Block 1, Anmeldung — Ticket | Falsche Cookie-Bezeichnung -> jede Ticket-Anfrage schlägt mit 401 fehl, obwohl Zugang korrekt ist |
|
||||||
|
| A3 | Exakte Feldnamen der PBS-Snapshot-Liste (`verify-state`, `backup-time`, `size`) | Block 1, PBS | Falsche Feldnamen -> `undefined`-Werte in der UI statt eines klaren Fehlers, bis manuell gegen den API-Viewer geprüft |
|
||||||
|
| A4 | Proxmox verwendet 401 breiter als übliche REST-Konvention (auch für Berechtigungsfehler, nicht nur Authentifizierung) | Block 1, Fehlerverhalten | Falls doch 403 vorkommt: UI zeigt „Zugang abgelehnt" statt einer treffenderen „Rolle reicht nicht"-Meldung — kosmetisch, kein Blocker |
|
||||||
|
| A5 | PMG-Statistik-Endpunkt liefert keine eigene Quarantäne-Zahl unter `/statistics/mail` (separater Pfad vermutet, nicht recherchiert) | Block 1, PMG | Falls Quarantäne-Zahl doch im selben Aufruf steckt: unnötiger zweiter API-Aufruf in der ersten Fassung — kein Blocker, nur Ineffizienz |
|
||||||
|
|
||||||
|
**Empfehlung:** A1–A3 vor dem ersten Implementierungs-Task als `checkpoint:human-verify` gegen einen echten PVE-/PBS-/PMG-Testserver bestätigen (der Auftrag nennt keinen erreichbaren Testserver für diese Recherche-Session — siehe Environment Availability).
|
||||||
|
|
||||||
|
## Environment Availability
|
||||||
|
|
||||||
|
Kein für diese Recherche erreichbarer PVE-/PBS-/PMG-Server bekannt oder im Auftrag genannt — anders als beim Windows-Test-VM- oder ViCoTest-Zugang aus dem Projektgedächtnis gibt es dafür keinen dokumentierten Zugriffsweg. Die API-Formen in diesem Dokument sind ausschließlich aus Doku/Forenbelegen zusammengetragen (siehe Assumptions Log), nicht live verifiziert. Der Planer sollte den ersten Implementierungs-Task so schneiden, dass ein `checkpoint:human-verify` (Anlegen eines echten Testzugangs durch den Nutzer) vor der Feldnamen-kritischen PBS/PMG-Arbeit steht — für PVE ist die Beleglage deutlich fester (offizielle `pveum-plain.html`/Wiki-Seite bestätigen Header-Form und CSRF-Verhalten wörtlich).
|
||||||
|
|
||||||
|
| Abhängigkeit | Gebraucht für | Verfügbar (diese Recherche-Session) | Fallback |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Erreichbarer PVE-Server | Verifikation `cluster/resources`-Feldnamen, Token-Header | ✗ | Foren-/Community-Beleg, `checkpoint:human-verify` vor Bau |
|
||||||
|
| Erreichbarer PBS-Server | Verifikation Snapshot-/Verify-Feldnamen | ✗ | dito |
|
||||||
|
| Erreichbarer PMG-Server | Verifikation Statistik-Feldnamen, Token-Unterstützung | ✗ | dito, höchste Priorität wegen A1 |
|
||||||
|
|
||||||
|
## Validation Architecture
|
||||||
|
|
||||||
|
### Test Framework
|
||||||
|
| Property | Value |
|
||||||
|
|---|---|
|
||||||
|
| Framework | Vitest 3.2.6 (`apps/api`, `environment: 'node'`) `[VERIFIED: docs/anleitung-entwicklung.md, Abschnitt "Tests"]` |
|
||||||
|
| Config file | `apps/api/vitest.config.ts` |
|
||||||
|
| Quick run command | `pnpm --filter @tessera/api test` |
|
||||||
|
| Full suite command | `pnpm test` (Root, über Turborepo beide Apps) |
|
||||||
|
|
||||||
|
### Phase Requirements -> Test Map
|
||||||
|
| Behavior | Test Type | Automated Command |
|
||||||
|
|---|---|---|
|
||||||
|
| Verschlüsselung/Entschlüsselung Token-Secret/Passwort | unit | `CryptoService` bereits getestet; neuer Roundtrip-Test analog `crypto.service.spec.ts` |
|
||||||
|
| RLS-Abdeckung `ProxmoxServer`/`ProxmoxServerStatus` | guard | `pnpm --filter @tessera/api exec vitest run src/prisma/rls-coverage.spec.ts` |
|
||||||
|
| Zugriffsklassifikation vollständig dokumentiert | guard | `pnpm --filter @tessera/api exec vitest run src/prisma/rls-access-inventory.spec.ts` |
|
||||||
|
| Scheduler: ein Auftrag je Mandant, kein Verdrängen | unit | analog `dkv-scheduler.service.spec.ts` |
|
||||||
|
| TLS-Bypass nur bei `tlsRejectUnauthorized === false` dieser einen Zeile | unit | neuer Test, Vorbild fehlt (icon-discovery hat keinen bedingten Pfad) — selbst schreiben |
|
||||||
|
| `@UseModule('proxmox')` blockiert ohne Freigabe | unit | analog `module.guard.spec.ts` |
|
||||||
|
| Widget verschwindet ohne Modulzugriff | unit | analog `widget-wrapper.test.tsx`/`widget-module-map.spec.ts` |
|
||||||
|
|
||||||
|
### Sampling Rate
|
||||||
|
- **Per Task Commit:** `pnpm --filter @tessera/api test`
|
||||||
|
- **Per Wave Merge:** `pnpm test` (Root)
|
||||||
|
- **Phase Gate:** volle Suite grün vor `/gsd-verify-work`
|
||||||
|
|
||||||
|
### Wave 0 Gaps
|
||||||
|
- Kein PVE/PBS/PMG-Testserver erreichbar (siehe Environment Availability) — Feldnamen-kritische Tests bleiben bis zur manuellen Verifikation mit gemockten Antworten gebaut, nicht gegen einen echten Server.
|
||||||
|
|
||||||
|
## Security Domain
|
||||||
|
|
||||||
|
### Applicable ASVS Categories (Level 1)
|
||||||
|
|
||||||
|
| ASVS Category | Applies | Standard Control |
|
||||||
|
|---|---|---|
|
||||||
|
| V2 Authentication | ja (gegenüber Proxmox, nicht gegenüber Tessera-Nutzern) | Token/Passwort serverseitig gespeichert, nie an den Browser zurückgegeben (Maskierung wie `LdapConfigService`) |
|
||||||
|
| V4 Access Control | ja | `@UseModule('proxmox')` + `ModuleAccessGate` (zweistufig, wie alle Module) |
|
||||||
|
| V5 Input Validation | ja | `class-validator`-DTOs für Server-Adresse/Zugang (URL-Form, Enum für Typ/Auth-Methode) |
|
||||||
|
| V6 Cryptography | ja | `CryptoService` (AES-256-GCM), niemals selbst hand-rollen |
|
||||||
|
| V9 Communications | ja | TLS-Bypass ist die zentrale Bedrohung dieses Moduls — siehe unten |
|
||||||
|
|
||||||
|
### Known Threat Patterns
|
||||||
|
|
||||||
|
| Pattern | STRIDE | Standard Mitigation |
|
||||||
|
|---|---|---|
|
||||||
|
| TLS-Bypass leakt Zugangsdaten an MITM | Information Disclosure | Bypass nur pro Server-Zeile, Default „prüfen", niemals global/Umgebungsvariable (siehe Block 2) |
|
||||||
|
| Gespeichertes Token/Passwort im Klartext lesbar bei DB-Dump | Information Disclosure | `CryptoService`-Verschlüsselung, Schlüssel getrennt vom DB-Backup aufbewahrt (bestehende Vorgabe, `docs/anleitung-entwicklung.md`) |
|
||||||
|
| Fremdmandant liest Proxmox-Zugang eines anderen Mandanten | Elevation of Privilege | RLS auf `ProxmoxServer`/`ProxmoxServerStatus`, `forTenant()`-Bindung, Pflicht-Testabdeckung (siehe Anschlussstellen) |
|
||||||
|
| Server-Antwort mit riesigem Payload (viele hundert VMs) legt den API-Prozess lahm | Denial of Service | Nur der Scheduler ruft Proxmox live auf (begrenzte Frequenz), die Modulseite liest immer aus dem Cache — kein ungebremster Nutzer-Trigger auf die Fremd-API |
|
||||||
|
|
||||||
|
## Sources
|
||||||
|
|
||||||
|
### Primary (HIGH confidence — aus tatsächlich gelesenem Projekt-Code)
|
||||||
|
- `apps/api/src/favorites/icon-discovery.service.ts` — undici-Dispatcher-Mechanik, TLS-Bypass-Kommentar
|
||||||
|
- `apps/api/src/ldap/ldap-config.service.ts`, `apps/api/src/ldap/crypto.service.ts` — Verschlüsselung, Systemkontext-Backfill
|
||||||
|
- `apps/api/src/dkv/dkv-scheduler.service.ts`, `apps/api/src/tenders/tender-scheduler.service.ts` — Scheduler-Muster
|
||||||
|
- `apps/api/prisma/schema.prisma` — `CalendarSource`, `LdapConfig`, `Module`/`TenantModuleActivation`, `Tender`, `DkvInvoiceHistory`
|
||||||
|
- `apps/api/src/prisma/rls-coverage.spec.ts`, `apps/api/src/prisma/rls-access-inventory.spec.ts` — RLS-Gates
|
||||||
|
- `docs/mandantentrennung-zugriffsklassifikation.md` — Klassifikationspflicht
|
||||||
|
- `docs/anleitung-entwicklung.md` — Modul-/Kachel-Registrierungsmuster
|
||||||
|
- `.planning/quick/260922-m1h-dashboard-widgets-ein-modul-bringt-seine/260922-m1h-SUMMARY.md` — Drei-Stellen-Kachel-Muster
|
||||||
|
|
||||||
|
### Secondary (MEDIUM confidence — offizielle Proxmox-Doku, per WebFetch/WebSearch gelesen)
|
||||||
|
- pve.proxmox.com/pve-docs/pveum-plain.html — API-Token-Header, PVEAuditor-Rolle
|
||||||
|
- pve.proxmox.com/wiki/Proxmox_VE_API — Ticket-Endpunkt, CSRF-Verhalten
|
||||||
|
- pbs.proxmox.com/docs/user-management.html — PBSAPIToken-Header, Audit/DatastoreAudit-Rollen
|
||||||
|
|
||||||
|
### Tertiary (LOW confidence — Forenbelege, nicht in Primärdoku bestätigt)
|
||||||
|
- forum.proxmox.com (mehrere Threads) — PMG-Token-Lücke, `cluster/resources`-Feldnamen, PBS-Snapshot-Felder, PMG-Statistik-Felder, RRD-Counter-Typ
|
||||||
|
- pmg.proxmox.com/pmg-docs/pmg-admin-guide.html — Auditor-Rollenbeschreibung (aus Sekundärzitaten, nicht direkt aus dem Volltext extrahierbar — Dokument zu groß für den Abruf)
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
**Confidence breakdown:**
|
||||||
|
- PVE-Auth/CSRF/Rollen: HIGH — offizielle Doku wörtlich zitiert
|
||||||
|
- PBS-Auth/Rollen: HIGH (Auth-Header, Rollen), MEDIUM (Snapshot-Feldnamen, nur Forenbeleg)
|
||||||
|
- PMG-Auth: LOW (Token-Unterstützung nicht in Primärquelle bestätigt) — als `checkpoint:human-verify` markiert
|
||||||
|
- Bestandsmuster (Crypto/Scheduler/RLS/Modul-Registrierung): HIGH — aus gelesenem Code zitiert
|
||||||
|
|
||||||
|
**Research date:** 2026-09-23
|
||||||
|
**Valid until:** ~30 Tage für Bestandsmuster (stabil); Proxmox-API-Details sollten vor dem ersten Implementierungs-Task gegen einen echten Server nachgeprüft werden, unabhängig vom Datum (siehe Assumptions Log)
|
||||||
Reference in New Issue
Block a user