Plan: die Erneuerungswache muss alle Zertifikate abdecken, nicht eines

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 <noreply@anthropic.com>
main
nexxo 2026-08-04 17:16:18 +02:00
parent 23027ac3c7
commit a317145468
1 changed files with 102 additions and 0 deletions

View File

@ -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 215241)
- Modify: `deploy/update.sh` (VPN-Block ~Zeilen 335360)
- Modify: `deploy/update-agent.sh` (`sync_vpn_certificate()`, ~Zeilen 336363)
- 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`