Fix-Runde 1: der Update-Agent stirbt nicht mehr an einem gestoppten Gateway, und der Gesundheits-Port luegt nicht mehr
Kritisch: sync_vpn_certificate() rief docker compose exec als blanke Zuweisung unter set -Eeuo pipefail auf — schlaegt das fehl (kein laufender vpn-gateway), beendet set -e den ganzen Agenten, still, nachdem write_alive schon lief. Beide Zuweisungen bekommen jetzt `|| stamp=''`, mit Begruendung im Kommentar. Wichtig: der Gesundheits-Port antwortete immer mit 204, auch wenn das Startskript fuer den Konsolennamen kein Zertifikat fand und den Gateway ohne jede Seite auf 443 rendert. VPN_READY wurde dann wahr, und ausgegebene Client-Konfigurationen nannten einen Resolver, der die Verbindung ablehnt. vpn-entrypoint.sh prueft jetzt, ob die Konsolen-Seite wirklich gerendert wurde, und antwortet sonst mit 503 — wget --spider (gegen das echte caddy:2-alpine-Image verifiziert, nicht angenommen) behandelt das als Fehlschlag, vpn_ready bleibt false, und die bestehende Warnung in update.sh greift wieder: sie ist nicht verschwunden, sondern hierher gewandert. Zwei Tests in VpnGatewayConfigTest decken beide Richtungen ab; der alte Test mit der jetzt falschen Annahme "unabhaengig von jedem Zertifikat" wich dem Test fuer den Fall mit Zertifikat. Kleinigkeit: ein Satz im Kommentar von sync_vpn_certificate() haelt fest, dass die Zertifikatsliste einmalig beim Start geschrieben wird und ein nachtraeglich ausgestelltes Zertifikat erst den naechsten Neustart des Gateways sieht.main
parent
4dbbed72e2
commit
5967c56d16
|
|
@ -349,12 +349,28 @@ sync_vpn_certificate() {
|
|||
# keinen Neustart ausgeloest, und der Tunnel liefe mit einem abgelaufenen
|
||||
# Zertifikat weiter. Genau der Ausfall, den diese Funktion verhindern soll.
|
||||
#
|
||||
# Geschrieben EINMAL beim Start des Gateways, nicht laufend nachgefuehrt: ein
|
||||
# Name, dessen Zertifikat erst NACH diesem Start ausgestellt wird, bleibt bis
|
||||
# zum naechsten Neustart des Gateways unbedient — diese Funktion sieht ihn
|
||||
# nicht, weil er nie in der Liste stand. Kein Rueckschritt gegenueber vorher,
|
||||
# aber auch keine laufende Ueberwachung neuer Namen.
|
||||
#
|
||||
# Gelesen wird weiterhin INNERHALB des Gateways: Caddys Speicher gehoert
|
||||
# seinem eigenen Dienstkonto, und dieser Agent laeuft unprivilegiert und
|
||||
# koennte ihn nicht einmal durchqueren.
|
||||
#
|
||||
# `|| stamp=''`, nicht bloss eine Zuweisung: genau wie beim `sed` oben stirbt
|
||||
# unter `set -Eeuo pipefail` sonst der GANZE Agent an dieser Stelle, wenn
|
||||
# `docker compose exec` fehlschlaegt — und das tut es auf jedem Wirt ohne
|
||||
# laufenden vpn-gateway (nie konfiguriert, gestoppt, abgestuerzt, mitten in
|
||||
# einem Deploy neu erzeugt). `write_alive running` ist zu diesem Zeitpunkt
|
||||
# schon gelaufen, der Takt saehe also gesund aus, waehrend der Agent seinen
|
||||
# eigentlichen Zweck — die Anfrage vom Bedienfeld zu lesen — nie wieder
|
||||
# erreicht. Frueher konnte das nicht passieren: das erste Kommando war der
|
||||
# `sed` auf `.env`, hinter genau dieser Wache. Dieser Aufruf steht davor.
|
||||
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')"
|
||||
2>/dev/null | tr -d '\r\n')" || stamp=''
|
||||
|
||||
# Rueckfall auf den alten Weg, solange ein Gateway von vor dieser Fassung
|
||||
# laeuft und die Liste noch nicht schreibt. Ohne ihn bliebe die Erneuerung
|
||||
|
|
@ -362,7 +378,9 @@ sync_vpn_certificate() {
|
|||
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')"
|
||||
# Dieselbe Wache wie oben: ein Gateway, das gerade nicht laeuft, darf den
|
||||
# Agenten nicht mitreissen.
|
||||
stamp="$(docker compose exec -T vpn-gateway stat -c %Y "$path" 2>/dev/null | tr -d '\r\n')" || stamp=''
|
||||
fi
|
||||
|
||||
[[ -n "$stamp" ]] || return 0
|
||||
|
|
|
|||
|
|
@ -268,9 +268,23 @@ reconcile_vpn_readiness() {
|
|||
# Over plain HTTP on the health port, not HTTPS: the TLS site matches on the
|
||||
# console's hostname, so a request to the bare address presents no SNI, gets
|
||||
# no certificate and fails the handshake — which looks exactly like the
|
||||
# outage this is meant to detect. Caddy refuses to start at all if the
|
||||
# certificate is unreadable, so a health port that answers still proves the
|
||||
# whole gateway loaded.
|
||||
# outage this is meant to detect.
|
||||
#
|
||||
# The health port itself now answers 204 only when the console's certificate
|
||||
# was actually found and its site rendered (see docker/caddy/vpn-entrypoint.sh)
|
||||
# — 503 otherwise. That used to read "Caddy refuses to start at all if the
|
||||
# certificate is unreadable, so a health port that answers proves the whole
|
||||
# gateway loaded", which was true only of the fixed configuration: this
|
||||
# gateway now starts and serves the health port regardless, skipping any site
|
||||
# whose certificate is missing. A 204 that only meant "the process is up"
|
||||
# would no longer be a fit signal for VPN_READY — a client would be handed a
|
||||
# resolver pointing at a console the gateway isn't actually serving on 443.
|
||||
# `wget --spider` (and the `-O /dev/null` fallback) both fail on a 503 —
|
||||
# checked directly against this image (`caddy:2-alpine`'s BusyBox wget), not
|
||||
# assumed — so a missing console certificate lands exactly here: vpn_ready
|
||||
# stays false and the warning below fires, the same one that used to run when
|
||||
# the profile was disabled outright. The message did not go away with that
|
||||
# branch; it moved here.
|
||||
# The same variable compose hands the gateway, so the two cannot drift.
|
||||
local hub port health
|
||||
hub="$(sed -n 's/^CLUPILOT_WG_HUB_ADDRESS=//p' .env | tail -1)"
|
||||
|
|
|
|||
|
|
@ -80,14 +80,28 @@ echo "${VPN_TUNNEL_HOSTS:-}" | tr ',' '\n' | while read -r host; do
|
|||
emit_site "$host"
|
||||
done
|
||||
|
||||
# Der Gesundheits-Port, ohne TLS und ohne Namen. Daran hängt VPN_READY und damit,
|
||||
# ob ausgegebene Client-Konfigurationen den Resolver überhaupt nennen — er darf
|
||||
# deshalb von keinem Zertifikat abhängen.
|
||||
# Antwortet der Gesundheits-Port mit 204, gilt der Tunnel als BEREIT — daran
|
||||
# haengt, ob ausgegebene Client-Konfigurationen den Resolver ueberhaupt nennen.
|
||||
# Er darf deshalb nicht bloss sagen „dieser Gateway laeuft": ohne Zertifikat fuer
|
||||
# den Konsolennamen rendert dieses Skript einen Gateway, der auf 443 gar nichts
|
||||
# bedient — und ein Client bekaeme einen Resolver genannt, der ihn auf eine
|
||||
# Adresse schickt, die die Verbindung ablehnt. Frueher war das unmoeglich, weil
|
||||
# Caddy ohne lesbares Zertifikat ueberhaupt nicht startete; genau diese Annahme
|
||||
# hat dieses Skript aufgehoben.
|
||||
if [ -n "${VPN_INTERNAL_HOST:-}" ] && grep -q "^https://${VPN_INTERNAL_HOST}:443 {" "$OUT"; then
|
||||
HEALTH_CODE=204
|
||||
else
|
||||
HEALTH_CODE=503
|
||||
fi
|
||||
|
||||
# Der Gesundheits-Port selbst, ohne TLS und ohne Namen: ein Client, der nur den
|
||||
# Resolver braucht, soll nicht erst ein Zertifikat vorweisen muessen, um zu
|
||||
# erfahren, ob der Tunnel steht.
|
||||
{
|
||||
echo ""
|
||||
echo "http://${HUB}:${HEALTH} {"
|
||||
echo " bind ${HUB}"
|
||||
echo " respond /healthz 204"
|
||||
echo " respond /healthz ${HEALTH_CODE}"
|
||||
echo " respond 404"
|
||||
echo "}"
|
||||
} >> "$OUT"
|
||||
|
|
|
|||
|
|
@ -85,20 +85,36 @@ it('behält die Konsole, auch wenn sonst nichts ein Zertifikat hat', function ()
|
|||
->and($config)->not->toContain('www.clupilot.test');
|
||||
});
|
||||
|
||||
it('schreibt den Gesundheits-Port unabhängig von jedem Zertifikat', function () {
|
||||
// Daran hängt VPN_READY, und damit ob Client-Konfigurationen den Resolver
|
||||
// überhaupt nennen. Ein Gesundheits-Port, der ein Zertifikat braucht, wäre
|
||||
// genau die Attrappe, die schon einmal VPN_READY auf einer gesunden Anlage
|
||||
// false stehen ließ.
|
||||
it('schreibt den Gesundheits-Port als bereit, wenn die Konsole ein Zertifikat hat', function () {
|
||||
// Genau das Signal, an dem VPN_READY hängt: 204 heißt, ein Client darf den
|
||||
// ausgegebenen Resolver benutzen, weil die Konsole auf 443 wirklich bedient
|
||||
// wird — nicht bloß, dass irgendein Caddy-Prozess läuft.
|
||||
$config = renderVpnConfig([
|
||||
'VPN_INTERNAL_HOST' => 'admin.clupilot.test',
|
||||
'VPN_TUNNEL_HOSTS' => '',
|
||||
], []);
|
||||
], ['admin.clupilot.test']);
|
||||
|
||||
expect($config)->toContain('http://10.66.0.1:8081')
|
||||
->and($config)->toContain('respond /healthz 204');
|
||||
});
|
||||
|
||||
it('schreibt den Gesundheits-Port als NICHT bereit, wenn die Konsole kein Zertifikat hat', function () {
|
||||
// Ohne Zertifikat für die Konsole bedient dieser Gateway auf 443 gar
|
||||
// nichts — ein 204 hier wäre eine Falschmeldung: VPN_READY würde wahr,
|
||||
// und ein Client bekäme einen Resolver genannt, der ihn auf eine Adresse
|
||||
// schickt, die die Verbindung ablehnt. Andere Zertifikate (hier: app.)
|
||||
// dürfen daran nichts ändern — es geht um die KONSOLE, nicht um "irgendein
|
||||
// Zertifikat", das war genau die falsche Annahme im alten Test.
|
||||
$config = renderVpnConfig([
|
||||
'VPN_INTERNAL_HOST' => 'admin.clupilot.test',
|
||||
'VPN_TUNNEL_HOSTS' => 'app.clupilot.test',
|
||||
], ['app.clupilot.test']);
|
||||
|
||||
expect($config)->toContain('http://10.66.0.1:8081')
|
||||
->and($config)->toContain('respond /healthz 503')
|
||||
->and($config)->not->toContain('respond /healthz 204');
|
||||
});
|
||||
|
||||
it('nimmt keinen Namen auf, den niemand konfiguriert hat', function () {
|
||||
$config = renderVpnConfig([
|
||||
'VPN_INTERNAL_HOST' => 'admin.clupilot.test',
|
||||
|
|
|
|||
Loading…
Reference in New Issue