From a31714546886ac7e9fe1ed9481383d2bc1dbd5e7 Mon Sep 17 00:00:00 2001 From: nexxo Date: Tue, 4 Aug 2026 17:16:18 +0200 Subject: [PATCH] Plan: die Erneuerungswache muss alle Zertifikate abdecken, nicht eines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vorabcheck vor dem ersten Dispatch. sync_vpn_certificate() in update-agent.sh ueberwacht GENAU EINEN Pfad aus VPN_CERT_PATH und startet den Gateway neu, wenn er sich aendert — weil Caddys tls-Direktive die Datei nur beim Start liest. Mit mehreren Namen haette eine Erneuerung von www. keinen Neustart ausgeloest, und der Tunnel liefe mit einem abgelaufenen Zertifikat weiter. Genau der Ausfall, den die Funktion laut ihrem eigenen Kommentar verhindern soll, nur eine Ebene groesser. Das Startskript schreibt jetzt mit, welche Zertifikate es geladen hat; die Wache liest diese Liste und faellt auf den alten Pfad zurueck, solange ein Gateway von vor dieser Fassung laeuft. Co-Authored-By: Claude Opus 5 --- ...026-08-04-oeffentliche-seiten-im-tunnel.md | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) 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`