CluPilotCloud/docs/runbooks/tunnel-recovery.md

11 KiB

Runbook: Wenn der Tunnel weg ist

Der WireGuard-Tunnel trägt alles: die Konsole, jeden Host, jede Provisionierung, das Terminal. Fällt er aus, sieht es aus, als sei das ganze Produkt kaputt — obwohl meistens nur eine Sitzung nicht wieder aufgebaut wurde.

Dieses Runbook beantwortet zwei Fragen. Erstens: womit komme ich noch hinein, wenn der Tunnel nicht steht? Zweitens: wie bringe ich ihn zurück?


Die eine Regel

Kein Weg zurück darf durch den Tunnel führen, der gerade kaputt ist.

Alles unten ist danach sortiert. Wer als Rettungsweg etwas einplant, das selbst WireGuard braucht, hat keinen Rettungsweg eingeplant.


Die vier Wege hinein, in dieser Reihenfolge

1. SSH auf den CluPilot-Server

Der Hebel für fast alles. Er hängt an nichts, was hier beschrieben wird — der Server ist über seine öffentliche Adresse erreichbar, unabhängig vom Tunnel.

ssh <benutzer>@<clupilot-server>
cd /opt/clupilot

Von hier aus laufen alle Befehle weiter unten.

2. Die Konsole des Anbieters (Hetzner: KVM oder Rettungssystem)

Für einen Host, nicht für den CluPilot-Server. Immer verfügbar, hängt an keinem Dienst von uns. Der Weg, wenn ein Host weder über den Tunnel noch über SSH antwortet.

3. Das Notfallskript auf dem Host

Liegt auf jedem übernommenen Host:

/usr/local/sbin/clupilot-emergency-open-firewall.sh

SecureHostFirewall lässt Port 22 nur aus dem WireGuard-Netz zu. Steht der Tunnel nicht, ist SSH aus dem Internet zu — das ist Absicht und genau der Grund, warum es dieses Skript gibt. Aufgerufen wird es über die Konsole des Anbieters (Weg 2), nicht per SSH; per SSH käme man ja gerade nicht hin.

Danach ist der Host wieder aus dem Internet erreichbar — und bleibt es, bis jemand die Regeln zurückschreibt. Nach der Reparatur, vom CluPilot-Server aus:

cd /opt/clupilot
sudo -u clupilot docker compose exec -T app php artisan clupilot:refresh-host-firewall --host=<name-oder-ip>

Ohne --host nimmt der Befehl jeden übernommenen Host; --dry-run sagt zuerst, welche das wären. Er startet je Host einen host-firewall-Lauf, der denselben Schritt SecureHostFirewall fährt wie die Übernahme — der Lauf ist also in der Konsole zu sehen, wird wiederholt und prüft vorher von der Hostseite aus nach, dass der Tunnel steht.

Nicht den ursprünglichen Übernahme-Lauf erneut anstoßen. SecureHostFirewall kürzt sich über den host_firewall-Brotkrumen ab, und der hängt am LAUF: auf einem bereits übernommenen Host würde der Schritt gar nichts tun und trotzdem „erledigt" melden. Genau darum gibt es den Befehl oben.

Derselbe Befehl ist auch der Weg, einem Host, der vor dem Frühwarnsystem übernommen wurde, die beiden Sperrmengen (clupilot_blocked, clupilot_blocked6) nachzureichen. Ohne sie scheitert jede Sperre still auf der Maschine, während sie im Portal als aktiv steht.

4. Eine WireGuard-Konfiguration, die offline liegt

In der Zugangsliste der Konsole gibt es zu jedem Mitarbeiter-Zugang einen Download. Eine davon gehört außerhalb der Konsole aufbewahrt — Passwort-Safe, zweites Gerät, ausgedruckt. Sonst braucht man die Konsole, um an den Zugang zu kommen, der einen zur Konsole bringt.


Zuerst: ein Befehl, der das meiste allein macht

cd /opt/clupilot && sudo -u clupilot bash deploy/rescue-tunnel.sh

Er stellt der Reihe nach fest, wo es klemmt, startet und zieht hoch, was fehlt, räumt die gemerkten Ströme weg — und sagt am Ende, was noch von Hand bleibt. Er fasst den Tunnel-Container nicht neu an; genau das wäre die Ursache und nicht die Lösung.

