diff --git a/docs/superpowers/specs/2026-07-30-host-uebernahme-statt-installation-design.md b/docs/superpowers/specs/2026-07-30-host-uebernahme-statt-installation-design.md index 9760ac6..2ee3930 100644 --- a/docs/superpowers/specs/2026-07-30-host-uebernahme-statt-installation-design.md +++ b/docs/superpowers/specs/2026-07-30-host-uebernahme-statt-installation-design.md @@ -91,37 +91,48 @@ Zwei Stellen, an denen der Betreiber etwas tut. Kein Wert wird abgetippt. --- -## 5. Der Einmal-Code und der eine öffentliche Endpunkt +## 5. Kein öffentlicher Endpunkt — die Befehlszeile trägt alles ### Das Henne-Ei-Problem -Alles soll im Tunnel passieren — aber um dem Tunnel beizutreten, braucht das Skript -die WireGuard-Angaben. Etwas muss einmal von außen kommen. +Alles soll im Tunnel passieren, aber um dem Tunnel beizutreten, braucht das Skript +die WireGuard-Angaben. Ein erster Entwurf löste das mit einem öffentlichen +`POST /enrol`. **Das war unnötig**, und der Betreiber hat es zu Recht bestritten. -### Die Auflösung +Die drei Werte, die das Skript vor dem Tunnel braucht — Hub-Schlüssel, +Hub-Adresse, zugeteilte Tunnel-IP — kennt die Konsole beim Anlegen des Hosts alle. +Sie kann sie **in die kopierte Befehlszeile schreiben**. Es gibt nichts abzurufen. -Genau **ein** öffentlicher Endpunkt, `POST /enrol`, der ohne gültigen Code nichts -preisgibt. +### Der eine Haken, und wie er aufgelöst wird -- Beim Anlegen des Hosts entsteht ein Code: 32 Zeichen aus `random_bytes`, in der - Datenbank **nur als Hash** (wie ein Passwort), mit `expires_at` **60 Minuten** und - `used_at`. -- Das Skript ruft `POST /enrol` mit dem Code. Antwort bei Erfolg: WireGuard-Subnetz, - Hub-Pubkey, Hub-Endpunkt, die dem Host zugeteilte Tunnel-IP, und ein - **Fortschritts-Token** für die Rückmeldungen aus §7. -- Der Code ist danach verbraucht (`used_at`), auch bei Abbruch. Ein zweiter Versuch - braucht einen neuen Code aus der Konsole. Ein Skriptlauf, der stirbt, wird nicht - „nochmal probiert" — er wird neu angestoßen, weil die halb installierte Maschine - ohnehin neu aufgesetzt gehört. -- **Ratenbegrenzung** und immer dieselbe Antwortzeit bei falschem Code: der - Endpunkt darf nicht verraten, ob ein Code existiert. -- Antwortet **nicht**, wenn der Host in der Konsole schon `active` ist. +Damit der Hub den Host hereinlässt, muss er dessen **öffentlichen** Schlüssel +kennen. Erzeugte der Host ihn selbst — wie `ConfigureWireguard` es heute tut —, +müsste er ihn melden, **bevor** der Tunnel steht. Genau dafür wäre ein öffentlicher +Endpunkt nötig gewesen. -### Was der Endpunkt NICHT tut +Also erzeugt **CluPilot** das Schlüsselpaar beim Anlegen des Hosts und gibt es mit. +Der Hub kennt den Peer damit von Anfang an, und es gibt keinen Rückkanal vor dem +Tunnel. -Er nimmt keinen Proxmox-Token entgegen und schreibt keine Zugangsdaten. Alles, was -ein Geheimnis ist, geht den Rückweg durch den Tunnel (§6). Der öffentliche Endpunkt -gibt nur her, was ein Angreifer mit gültigem Code ohnehin bekäme: wo der Hub steht. +### Und danach wird getauscht + +Sobald der Tunnel steht, erzeugt der Host mit `wg genkey` einen **frischen** +Schlüssel, meldet den öffentlichen Teil **durch den Tunnel** (§6), und CluPilot +tauscht den Peer aus. Der Schlüssel, der je in einer Befehlszeile und einer +Zwischenablage stand, ist danach wertlos. + +Das ist der Grund, warum diese Lösung der öffentlichen Tür vorzuziehen ist: der +Preis ist ein Schlüssel mit einer Lebensdauer von Minuten, der Gewinn ist, dass die +Netzgrenze **unverändert** bleibt. Es kommt kein einziger neuer Weg von außen +hinzu. + +### Der Einmal-Code + +Bleibt trotzdem, aber er ist jetzt nur noch ein **Ausweis für den Rückweg**, nicht +der Schlüssel zu einer Auskunft: 32 Zeichen aus `random_bytes`, in der Datenbank +**nur als Hash**, mit `expires_at` (**24 Stunden**, großzügig — er öffnet nichts +nach außen) und `used_at`. Das Skript legt ihn den Aufrufen aus §6 und §7 bei, +damit ein Gerät im Tunnel nicht für einen fremden Host sprechen kann. --- @@ -131,13 +142,19 @@ Nachdem das Skript dem Tunnel beigetreten ist, meldet es sich über die **Tunnel-Adresse** von CluPilot — nicht über die öffentliche. `POST /host/register` (nur aus dem WireGuard-Subnetz erreichbar, `TRUSTED_RANGES`) -mit dem Fortschritts-Token und: +mit dem Einmal-Code als Ausweis und: - dem Proxmox-API-Token, den das Skript auf der Maschine erzeugt hat +- dem **frisch erzeugten WireGuard-Pubkey** (§5): CluPilot tauscht den Peer aus und + verwirft den, der in der Befehlszeile stand - dem SSH-Host-Schlüssel-Fingerabdruck (nur zur Anzeige; die Plattform verbindet sich nicht mehr) - Knotenname, `pve_version`, Plattengröße, RAM, Kerne +**Reihenfolge beim Schlüsseltausch:** erst den neuen Peer aufnehmen, dann den alten +entfernen. Andersherum schneidet sich der Host im selben Aufruf den Ast ab, auf dem +er sitzt — und die Antwort erreicht ihn nie. + CluPilot legt den Token in den Tresor (`SecretVault`, verschlüsselt mit `SECRETS_KEY`) und schreibt `hosts.api_token_ref`. **Der Betreiber sieht den Token nie.** @@ -150,8 +167,14 @@ Zwischenspeichern. Der Tunnel ist ohnehin da, wenn dieser Aufruf passiert. ## 7. Fortschritt: der Host erzählt, die Konsole zeigt -`POST /host/progress` (ebenfalls nur aus dem Tunnel, mit dem Fortschritts-Token) -meldet je Abschnitt einen Schlüssel und einen Zustand. +`POST /host/progress` (ebenfalls nur aus dem Tunnel, mit dem Einmal-Code als +Ausweis) meldet je Abschnitt einen Schlüssel und einen Zustand. + +Die Abschnitte **vor** `wireguard_joined` kann der Host naturgemäß noch nicht +melden — es gibt bis dahin keinen Weg zu CluPilot. Sie werden lokal mitgeschrieben +und beim ersten erreichbaren Aufruf **nachgereicht**, mit ihren Zeitstempeln. Die +Konsole zeigt bis dahin „wartet auf den Tunnel" und, nach einer Frist, dass sie +lange wartet. Die Abschnitte des Skripts, in dieser Reihenfolge: @@ -264,17 +287,24 @@ Die Beweislast teilt sich: **Was die Suite prüfen kann und muss:** -1. `POST /enrol` gibt ohne gültigen Code nichts preis, und die Antwortzeit - unterscheidet nicht zwischen „Code falsch" und „Code existiert nicht". -2. Ein Code gilt **einmal** und verfällt nach 60 Minuten. -3. Der Code liegt **nur als Hash** in der Datenbank. -4. `POST /host/register` und `/host/progress` sind **von außerhalb des Tunnels - nicht erreichbar** — der wichtigste Test dieser Spec. -5. Der Proxmox-Token landet im Tresor und **nie** in einer Antwort, einem Protokoll +1. **Es entsteht kein öffentlich erreichbarer Weg.** Ein Test hält fest, dass + `register`, `progress` und `routes` alle drei hinter der Tunnel-Beschränkung + liegen — der wichtigste Test dieser Spec, und der Grund, warum der Entwurf so + aussieht. +2. Ein Einmal-Code gilt **einmal**, verfällt nach 24 Stunden und liegt **nur als + Hash** in der Datenbank. +3. Ein Aufruf mit dem Code eines **fremden** Hosts wird abgewiesen — sonst spricht + ein Gerät im Tunnel für eine Maschine, die ihm nicht gehört. +4. Der Proxmox-Token landet im Tresor und **nie** in einer Antwort, einem Protokoll oder einer Ansicht. -6. `GET /host/routes` liefert genau die Routen dieses Hosts und keine fremden. -7. Die geschrumpfte Kette hat die sechs Schritte in der festgelegten Reihenfolge. -8. Ein Fortschrittsbericht für einen fremden Host wird abgewiesen. +5. **Der Schlüsseltausch nimmt erst auf, dann weg.** Ein Test, der die Reihenfolge + umdreht, muss zeigen, dass der Host dabei aus dem Tunnel fällt — sonst ist die + Reihenfolge nur ein Kommentar. +6. Der Schlüssel aus der Befehlszeile ist nach dem Tausch **nicht mehr gültig**. +7. `GET /host/routes` liefert genau die Routen dieses Hosts und keine fremden. +8. Die geschrumpfte Kette hat die sechs Schritte in der festgelegten Reihenfolge. +9. Nachgereichte Fortschrittsmeldungen behalten **ihre** Zeitstempel, nicht den des + Eingangs — sonst sieht eine zwanzigminütige Installation aus wie eine Sekunde. **Was nur auf echter Hardware zu beweisen ist:** alles im Skript. Der Handoff sagt dazu das Richtige — *„Jeder Schritt, der scheitert, ist ein Fund — und der Server @@ -286,8 +316,10 @@ auf einer frisch aufgesetzten Maschine immer gleich ausgeht. ## 12. Reihenfolge -1. Einmal-Code, `POST /enrol`, Ablauf und Verbrauch — mit den Sicherheitstests. -2. Tunnel-Endpunkte `register` und `progress`, Token in den Tresor. +1. Einmal-Code (Hash, Ablauf, Verbrauch) und die Schlüsselpaar-Erzeugung beim + Anlegen des Hosts — mit den Sicherheitstests aus §11. +2. Tunnel-Endpunkte `register` und `progress`, Token in den Tresor, Schlüsseltausch + in der richtigen Reihenfolge. 3. Adminbereich: Host anlegen zeigt die Befehlszeile, Fortschritt läuft mit. 4. Routen-Endpunkt und Umstellung von `SshTraefikWriter` auf den Abruf. 5. Die Kette schrumpfen, Bereitschaftsprüfungen nachziehen. @@ -301,10 +333,12 @@ ist die Abnahme, und sie ist die einzige, die zählt. ## 13. Risiken -- **Ein öffentlicher Endpunkt mehr.** Heute ist alles außer Portal und Website - zugesperrt. `POST /enrol` ist die erste Ausnahme. Er gibt ohne gültigen Code - nichts preis und ist ratenbegrenzt — aber er ist neu, und neue Türen sind - Türen. +- **Schlüsselmaterial in der Zwischenablage.** Der WireGuard-Schlüssel, mit dem der + Host dem Tunnel beitritt, steht in der kopierten Befehlszeile und damit kurz in + der Zwischenablage und im Verlauf des Rettungssystems. Entschärft dadurch, dass + er Minuten lebt und nach dem Tausch (§5) wertlos ist — und dass das + Rettungssystem beim ersten Neustart ohnehin verworfen wird. Wer den Tausch + weglässt, hat diesen Punkt dauerhaft; deshalb ist er nicht optional. - **Der Fortschritt hängt am Host.** Ein Skript, das stirbt, bevor es meldet, hinterlässt eine Konsole, die auf etwas wartet, das nie kommt. Deshalb je Abschnitt eine Frist und eine sichtbare Aussage, welcher offen ist.