diff --git a/docs/superpowers/specs/2026-08-04-oeffentliche-seiten-im-tunnel-design.md b/docs/superpowers/specs/2026-08-04-oeffentliche-seiten-im-tunnel-design.md new file mode 100644 index 0000000..11770b9 --- /dev/null +++ b/docs/superpowers/specs/2026-08-04-oeffentliche-seiten-im-tunnel-design.md @@ -0,0 +1,149 @@ +# Die öffentlichen Seiten im Tunnel — Entwurf + +**Stand:** 2026-08-04 +**Zustand:** Vom Betreiber freigegeben. Namensumfang, Verhalten bei fehlendem +Zertifikat und Verdrahtungsweg sind entschieden. + +## Worum es geht + +Solange die Website verborgen ist (`site.public` aus), sieht nur die echte +Seite, wer aus einem Netz in der Freigabeliste kommt — sonst erscheint die +Platzhalterseite. Über das Management-VPN funktioniert das für die **Konsole**, +aber nicht für Portal und Website. + +Gemeldet: am Telefon, mit aktivem Tunnel (`10.66.0.2/32`, Handshake frisch), +zeigt `app.clupilot.com` die Platzhalterseite. Am Notebook über die Büroleitung +(`62.178.50.33`, in der Freigabeliste) erscheint die echte Seite. + +## Warum es nicht funktioniert + +Nicht falsch eingestellt — nicht gebaut. Zwei Zeilen erklären alles: + +- **Der Tunnel trägt nur das Management-Netz.** Im Client steht + `AllowedIPs = 10.66.0.0/24`. `app.clupilot.com` löst auf die öffentliche + Adresse des Servers auf, und dieser Verkehr geht am Tunnel vorbei. Der Server + sieht also die öffentliche Adresse des Telefons, nicht `10.66.0.2`. +- **Der interne Resolver biegt genau einen Namen um.** + [`docker-compose.yml:212`](../../../docker-compose.yml) — + `--address=/${VPN_INTERNAL_HOST}/${CLUPILOT_WG_HUB_ADDRESS}`. Alles andere + geht an `1.1.1.1`. Und der Tunnel-Gateway + ([`docker/caddy/vpn.Caddyfile`](../../../docker/caddy/vpn.Caddyfile)) hat + genau einen Site-Block, für denselben einen Namen. + +Die Konsole ist im Tunnel erreichbar, weil ihr Name dort auf `10.66.0.1` zeigt +und der Gateway ihn bedient. Für Portal, Website und Statusseite gilt beides +nicht. + +## Was entschieden ist + +| Frage | Entscheidung | +|---|---| +| Welche Namen | `app.`, `www.` (samt Aliassen) und `status.` | +| `files.` | **Nicht.** Ein Server im Rettungssystem holt dort sein Archiv und ist per Definition nicht im Tunnel | +| Fehlt ein Zertifikat | Diesen Namen auslassen, die übrigen bedienen | +| Wer verdrahtet | Der Gateway findet seine Zertifikate selbst | + +## Die Strecke + +### 1. Der Resolver — eine hosts-Datei, kein zweiter `--address` + +Ein Artisan-Befehl schreibt die konfigurierten Namen im hosts-Format in das +vorhandene `dns-hosts`-Volume: + +``` +10.66.0.1 app.clupilot.com +10.66.0.1 www.clupilot.com +10.66.0.1 status.clupilot.com +``` + +dnsmasq überwacht dieses Verzeichnis bereits (`--hostsdir`) und übernimmt +Änderungen ohne Neustart. `RegisterHostDns` schreibt dort schon die Host-Namen +hinein, und der `app`-Container hängt das Volume schreibbar ein — gleicher Ort, +gleicher Mechanismus, kein root, keine Compose-Änderung. + +Bewusst **keine** zweite `--address`-Zeile: die fängt zusätzlich jede +Unterdomain mit ab, und eine Compose-`command`-Zeile kann nicht über eine +variable Namensliste schleifen. + +Die Hub-Adresse kommt aus `CLUPILOT_WG_HUB_ADDRESS`, nicht als Literal — auf +der nächsten Installation ist sie eine andere. + +### 2. Der Gateway erzeugt seine Konfiguration beim Start + +Ein Startskript rendert die Caddy-Konfiguration aus zwei Quellen: den +konfigurierten Namen und den Zertifikaten, die im eingehängten Speicher +**tatsächlich liegen**. Ein Site-Block je Name mit `.crt` **und** `.key`; jeder +andere Name wird ausgelassen und beim Start benannt. + +Der Konsolenname steht zuerst. Damit kann „für `www.` gibt es noch kein +Zertifikat" den Tunnel-Zugang zur Konsole nie mitnehmen — genau der Ausfall, +vor dem [`update.sh:352`](../../../deploy/update.sh) heute warnt: + +> *„Both or neither: leaving the profile on with empty tls paths starts a Caddy +> that cannot load its configuration and crashes forever."* + +Das Zertifikat bleibt das, was der öffentliche Caddy ohnehin erneuert. Es hängt +am Namen, nicht an der Adresse, die ihn ausliefert — dieselbe Begründung, die im +Kopf der `vpn.Caddyfile` schon steht. + +Der Gesundheits-Port bleibt unverändert und unabhängig von jedem Zertifikat: er +ist das Signal, an dem die Bereitschaft des Tunnels hängt. + +### 3. `VPN_CERT_PATH` / `VPN_KEY_PATH` bleiben stehen + +Sie werden überflüssig, aber sie werden **nicht** entfernt. `install-agent.sh` +und `update.sh` schreiben, prüfen und löschen sie an fünf Stellen, und ein +Fehler darin sperrt den Betreiber aus dem Tunnel aus. + +Der Gateway ignoriert sie künftig und sucht selbst. Die eine nötige Änderung an +`update.sh`: es darf das VPN-Profil nicht mehr abschalten, bloß weil die Pfade +leer sind. Ob der Tunnel bereit ist, entscheidet weiterhin die +Gesundheitsprüfung, die es seit [`update.sh:263`](../../../deploy/update.sh) +ohnehin macht — die ist bereits probe-basiert und nicht pfad-basiert. + +### 4. `clupilot:bind-hosts` härtet die Eingabe + +Ein Wert, der kein Hostname sein kann, wird abgelehnt statt geschrieben — +geprüft je Name, auch in einer Komma-Liste, gegen dasselbe Muster, das der +root-Helfer in `apply-proxy-hosts` benutzt. + +Der Anlass ist konkret: In die `.env` der Produktivmaschine gelangte + +``` +SITE_HOST=[www.clupilot.com](https://www.clupilot.com) +``` + +— ein Markdown-Link, aus einem Kopiervorgang. `EnvFileEditor` prüft nur, ob die +Zeile `KEY=value` ist, also wurde er widerspruchslos geschrieben. Die Website +war danach an einen Namen gebunden, den keine Anfrage je trifft. Das Repo kennt +diese Falle bereits: `RestrictConsoleNetwork::isNetwork()` existiert mit der +Begründung, ein Eintrag, der nichts trifft, werde „stored happily and reports +success". + +## Was nicht angefasst wird + +`PublicSiteGate`, `RestrictConsoleNetwork` und die Freigabeliste. Sobald der +Verkehr durch den Tunnel kommt, ist die Quelladresse `10.66.0.x` — die steht in +`TRUSTED_RANGES` und wird geprüft, wie sie soll. Daran ist nichts zu ändern. + +## Das Risiko, das benannt gehört + +Der Resolver biegt diese Namen für **jeden** Tunnel-Teilnehmer um, nicht nur für +den Betreiber — auch für die Proxmox-Hosts. Die erreichen das Portal danach über +den Gateway statt über das öffentliche Netz. + +Das ist unschädlich und eher besser (der Weg verlässt die Maschine nicht), aber +es ist eine Verhaltensänderung, die niemand bestellt hat. Sie steht deshalb +hier und nicht in einer Überraschung. + +## Abnahme + +1. Am Telefon über VPN zeigen **`app.`, `www.` und `status.`** die echte Seite, + nicht die Platzhalterseite. +2. Ohne VPN und ohne Adresse in der Freigabeliste: weiterhin die + Platzhalterseite. +3. Ein Name ohne Zertifikat schaltet den Gateway nicht ab — die Konsole bleibt + im Tunnel erreichbar, und der ausgelassene Name wird beim Start gemeldet. +4. `clupilot:bind-hosts` lehnt einen Markdown-Link ab und schreibt nichts. +5. `files.` ist im Tunnel **nicht** umgebogen und bleibt öffentlich erreichbar. +6. Voller Testlauf grün.