Was er tut, steht unten einzeln, falls man es von Hand nachvollziehen will.


Von Hand feststellen, wo es klemmt

Vom CluPilot-Server aus, in dieser Reihenfolge. Jede Zeile schließt eine Möglichkeit aus.

cd /opt/clupilot

# 1. Steht wg0 überhaupt, und wie viele Zugänge kennt es?
sudo -u clupilot docker compose exec -T vpn-hub wg show

# 2. Läuft der Container, dem der Namensraum mit wg0 gehört?
sudo -u clupilot docker compose ps vpn-hub

# 3. Was sagt er beim Starten?
sudo -u clupilot docker compose logs --tail=40 vpn-hub

Kein Interface, kein wg0 → weiter bei „Der Tunnel kommt nicht hoch".

Interface da, Zugänge da, aber Letzter Kontakt: nie oder uralt, und die Zahlen stehen bei ↑ etwas / ↓ 0 B → weiter bei „Der Tunnel steht, aber niemand kommt an".

Ein einzelner Zugang fehlt, die anderen stehen → das Problem liegt auf dessen Gegenseite, nicht hier. Bei einem Host: Weg 2 und systemctl status wg-quick@wg0 auf der Maschine.


Der Tunnel steht, aber niemand kommt an

Das häufigste Bild, und das mit der unauffälligsten Ursache.

Der Kernel merkt sich laufende UDP-Ströme samt Ziel. Wird der Tunnel-Container neu gebaut (nicht neu gestartet — neu gebaut), bekommt er eine neue Adresse im Compose-Netz, und die Weiterleitung für den WireGuard-Port wird neu geschrieben. Die gemerkten Einträge zeigen aber weiter auf den alten Container.

Und sie verfallen nicht: WireGuard schickt alle 25 Sekunden ein Lebenszeichen und hält den kaputten Eintrag damit am Leben. Der Zustand heilt also nie von allein.

Daran erkennt man ihn:

  • Ein Telefon kommt nach Aus- und Einschalten sofort zurück — ein neuer Quellport bekommt einen frischen Eintrag.
  • Ein Host kommt gar nicht zurück — er schickt stur vom selben Port.
  • Auf der Zugangsseite: ↑ wächst weiter, ↓ bleibt bei 0 B.

Der Griff, auf dem Wirt, nicht im Container:

sudo conntrack -D -p udp --dport 51820

Fehlt das Werkzeug: sudo apt-get install -y conntrack. Danach dreißig Sekunden warten — jeder Zugang meldet sich von selbst wieder, alle 25 Sekunden läuft sein Keepalive.

Seit v1.4.9 kann das gar nicht mehr passieren. Der Hub hat eine feste Adresse im Compose-Netz (172.18.0.240, über CLUPILOT_VPN_HUB_IP änderbar). Nach einem Neubau entsteht damit exakt dieselbe Weiterleitung, und die gemerkten Ströme bleiben gültig. Was unten steht, gilt für Server, die älter sind — und als Rückfahrkarte, falls jemand das Subnetz ändert.

Seit v1.4.6 macht das Update das Aufräumen ohnehin selbst. Es merkt sich die Container-ID vor und nach docker compose up -d; hat sie sich geändert, räumt es die Ströme weg. Kann es das nicht (es läuft als Dienstbenutzer, der Griff braucht Root), steht der Befehl als Warnung im Protokoll. Siehe „Einmal einrichten" unten.


Der Tunnel kommt nicht hoch

wg show zeigt gar kein Interface.

cd /opt/clupilot

# Von Hand hochziehen — sagt auch gleich, woran es scheitert.
sudo -u clupilot docker compose exec -T vpn-hub wg-quick up wg0

# Steht die Konfiguration überhaupt noch im Datenträger?
sudo -u clupilot docker compose exec -T vpn-hub ls -l /etc/wireguard/

Die Zugänge stehen in wg0.conf und überleben einen Neustart. Fehlt die Datei, ist der wireguard-Datenträger das Problem, nicht WireGuard.

Läuft der Container gar nicht:

sudo -u clupilot docker compose up -d vpn-hub
sudo -u clupilot docker compose logs --tail=40 vpn-hub

