Entwurf: die oeffentlichen Seiten im Tunnel erreichbar

Am Telefon mit aktivem VPN zeigt app.clupilot.com die Platzhalterseite. Nicht
falsch eingestellt, sondern nie gebaut: der Tunnel traegt nur 10.66.0.0/24, und
der interne Resolver biegt genau einen Namen um — den der Konsole. Portal,
Website und Statusseite loesen oeffentlich auf und gehen am Tunnel vorbei.

Der Gateway sucht seine Zertifikate kuenftig selbst und laesst Namen ohne
Zertifikat aus, statt gar nicht zu starten. Sonst nimmt ein fehlendes Zertifikat
fuer www. den Tunnel-Zugang zur Konsole mit — genau der Ausfall, vor dem
update.sh heute warnt.

files. bleibt absichtlich draussen: ein Server im Rettungssystem holt dort sein
Archiv und ist per Definition nicht im Tunnel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
main
nexxo 2026-08-04 17:04:04 +02:00
parent c68985f160
commit afc2f90dac
1 changed files with 149 additions and 0 deletions

View File

@ -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.