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 <noreply@anthropic.com>claude/nice-moser-521659
parent
3704620bb5
commit
87ecb4d064
|
|
@ -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
|
||||
`<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:
|
||||
|
||||
```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=<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-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
|
||||
`<subdomain>.<Zone>` (`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', '<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-`<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 B`
|
||||
bedeutet in dieser Nextcloud-Fassung tatsächlich null. Kein weiterer
|
||||
Handlungsbedarf — Ergebnis unten eintragen.
|
||||
|
||||
- **Der Upload gelingt:** `readonly` liefert **keine** Speicherbeschränkung,
|
||||
auf keinen Fall stehen lassen und hoffen. Dann folgt sofort:
|
||||
1. 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.
|
||||
2. 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.
|
||||
3. Dieselbe Änderung im englischen Pendant `lang/en/users.php` nachziehen.
|
||||
4. 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.
|
||||
|
||||
---
|
||||
|
||||
## 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* |
|
||||
Loading…
Reference in New Issue