15 KiB
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:
- Kommt eine Mail wirklich an, mit echtem Absender, durch einen echten Mailserver, in ein echtes Postfach?
- Bedeutet
0 Bin 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 Rollereadonlynicht, 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:
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:
cd /opt/clupilot
sudo -u clupilot docker compose exec -T app php artisan tinker
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:
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
<uuid>: würde nachgetragen, oder eine Sammelzeile übersprungen (<grund>): <anzahl> — die möglichen Gründe sind kein Host, keine VMID und anderer Lauf aktiv (ConfigureInstanceMail::reasonToSkip()). Am Ende steht:
Probelauf: <n> Instanz(en) bekämen den Mailversand, <m> übersprungen. Nichts wurde geändert.
Woran man erkennt, dass es geklappt hat: die Zahl <n> 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:
App\Models\Instance::where('status', 'active')->first()->uuid;
Schritt 2: Eine Testinstanz nachrüsten
cd /opt/clupilot
sudo -u clupilot docker compose exec -T app php artisan clupilot:configure-instance-mail --instance=<uuid>
Ausgegeben wird <uuid>: Lauf <id> 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-relayhinterlegte Adresse (nach Plannoreply@clupilot.cloud), - ein Link zum Setzen des Passworts, der in die Nextcloud des Kunden
führt — die Adresse dieser Instanz, nach dem Muster
<subdomain>.<Zone>(ConfigureNextcloud::execute()trägt genau diesen Wert alstrusted_domains 1ein) —, 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:
cd /opt/clupilot
sudo -u clupilot docker compose exec -T app php artisan tinker
$instance = App\Models\Instance::where('uuid', '<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
/provisioningprüfen. mail_smtphoststeht 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():
$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-<select>
auf „Nur Lesen" stellen (Users::setRole() → SyncSeatToNextcloud::dispatch($uuid, 'role') → NextcloudUsers::applyRole()).
Woran man erkennt, dass es geklappt hat: Seite neu laden (auch das läuft über die Warteschlange, kein automatisches Update); die Statuszeile zeigt wieder den normalen Status, nicht „fehlgeschlagen".
Schritt 2: Als dieser Benutzer anmelden und eine Datei hochladen
Anmeldename ist nc_username (bei der ersten Einladung einmalig auf die
E-Mail-Adresse gesetzt und danach nie mehr geändert), Passwort das per Mail
gesetzte aus Nachweis A. Anmelden an der Nextcloud-Adresse dieser Instanz
(<subdomain>.<Zone>, dieselbe wie oben) und versuchen, eine beliebige Datei
hochzuladen.
Die beiden Ausgänge
-
Der Upload wird abgewiesen: die Rolle hält, was sie verspricht.
0 Bbedeutet in dieser Nextcloud-Fassung tatsächlich null. Kein weiterer Handlungsbedarf — Ergebnis unten eintragen. -
Der Upload gelingt:
readonlyliefert keine Speicherbeschränkung, auf keinen Fall stehen lassen und hoffen. Dann folgt sofort:- Den Quota-Zweig aus
NextcloudUsers::applyRole()entfernen — die Rolle behält nur noch ihre Gruppenzuordnung (group:adduser/group:removeuser), keinen Speicherplatz-Befehl mehr, weder beim Setzen noch beim Verlassen. - Den Text
users.role_readonly_means(lang/de/users.php, aktuell „Kann nichts hochladen oder anlegen. Was ihm freigegeben wird, kann er im Rahmen der Freigabe bearbeiten.") auf das kürzen, was dann noch stimmt — die Beschränkung läuft in diesem Fall ausschließlich über Freigaberechte, nicht über Speicherplatz. - Dieselbe Änderung im englischen Pendant
lang/en/users.phpnachziehen. - Die zugehörigen Tests in
tests/Feature/Nextcloud/NextcloudUsersTest.php(Aufgabe 6) an den entfernten Quota-Befehl anpassen.
Das ist dann eine eigene, kleine Folgeaufgabe — keine, die dieses Runbook selbst erledigt.
- Den Quota-Zweig aus
Ergebnis
Auszufüllen von der Person, die den jeweiligen Nachweis tatsächlich gegen echte Hardware durchgeführt hat. Ein Nachweis, den niemand aufschreibt, wird beim nächsten Zweifel erneut geführt.
Nachweis A — Einladungsmail
| Feld | Wert |
|---|---|
| Datum | steht aus |
| Nextcloud-Fassung | steht aus |
| Ausgang | steht aus |
Nachweis B — Speicherplatz bei readonly
| Feld | Wert |
|---|---|
| Datum | steht aus |
| Nextcloud-Fassung | steht aus |
| Ausgang | steht aus |
| Falls „liefert keine Beschränkung": Folgeaufgabe angelegt? | steht aus |