diff --git a/docs/superpowers/plans/2026-08-04-oeffentliche-seiten-im-tunnel.md b/docs/superpowers/plans/2026-08-04-oeffentliche-seiten-im-tunnel.md index 2b148a4..0f99698 100644 --- a/docs/superpowers/plans/2026-08-04-oeffentliche-seiten-im-tunnel.md +++ b/docs/superpowers/plans/2026-08-04-oeffentliche-seiten-im-tunnel.md @@ -60,6 +60,7 @@ Caddy 2 (alpine), Docker Compose. | `tests/Feature/VpnGatewayConfigTest.php` | **neu** — führt das Skript wirklich aus | | `docker-compose.yml` | **ändern** — Gateway bekommt Namen und Entrypoint | | `deploy/update.sh` | **ändern** — Profil hängt nicht mehr an den Zertifikatspfaden | +| `deploy/update-agent.sh` | **ändern** — die Erneuerungswache deckt alle geladenen Zertifikate ab | | `app/Console/Commands/BindHosts.php` | **ändern** — Hostnamen prüfen | | `tests/Feature/BindHostsCommandTest.php` | **ändern** — zwei Tests dazu | | `VERSION` | **ändern** — Release | @@ -489,6 +490,48 @@ it('nimmt keinen Namen auf, den niemand konfiguriert hat', function () { expect($config)->not->toContain('files.clupilot.test'); }); +it('schreibt die geladenen Zertifikate mit, damit die Erneuerung greift', function () { + // Der Update-Agent startet den Gateway neu, wenn sich eines der geladenen + // Zertifikate aendert — Caddys `tls` liest die Datei nur beim Start. Bisher + // ueberwachte er GENAU EINEN Pfad aus der .env; mit mehreren Namen liefe + // der Tunnel nach einer Erneuerung von www. mit einem abgelaufenen + // Zertifikat weiter. Die Liste ist, woran er sie erkennt. + $dir = sys_get_temp_dir().'/clupilot-vpn-'.bin2hex(random_bytes(6)); + mkdir($dir.'/certs/certificates/acme', 0755, true); + + foreach (['admin.clupilot.test', 'app.clupilot.test'] as $name) { + file_put_contents($dir."/certs/certificates/acme/{$name}.crt", 'cert'); + file_put_contents($dir."/certs/certificates/acme/{$name}.key", 'key'); + } + + $list = $dir.'/certs.list'; + + $process = Process::fromShellCommandline( + 'bash '.escapeshellarg(base_path('docker/caddy/vpn-entrypoint.sh')) + ); + $process->setEnv([ + 'VPN_INTERNAL_HOST' => 'admin.clupilot.test', + // www. hat kein Zertifikat und darf deshalb auch nicht in der Liste stehen. + 'VPN_TUNNEL_HOSTS' => 'app.clupilot.test,www.clupilot.test', + 'VPN_HUB_ADDRESS' => '10.66.0.1', + 'VPN_HEALTH_PORT' => '8081', + 'VPN_CERT_DIR' => $dir.'/certs', + 'VPN_CONFIG_OUT' => $dir.'/rendered.Caddyfile', + 'VPN_CERT_LIST' => $list, + 'VPN_RENDER_ONLY' => '1', + ]); + $process->run(); + + $written = is_file($list) ? file_get_contents($list) : ''; + + exec('rm -rf '.escapeshellarg($dir)); + + expect($process->getExitCode())->toBe(0, $process->getErrorOutput()) + ->and($written)->toContain('admin.clupilot.test.crt') + ->and($written)->toContain('app.clupilot.test.crt') + ->and($written)->not->toContain('www.clupilot.test'); +}); + it('gibt jedem Block die Weiterleitung mit der echten Quelladresse', function () { // Ohne X-Forwarded-For sähe die Anwendung den Gateway statt des Anrufers, // und die Freigabeliste prüfte die falsche Adresse. @@ -604,6 +647,25 @@ fi exec caddy run --config "$OUT" --adapter caddyfile ``` +Und in `emit_site()`, direkt nach dem Schreiben des Blocks, den geladenen Pfad +mitschreiben: + +```sh + echo "$crt" >> "$CERT_LIST" +``` + +sowie oben bei den Variablen: + +```sh +# Die Zertifikate, die dieser Lauf wirklich geladen hat. Der Update-Agent +# ueberwacht sie und startet den Gateway nach einer Erneuerung neu — Caddys +# `tls` liest die Datei EINMAL beim Start, und ohne Neustart liefe der Tunnel +# danach mit einem abgelaufenen Zertifikat weiter, ausgerechnet fuer die +# einzigen Leute, die die Konsole noch erreichen. +CERT_LIST="${VPN_CERT_LIST:-/tmp/vpn-certs.list}" +: > "$CERT_LIST" +``` + Ausführbar machen: `chmod +x docker/caddy/vpn-entrypoint.sh` - [ ] **Step 4: Tests laufen lassen** @@ -625,6 +687,7 @@ git commit -m "Der Tunnel-Gateway laesst Namen ohne Zertifikat aus, statt nicht **Files:** - Modify: `docker-compose.yml` (Block `vpn-gateway`, ~Zeilen 215–241) - Modify: `deploy/update.sh` (VPN-Block ~Zeilen 335–360) +- Modify: `deploy/update-agent.sh` (`sync_vpn_certificate()`, ~Zeilen 336–363) - Delete: `docker/caddy/vpn.Caddyfile` **Interfaces:** @@ -693,6 +756,45 @@ und lass das Profil eingeschaltet, solange `VPN_INTERNAL_HOST` gesetzt ist. Die Prüfung „gehört das Zertifikat zum aktuellen Hostnamen" darf bleiben; sie schadet nicht und räumt einen veralteten Pfad weg. +- [ ] **Step 3b: Die Zertifikatserneuerung auf alle Namen ausweiten** + +In `deploy/update-agent.sh`, Funktion `sync_vpn_certificate()`: sie liest heute +**einen** Pfad aus `VPN_CERT_PATH` und `stat`t ihn im Gateway-Container. Mit +mehreren Namen greift das nicht mehr — eine Erneuerung von `www.` löste keinen +Neustart aus, und der Tunnel liefe mit einem abgelaufenen Zertifikat weiter. + +Ersetze die Ermittlung von `path`/`stamp` durch die Liste, die das Startskript +schreibt, mit Rückfall auf den alten Pfad: + +```bash + # Die Liste, die das Startskript beim Rendern geschrieben hat: genau die + # Zertifikate, die dieser Gateway wirklich geladen hat. Vorher stand hier + # EIN Pfad aus der .env — mit mehreren Namen haette eine Erneuerung von www. + # keinen Neustart ausgeloest, und der Tunnel liefe mit einem abgelaufenen + # Zertifikat weiter. Genau der Ausfall, den diese Funktion verhindern soll. + # + # Gelesen wird weiterhin INNERHALB des Gateways: Caddys Speicher gehoert + # seinem eigenen Dienstkonto, und dieser Agent laeuft unprivilegiert und + # koennte ihn nicht einmal durchqueren. + stamp="$(docker compose exec -T vpn-gateway sh -c \ + 'xargs -r stat -c %Y < /tmp/vpn-certs.list 2>/dev/null | sort | tr "\n" ","' \ + 2>/dev/null | tr -d '\r\n')" + + # Rueckfall auf den alten Weg, solange ein Gateway von vor dieser Fassung + # laeuft und die Liste noch nicht schreibt. Ohne ihn bliebe die Erneuerung + # zwischen Update und Neustart des Gateways unbeaufsichtigt. + if [[ -z "$stamp" ]]; then + path="$(sed -n 's/^VPN_CERT_PATH=//p' "$ROOT/.env" 2>/dev/null | tail -1)" + [[ -n "$path" ]] || return 0 + stamp="$(docker compose exec -T vpn-gateway stat -c %Y "$path" 2>/dev/null | tr -d '\r\n')" + fi + + [[ -n "$stamp" ]] || return 0 +``` + +Die frühe Rückgabe `[[ -f "$ROOT/.env" ]] || return 0` bleibt stehen — ihre +Begründung (fehlende `.env` beendet sonst den ganzen Agenten) gilt unverändert. + - [ ] **Step 4: Syntax und voller Testlauf** Run: `bash -n deploy/update.sh`