VPN Standort C #
Verbindet den Cluster als WireGuard-Client mit dem WireGuard-Server im CDP-Cluster (vpn.casa-due-pur.de). Die Adressen des dortigen Tailnets (100.64.2.0/23, fd7a:115c:a1e0:2::/64) werden über Headscale im eigenen Tailnet erreichbar.
Nicht zu verwechseln mit apps/wireguard: dort läuft ein WireGuard-Server mit Web-Portal für Mitarbeiter-Zugänge. Die Schwester-Apps apps/vpn-site-a (WireGuard) und apps/vpn-site-b (OpenVPN) setzen dasselbe Muster für andere Standorte um.
Funktion #
1zyria-Tailnet-Client
2 └─> Pod vpn-site-c (zyria-Cluster)
3 ├── tailscale0 (Subnet Router, annonciert das CDP-Tailnet)
4 └── wg0 (MASQUERADE auf 10.26.15.1)
5 └─> vpn.casa-due-pur.de:51194 (CDP-Cluster, apps/wireguard)
6 wg0 ──> tailscale0 (Gateway-Container, MASQUERADE)
7 └─> CDP-Tailnet-Node 100.64.2.x- wireguard:
linuxserver/wireguardim Client-Modus. Sobald unter/config/wg_confseine*.confliegt, führt der Entrypoint lediglichwg-quick up wg0aus und startet keinen Server. - tailscale: meldet sich mit einem Pre-Auth-Key am zyria-Headscale an und annonciert die CDP-Tailnet-Bereiche über
TS_ROUTES. - coredns: beantwortet die Zone
hsn.casa-due-pur.defür das eigene Tailnet und holt die Antworten durch den Tunnel beim CDP-Gateway, siehe Namensauflösung. - Gegenstelle: Auf der CDP-Seite reicht der Container
tailscaleim Controllermainvonapps/wireguarddie Pakete ins CDP-Tailnet weiter. Ohne ihn endet der Verkehr am WireGuard-Server. Die Freigabe ist dort auf die Tunnel-IP dieses Peers beschränkt (GW_PEERS_V4/GW_PEERS_V6).
Warum ein Pod mit zwei Containern #
Container eines Pods teilen sich den Netzwerk-Namespace. Dadurch sieht tailscaled das von wg-quick angelegte Interface wg0 samt Routen und kann Traffic aus dem Tailnet direkt in den Tunnel weiterleiten. Mit zwei getrennten Deployments wäre das nicht möglich, der Tunnel läge in einem fremden Namespace.
Ein Init-Container setzt net.ipv4.ip_forward und net.ipv6.conf.all.forwarding. Diese Sysctls sind netzwerk-namespaced und betreffen daher nur diesen Pod, nicht den Node.
Besonderheiten dieser Anbindung #
Das Ziel ist hier ein anderes Tailnet und kein RFC1918-Netz. Daraus ergeben sich drei Abweichungen von vpn-site-a.
--netfilter-mode=off statt --snat-subnet-routes. Im Standardmodus installiert tailscaled in ts-forward die Regel -s 100.64.0.0/10 -o tailscale0 -j DROP. Die Antworten aus dem CDP-Tailnet kommen über wg0 mit einer Quelle aus 100.64.2.0/23 und würden genau dort verworfen: Der Hinweg funktioniert, jede Antwort verschwindet. Eine vorgeschaltete ACCEPT-Regel hält nicht, weil tailscaled seine Hooks bei jedem Neustart wieder an Position 1 setzt. Das Maskieren auf die Tunnel-IP übernimmt deshalb PostUp in wg/wg0.conf.
IPv6-Policy-Regel. tailscaled legt fd7a:115c:a1e0::/48 in Routing-Tabelle 52 ab. Deren Regel (Priorität 5270) greift vor main. Das CDP-Netz fd7a:115c:a1e0:2::/64 liegt in diesem Bereich und liefe ohne Gegenmaßnahme zurück in tailscale0. PostUp setzt deshalb ip -6 rule add to fd7a:115c:a1e0:2::/64 lookup main priority 5200. Für IPv4 ist das nicht nötig: Tabelle 52 enthält dort nur die /32 der eigenen Nodes aus 100.64.0.0/23.
Kein Adresskonflikt, aber enge Nachbarschaft. Das eigene Tailnet nutzt 100.64.0.0/23 und fd7a:115c:a1e0::/48 mit sequentieller Vergabe aus fd7a:115c:a1e0::/64. Würde eines der beiden Headscale-Präfixe erweitert oder verschoben, kollidiert es mit dem CDP-Tailnet.
Konfiguration #
wg/wg0.conf: Peer „Fabrice Zuhause“ aus wg-portal aufvpn.casa-due-pur.de, ergänzt umPostUp/PostDown. Die vom Portal generierteDNS-Zeile muss bei jedem Neu-Export wieder entfernt werden.TS_ROUTESinvalues.yaml: identisch mitAllowedIPsinwg0.conf, bis auf10.26.15.112/32. Diese Adresse braucht nur der DNS-Forwarder, sie darf nicht ins Tailnet annonciert werden.TS_AUTHKEYinkustomization.yaml: Pre-Auth-Key mit Forced Tagtag:site-c, siehe unten.
Der Key wird nur beim ersten Start verbraucht. Danach liegt der Node-State im Secret vpn-site-c-tailscale-state, das tailscaled selbst pflegt (siehe rbac.yaml).
Fallstricke #
AllowedIPsniemals0.0.0.0/0.wg-quickbaut dann Policy-Routing mitfwmarkauf und kappt die Verbindung des Pods zum Cluster-Netz und zu Headscale.- Kein
DNS = ...inwg0.conf.wg-quickruft damitresolvconfauf und überschreibt/etc/resolv.conffür alle Container des Pods.tailscaledkann danachkubernetes.default.svcnicht mehr auflösen und bricht beim Laden seines State-Secrets ab. Die geänderte Datei überlebt Container-Neustarts, erst ein neuer Pod behebt es. PostUp-Befehle idempotent halten.wg-quickführt Hooks mitset -eaus. Ein Befehl, der beim zweiten Start fehlschlägt, reißt den Tunnel ab.TS_ACCEPT_ROUTESbleibtfalse. Sonst übernimmttailscaleddie Subnet-Routen des eigenen Tailnets in Tabelle 52, darunter159.69.238.80/28vom Nodewireguard. Tabelle 52 greift vormain, und der WireGuard-Endpoint159.69.238.91liegt in genau diesem Netz: Die Tunnelpakete liefen dann selbst durch das Tailnet und hingen an einem fremden Router. Dasselbe gilt für den Gateway-Container auf cdp-Seite.TS_ACCEPT_DNSbleibtfalse. Headscale ist mitoverride_local_dnskonfiguriert; ein Router-Pod, der die Tailnet-Nameserver übernimmt, kann seinen eigenen Control-Server nicht mehr auflösen.TS_ROUTESohne Leerzeichen nach dem Komma. Tailscale splittet nur an,und übergibt das Ergebnis ungetrimmt annetip.ParsePrefix.
Zugriffskontrolle #
Der Node trägt tag:site-c als Forced Tag vom Pre-Auth-Key, nicht über --advertise-tags:
1kubectl -n headscale exec deploy/headscale -- \
2 headscale preauthkeys create --user <user> --reusable --tags tag:site-c --expiration 8760hServerseitig gesetzt ist der Tag autoritativ und vom Client nicht überschreibbar. Schickt der Client zusätzlich --advertise-tags, prüft Headscale das gegen tagOwners und lehnt die Registrierung ab, solange die Policy den Tag nicht kennt.
In apps/headscale/config-patch.yaml stehen dafür:
group:site-cmit einer Regel auf100.64.2.0/23:*undfd7a:115c:a1e0:2::/64:*. Subnetz-Routen werden über die Ziel-CIDR gematcht, nicht über den Tag des Routers.tag:site-cintagOwners.autoApproversfür beide Präfixe auftag:site-c.
Die Zugriffskontrolle ist zweistufig: Die zyria-ACL entscheidet, wer den Tunnel benutzen darf. Die CDP-ACL entscheidet, was der Gateway-Node dort erreicht. Wegen der doppelten Maskierung sieht die CDP-Seite jeden Zugriff als Verkehr des Gateway-Nodes. Eine Unterscheidung nach zyria-Benutzer ist dort nicht möglich.
Namensauflösung für das CDP-Tailnet #
Die MagicDNS-Namen des CDP-Tailnets (*.hsn.casa-due-pur.de) kennt nur dessen Headscale. Ein CoreDNS im Pod holt sie live durch den Tunnel:
1zyria-Client ──> 100.100.100.100 (Split-DNS hsn.casa-due-pur.de) ──> 100.64.0.12:53
2 └─> coredns im Pod vpn-site-c
3 └─> wg0 ──> 10.26.15.112:53 (wg0-Adresse des CDP-WireGuard-Servers)
4 └─> DNAT im Gateway-Container ──> 100.100.100.100 (MagicDNS des CDP-Gateways)- Die Zone und die Zieladresse stehen in
dns.nameservers.splitvonapps/headscale/values.yaml. Freigegeben ist der Zugriff auftag:site-c:53nur fürgroup:site-c. 10.26.15.112/32steht inAllowedIPs, damit CoreDNS den Server durch den Tunnel erreicht. InTS_ROUTESfehlt die Adresse bewusst.- Auf CDP-Seite leitet der Gateway-Container von
apps/wireguardAnfragen anGW_DNS_V4:53auf sein MagicDNS weiter. Die umgeleiteten Pakete durchlaufen dieselbe Sperrewg-gateway: Nur die freigeschalteten Peers bekommen Antworten. - Neue oder umbenannte CDP-Nodes sind sofort auflösbar, höchstens verzögert um den Cache von 120 Sekunden. Es gibt nichts zu pflegen.
- Ohne Tunnel liefert die Zone
SERVFAIL. Einen öffentlichen Fallback gibt es nicht, weil die Namen nur im CDP-Tailnet existieren. - Kurznamen wie
winmgrfunktionieren nicht, nur vollständige Namen. Eine Suchdomain würde mit gleichnamigen Nodes im eigenen Tailnet kollidieren (addc,wireguard,rustdesk).
Prüfen:
1kubectl -n vpn-site-c exec deploy/vpn-site-c -c wireguard -- nslookup winmgr.hsn.casa-due-pur.de 127.0.0.11dig +short winmgr.hsn.casa-due-pur.de @100.100.100.100Endpoint mit dynamischer IP #
WireGuard löst den Endpoint-Hostnamen ausschließlich beim wg-quick up auf und danach nie wieder. Ändert sich die Adresse der Gegenstelle, sendet der Tunnel dauerhaft an die alte Adresse.
Ein Sidecar-Container prüft deshalb alle 30 Sekunden das Alter des letzten Handshakes. Überschreitet es 180 Sekunden, löst er den Namen neu auf und setzt den Endpoint per wg set wg0 peer <pubkey> endpoint <name:port> nach. Der Sidecar nutzt dasselbe Image wie der Hauptcontainer und läuft mit readOnlyRootFilesystem und nur NET_ADMIN.
1[STALE] Handshake 187s alt, Endpoint aktuell <alte-adresse>
2[FIXED] Endpoint neu aufgeloest: <alte-adresse> -> <neue-adresse>Betrieb #
Der Pod läuft mit replicas: 1 und strategy: Recreate. Zwei gleichzeitig laufende Pods würden sich Tunnel-IP und Headscale-Node streitig machen.
Die Readiness-Probe prüft das Alter des letzten Handshakes (Schwelle 150 Sekunden), nicht dessen bloße Existenz. Die Liveness-Probe prüft nur, ob wg0 existiert. Das Nachfassen bei veraltetem Handshake übernimmt der Sidecar ohne Neustart.
1kubectl -n vpn-site-c exec deploy/vpn-site-c -c wireguard -- wg show
2kubectl -n vpn-site-c exec deploy/vpn-site-c -c tailscale -- tailscale status
3kubectl -n vpn-site-c exec deploy/vpn-site-c -c wireguard -- ip -6 rule
4kubectl -n vpn-site-c exec deploy/vpn-site-c -c wireguard -- iptables -t nat -S POSTROUTINGInstallation #
Die Anwendung wird mittels Kustomize und Helm bereitgestellt.
1kubectl kustomize --enable-helm apps/vpn-site-c | kubectl apply -f -Abhängigkeiten #
- Laufender Headscale (
apps/headscale) mit den Policy-Einträgen fürtag:site-c. - Auf der CDP-Seite:
apps/wireguardmit Gateway-Container und dieser Peer-IP inGW_PEERS_V4/GW_PEERS_V6. - Node-Kernel mit WireGuard-Unterstützung;
/lib/moduleswird read-only eingehängt. /dev/net/tunauf dem Node für den Kernel-Modus vontailscaledundwg-quick.- Ausgehend UDP
51194zuvpn.casa-due-pur.de.
Offene Punkte #
Der private WireGuard-Schlüssel und der Headscale-Pre-Auth-Key liegen im Klartext im Repository. Das ist konsistent mit dem restlichen Repo, bleibt aber die offene Security-Aufgabe (SOPS oder Sealed Secrets), die auch in .pre-commit-config.yaml vermerkt ist.