Prozess 1 ist /usr/local/bin/vpn-hub. Das Skript zieht wg0 hoch und tut danach nichts mehr — genau das ist die Aufgabe. Steht dort etwas anderes, läuft der Server auf einem Stand vor v1.4.8, und der Tunnel hängt noch am Provisionierungs-Arbeiter.


Ein Host kommt nicht zurück, alle anderen schon

Dann liegt es auf der Maschine. Über die Konsole des Anbieters (Weg 2):

systemctl status wg-quick@wg0
wg show
systemctl restart wg-quick@wg0

ConfigureWireguard schreibt dem Host Endpoint = <Hub> und PersistentKeepalive = 25 — er meldet sich also von sich aus, alle 25 Sekunden. Tut er das nicht, läuft wg0 dort nicht.

Kommt man über die Konsole nicht weit genug, weil SSH zu ist: erst Weg 3, dann weiter.


Einmal einrichten, damit der Plan später trägt

Vier Dinge, die man vorher macht und nicht, wenn es brennt.

  1. Eine WireGuard-Konfiguration offline ablegen. Siehe Weg 4.

  2. Zugang zur Anbieterkonsole prüfen — einmal einloggen, bevor man es braucht. Ein vergessenes Passwort ist im Ernstfall ein zweiter Ausfall.

  3. Dem Dienstbenutzer den conntrack-Griff erlauben, damit das Update ihn selbst ausführen kann. In /etc/sudoers.d/clupilot-conntrack:

    clupilot ALL=(root) NOPASSWD: /usr/sbin/conntrack -D -p udp --dport 51820
    

    Genau dieser eine Aufruf, nicht conntrack allgemein.

  4. conntrack installieren, falls nicht vorhanden: sudo apt-get install -y conntrack.


Was inzwischen NICHT mehr passieren sollte

Fünf Ursachen sind abgestellt. Wer sie trotzdem sieht, hat einen Server auf altem Stand.

  • Bis v1.4.3 hing wg0 am Leben des Warteschlangen-Arbeiters: Prozess 1 des Containers war php artisan queue:work. Ein Zeitüberlauf, ein fataler Fehler oder ein Speicherlimit beendete den Arbeiter, damit den Container, damit den Tunnel. Das las sich von außen als „das VPN ist unzuverlässig". Seit v1.4.4 läuft der Arbeiter in einer Schleife neben dem Tunnel.
  • v1.4.2 gab dem Tunnel-Container einen Netz-Alias. Ein Alias ist Teil der Netzkonfiguration, und Compose baut einen Container neu, sobald die sich ändert — mit allem, was oben unter „gemerkte Ströme" steht. Seit v1.4.4 ist der Alias weg; nginx spricht die Terminal-Brücke unter dem Dienstnamen des Nachbarn an.
  • Bis v1.4.5 startete jedes Update queue-provisioning neu und riss damit jede Sitzung ab, auch wenn sich nur PHP-Code geändert hatte. Seit v1.4.6 bekommt der Arbeiter ein Signal und wird von seiner Schleife neu gestartet, während der Container steht. Dasselbe gilt seither für den Knopf „Dienste neu starten" in der Konsole — der lief über denselben Befehl.
  • Bis v1.4.7 wohnte wg0 im Container des Provisionierungs-Arbeiters, also im Abbild clupilot-app. Das wird bei fast jeder Freigabe neu gebaut, und ein neu gebauter Container bekommt eine neue Adresse — mit allem, was oben unter „gemerkte Ströme" steht. Seit v1.4.8 gehört der Namensraum einem eigenen Container vpn-hub mit eigenem, selten wechselndem Abbild. Nachgemessen: App-Abbild neu gebaut, Arbeiter und Brücke neu erzeugt — der Hub blieb derselbe Container, wg0 und beide Zugänge unangetastet.
  • Bis v1.4.8 bekam der Hub bei jedem Neubau die nächste freie Adresse im Compose-Netz. Genau darauf zeigen die gemerkten UDP-Ströme — eine neue Adresse machte sie alle ungültig. Seit v1.4.9 hat er eine feste. Nachgemessen: 172.18.0.240 vor und nach up -d --force-recreate vpn-hub.

Erzwungen durch tests/Feature/DeploymentRunsAsTheAppUserTest.php — der Container darf nicht in der Neustart-Liste stehen, und der conntrack-Griff muss im Skript bleiben.