From 87ecb4d064b47a4334ab1c2e0b3104ca4a137681 Mon Sep 17 00:00:00 2001 From: nexxo Date: Mon, 3 Aug 2026 23:12:36 +0200 Subject: [PATCH] Runbook fuer den Nachweis gegen echte Hardware: Einladung und Speicherplatz Zwei Dinge, die sich gegen einen Fake nicht beweisen lassen. Der zweite kann den Entwurf umwerfen: ob Nextcloud "0 B" als null oder als unbegrenzt auslegt, steht in keiner Dokumentation. Beide Nachweise stehen aus, nicht simuliert: mail.clupilot.cloud existiert noch nicht. Das Runbook haelt Voraussetzungen, Ablauf und Fehlersuche fest, inklusive einer Luecke, die der Aufgabenzettel nicht kannte -- die Konsole kann heute nur ein bestehendes Postfach bearbeiten, keines mit dem Schluessel "instance-relay" neu anlegen -- und traegt ein leeres Ergebnisfeld mit dem Vermerk "steht aus" fuer beide Nachweise. Co-Authored-By: Claude Opus 5 --- docs/runbooks/mitarbeiter-nachweis.md | 341 ++++++++++++++++++++++++++ 1 file changed, 341 insertions(+) create mode 100644 docs/runbooks/mitarbeiter-nachweis.md diff --git a/docs/runbooks/mitarbeiter-nachweis.md b/docs/runbooks/mitarbeiter-nachweis.md new file mode 100644 index 0000000..645b41d --- /dev/null +++ b/docs/runbooks/mitarbeiter-nachweis.md @@ -0,0 +1,341 @@ +# Runbook: Der Nachweis gegen echte Hardware — Einladung und Speicherplatz + +Die Mitarbeiterverwaltung wurde gegen `FakeProxmoxClient` gebaut und geprüft — +zu Recht, ein Test darf keinen echten Mailserver und keine echte Kunden-VM +brauchen. Zwei Dinge lassen sich damit aber **nicht** zeigen, weil die Fälschung +genau dort aufhört, wo es zählt: + +1. **Kommt eine Mail wirklich an**, mit echtem Absender, durch einen echten + Mailserver, in ein echtes Postfach? +2. **Bedeutet `0 B` in einer echten Nextcloud auch wirklich null Byte** — oder + legt sie den Wert als „unbegrenzt" aus? Aus der Dokumentation ist das nicht + zu belegen. Fällt die zweite Frage falsch aus, hält die Rolle `readonly` + nicht, was sie verspricht, und der Entwurf muss nachgebessert werden. + +Dieses Runbook ist die Anleitung für beide Nachweise. Es führt sie **nicht +selbst durch**. + +--- + +## Der Stand heute: beide Nachweise stehen aus + +Der Mailserver `mail.clupilot.cloud` existiert noch nicht — der Betreiber +setzt ihn gerade erst auf. Ohne ihn kommt keine Mail heraus, und ohne Mail +lässt sich weder die Einladung (Nachweis A) noch, in der Folge, der +Speicherplatz-Test (Nachweis B; er braucht den Zugang, den die Einladung erst +verschafft) überhaupt beginnen. + +**Nichts hier wurde simulieren oder behauptet.** Das Ergebnisfeld am Ende +dieses Dokuments trägt für beide Nachweise den Vermerk „steht aus" — so lange, +bis jemand die Schritte unten tatsächlich gegen echte Hardware durchführt und +das Feld ausfüllt. + +--- + +## Voraussetzungen, in dieser Reihenfolge + +Jede Stufe setzt die davor voraus. Wer weiter unten anfängt, jagt einem +Fehler hinterher, der eigentlich weiter oben sitzt. + +### 1. Der Mailserver steht + +`mail.clupilot.cloud` muss laufen und von außen erreichbar sein (mailcow +oder was auch immer der Betreiber dafür einsetzt). Ohne diese Stufe sind alle +folgenden reine Vorbereitung ohne Wirkung. + +### 2. Das Postfach `instance-relay` ist angelegt, und der Versandtest darin ist grün + +`App\Services\Mail\GuestMailConfig` sucht das Versandkonto für jede +Kundeninstanz über genau diesen Schlüssel: + +```php +public const RELAY_KEY = 'instance-relay'; +``` + +**Wichtige Lücke, die dieser Schritt nicht von selbst schließt:** Die Konsole +(`/mail`, Reiter „Postfächer") kann heute nur ein **bestehendes** Postfach +bearbeiten (`app/Livewire/EditMailbox.php` lädt per `firstOrFail()`, es gibt +keinen „Neu anlegen"-Knopf). Die Migration, die die Tabelle befüllt +(`database/migrations/2026_07_28_100000_seed_mailboxes_from_environment.php`), +erzeugt ausschließlich die fünf Zeilen `no-reply`, `support`, `billing`, +`office`, `info` — **nicht** `instance-relay`. Der Datensatz mit diesem +Schlüssel existiert also nirgends von selbst und muss zuerst geschaffen +werden, bevor die Konsole ihn anzeigen kann, zum Beispiel per Tinker auf dem +CluPilot-Server: + +```bash +cd /opt/clupilot +sudo -u clupilot docker compose exec -T app php artisan tinker +``` + +```php +App\Models\Mailbox::create([ + 'key' => 'instance-relay', + 'address' => 'noreply@clupilot.cloud', + 'active' => true, + 'authenticates' => true, // oder false, wenn das Relay per IP vertraut +]); +``` + +Danach in der Konsole unter `/mail` → Reiter „Postfächer": Zeile +`instance-relay` über „Bearbeiten" mit Zugangsdaten (Adresse, Benutzername, +Passwort) vervollständigen, eine Testempfänger-Adresse eintragen und +**Testmail senden** klicken. + +**Woran man erkennt, dass es geklappt hat:** die grüne Meldung „`instance-relay`: +Zugestellt. Das Postfach kann verschicken." erscheint über der Tabelle +(`App\Services\Mail\MailboxTester::run()`), und die Spalte „Zuletzt bestätigt" +zeigt ab jetzt ein Datum statt „Noch nicht bestätigt". + +### 3. `Settings` trägt `mail.host`, `mail.port`, `mail.encryption` + +Konsole → `/mail` → Reiter „Versand". Server, Port und Verschlüsselung +eintragen, speichern (Passwortbestätigung nötig, `Admin\Mail::saveServer()` +verlangt `passwordRecentlyConfirmed()`). Diese drei Werte gelten für **jede** +Instanz gemeinsam — `GuestMailConfig::for()` liest sie aus `app_settings`, +nicht aus dem Postfach. + +**Woran man erkennt, dass es geklappt hat:** das Formular zeigt die +eingetragenen Werte nach einem Neuladen der Seite unverändert an; ein leerer +`mail.host` oder ein `mail.port` unter 1 lässt `GuestMailConfig::for()` sonst +sofort mit `no_server` abbrechen, ohne dass irgendetwas geschrieben wird. + +### 4. Port 587 des Mailservers lässt die öffentliche Adresse des Proxmox-Hosts zu + +**Das ist der häufigste Grund, aus dem Nachweis A scheitert**, und er wird +leicht übersehen, weil er nicht CluPilot betrifft. Die Kunden-VM verschickt +ihre Mail direkt vom Proxmox-Host aus ins Internet — nicht über den +WireGuard-Tunnel, der die Verwaltung trägt (`ConfigureNetwork` erlaubt der +Gast-Firewall nur eingehend 80/443; für ausgehenden Mailversand gilt die +Netzanbindung des Hosts). Die Zulassungsliste des Mailservers muss also die +**öffentliche IP-Adresse jedes Proxmox-Hosts** enthalten, der Kunden hostet — +nicht die Tunneladresse und nicht die Adresse des CluPilot-Servers. + +--- + +## Der Nachrüstlauf + +Der Mailversand kam erst mit der Mitarbeiterverwaltung in die Pipeline. Jede +vorher gebaute Instanz kennt bis heute keinen Mailserver — dieser Lauf trägt +ihn nach. + +### Schritt 1: Probelauf + +Auf dem CluPilot-Server, im App-Container: + +```bash +cd /opt/clupilot +sudo -u clupilot docker compose exec -T app php artisan clupilot:configure-instance-mail --dry-run +``` + +Ausgegeben wird für jede aktive Instanz mit Host und VMID entweder eine Zeile +`: würde nachgetragen`, oder eine Sammelzeile `übersprungen (): +` — die möglichen Gründe sind `kein Host`, `keine VMID` und `anderer +Lauf aktiv` (`ConfigureInstanceMail::reasonToSkip()`). Am Ende steht: + +``` +Probelauf: Instanz(en) bekämen den Mailversand, übersprungen. Nichts wurde geändert. +``` + +**Woran man erkennt, dass es geklappt hat:** die Zahl `` ist größer als +null, und **nichts** in der Konsole unter `/provisioning` hat sich bewegt — +`--dry-run` schreibt nichts. + +Der Befehl berücksichtigt **nur Instanzen mit `status = active`**. Für eine +gezielte Testinstanz im nächsten Schritt muss die UUID also zu einer aktiven +Instanz gehören — sonst liefert der Lauf `0 Lauf/Läufe gestartet, 0 +übersprungen`, weil die Instanz die Abfrage gar nicht erst trifft. Eine UUID +lässt sich, da es dafür keine sichtbare Konsolenseite gibt, per Tinker +nachschlagen: + +```php +App\Models\Instance::where('status', 'active')->first()->uuid; +``` + +### Schritt 2: Eine Testinstanz nachrüsten + +```bash +cd /opt/clupilot +sudo -u clupilot docker compose exec -T app php artisan clupilot:configure-instance-mail --instance= +``` + +Ausgegeben wird `: Lauf gestartet`, danach `1 Lauf/Läufe gestartet, +0 übersprungen.` Der Befehl legt einen `ProvisioningRun` mit +`pipeline = 'instance-mail'` an und stößt ihn über `AdvanceRunJob` an; der +eigentliche Schritt ist `App\Provisioning\Steps\Customer\ConfigureInstanceMail`, +registriert unter `instance-mail` in `config/provisioning.php`. + +**Woran man erkennt, dass es geklappt hat:** in der Konsole unter +`/provisioning` erscheint eine Zeile mit dem Kundennamen und, in Monospace +daneben, `instance-mail`. Der Status-Punkt steht zunächst auf „Läuft" +(blau/info) und wechselt — der Schritt hat 120 Sekunden Zeit +(`ConfigureInstanceMail::maxDuration()`) — auf „Fertig" in Grün. Bleibt er auf +„Fehlgeschlagen" (rot) stehen, zeigt der Lauf-Detailbereich rechts die +Fehlermeldung; die Instanz bleibt dabei voll nutzbar, dieser Schritt lässt eine +bezahlte Bereitstellung nie scheitern. + +--- + +## Nachweis A — kommt die Einladung an? + +### Schritt 1: Mitarbeiter anlegen und einladen + +Im Kunden-Portal der nachgerüsteten Testinstanz, Seite „Benutzer" (`/users`): +im Formular oben E-Mail-Adresse (eine, die man wirklich abrufen kann) und Rolle +eintragen, **„Hinzufügen"** klicken — das legt nur die Zeile an, verschickt +noch nichts (`Users::addSeat()`). Anschließend in der Tabellenzeile auf +**„Einladen"** klicken (`Users::sendInvite()`). + +**Woran man erkennt, dass der Klick angekommen ist:** die Statuszeile wechselt +sofort auf „wird eingerichtet …". Das läuft über die Warteschlange +(`SyncSeatToNextcloud` auf der Queue `provisioning`, Worker-Container +`queue-provisioning`) — der Browser wartet nicht, und die Seite pollt nicht +von selbst. Nach ein paar Sekunden die Seite neu laden: der Status wechselt +entweder auf den normalen Status („Eingeladen") — dann hat `NextcloudUsers:: +invite()` im Gast erfolgreich `user:add --generate-password` (neuer Nutzer) +oder `user:welcome --reset-password` (bestehender Nutzer) ausgeführt — oder +auf „fehlgeschlagen" mit Klartextgrund und einem „Nochmal versuchen"-Knopf. +Erst wenn hier **nicht** „fehlgeschlagen" steht, lohnt der Blick ins Postfach. + +### Schritt 2: Die Mail prüfen + +**Erwartet:** + +- eine Mail an die eingetragene Adresse, +- Absender: die für `instance-relay` hinterlegte Adresse (nach Plan + `noreply@clupilot.cloud`), +- ein Link zum Setzen des Passworts, der in **die Nextcloud des Kunden** + führt — die Adresse dieser Instanz, nach dem Muster + `.` (`ConfigureNextcloud::execute()` trägt genau diesen + Wert als `trusted_domains 1` ein) —, **nicht** ins CluPilot-Portal. + +Diesen letzten Punkt tatsächlich anklicken oder die Ziel-URL genau lesen: sie +muss ausdrücklich auf den Hostnamen der Kundeninstanz zeigen, nicht auf den +Portal- oder Konsolen-Host. + +### Wenn keine Mail kommt + +In dieser Reihenfolge, jeder Schritt schließt eine Möglichkeit aus. Ein +direkter SSH-Zugang zur Kunden-VM existiert im Produkt bewusst nicht — die +Gast-Firewall lässt eingehend nur 80/443 zu, Verwaltung läuft ausschließlich +über den Proxmox-Gastagenten (`ProxmoxClient::guestExec()`). Der einzige +verdrahtete manuelle Weg dorthin führt über Tinker auf dem CluPilot-Server: + +```bash +cd /opt/clupilot +sudo -u clupilot docker compose exec -T app php artisan tinker +``` + +```php +$instance = App\Models\Instance::where('uuid', '')->first(); +$pve = app(App\Services\Proxmox\ProxmoxClient::class)->forHost($instance->host); +$pve->guestExec( + $instance->host->node, + (int) $instance->vmid, + App\Support\NextcloudOcc::command('config:system:get mail_smtphost') +); +``` + +Das führt im Gast exakt `cd /opt/nextcloud && docker compose exec -T -u +www-data app php occ config:system:get mail_smtphost` aus +(`NextcloudOcc::command()`). + +- **Leere Ausgabe oder Fehler:** der Nachrüstlauf (oben) ist entweder nicht + gelaufen oder auf dieser Instanz fehlgeschlagen — zurück zu Schritt 2 des + Nachrüstlaufs, Lauf-Detailbereich in `/provisioning` prüfen. +- **`mail_smtphost` steht korrekt:** die Nextcloud selbst ist richtig + konfiguriert, das Problem liegt beim Versand. Ins mailcow-Protokoll (auf dem + Mailserver) schauen, ob die Verbindung von der Proxmox-Host-IP überhaupt + ankam. +- **Nichts kommt im mailcow-Protokoll an:** **Portsperre — der häufigste + Grund.** Siehe Voraussetzung 4: Port 587 muss die öffentliche Adresse des + jeweiligen Proxmox-Hosts zulassen. Das ist der erste Verdacht, nicht der + letzte. + +--- + +## Nachweis B — bedeutet `0 B` wirklich null? + +**Das ist der Nachweis, der den Entwurf umwerfen kann.** Für einen Sitz mit der +Rolle `readonly` setzt `NextcloudUsers::applyRole()`: + +```php +$befehle[] = $seat->isReadonly() + ? 'user:setting '.escapeshellarg($user).' files quota '.escapeshellarg('0 B') + : 'user:setting '.escapeshellarg($user).' files quota --delete'; +``` + +Aus der Nextcloud-Dokumentation ist nicht zu belegen, ob eine Instanz `0 B` +als „null Bytes" oder als „unbegrenzter Speicher" auslegt. Das lässt sich nur +an einer echten Installation prüfen — nicht am Code hier, der lediglich den +Wert setzt, und nicht an der Dokumentation, die die Frage offen lässt. + +### Schritt 1: Einen Sitz auf `readonly` stellen + +Am besten denselben Testsitz aus Nachweis A weiterverwenden — er muss bereits +eingeladen und synchronisiert sein (`nc_synced_at` gesetzt), sonst löst die +Rollenänderung gar keinen Nextcloud-Sync aus (`Users::queueSync()` bricht sonst +ab, ohne einen Job zu verschicken). In der Tabellenzeile das Rollen-`