Files
tessera-ctl/.planning/quick/260923-dhh-proxmox-modul-pve-pbs-und-pmg-anbinden-n/260923-dhh-RESEARCH.md
T

38 KiB
Raw Blame History

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)

// 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)

// 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)

// 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)