Drop the public endpoint: the command line already carries what the host needs
The operator was right to challenge POST /enrol. The three values a script needs before the tunnel exists — hub key, hub address, assigned tunnel IP — are all known to the console when the host is created, so they travel in the copied command instead of being fetched. The one thing that forced an endpoint was the hub needing the host's public key before the tunnel is up. So the console generates the pair and hands it over, and the host swaps in a freshly generated key through the tunnel afterwards. The key that passed through a clipboard lives for minutes. Net effect: the network boundary is unchanged. No new way in from outside. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feature/host-bootstrap
parent
0f1f397823
commit
c0cd367a0e
|
|
@ -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.
|
||||
|
|
|
|||
Loading…
Reference in New Issue