Zum Hauptinhalt springen
  1. Beiträge/
  2. Kleines Homelab/
  3. k3s-prod: Kubernetes Cluster Konfiguration und Anwendungsbereitstellung/

Headscale

··719 Wörter· ·
Inhaltsverzeichnis

Headscale
#

Headscale ist eine quelloffene, selbst gehostete Implementierung des Tailscale Control Servers. Es ermöglicht Ihnen, Ihr eigenes Tailscale-Netzwerk zu betreiben und somit die vollständige Kontrolle über Ihre Knoten und Daten zu haben.

Quelle
#

Das Projekt basiert auf der offiziellen Headscale-Website. Das Helm-Chart stammt aus der OCI-Registry codeberg.org/wrenix/helm-charts.

Dokumentation
#

Funktion
#

Dieser Dienst bietet einen selbst gehosteten Tailscale Control Server, der die Verwaltung von Tailscale-Knoten und die Einrichtung eines privaten Tailscale-Netzwerks ermöglicht. Er unterstützt die Integration mit Cert-Manager für TLS-Zertifikate und MetalLB für LoadBalancer-IPs.

Lokale Anpassungen
#

  • Die values.yaml-Datei konfiguriert den Headscale-Server, einschließlich der Listen-Adressen, Server-URL und DNS-Einstellungen.
  • Die Persistenz läuft über die StorageClass longhorn (siehe storage.yaml).
  • Es sind Patches für den DERP-Dienst enthalten, um Dual-Stack (IPv4/IPv6)-Unterstützung zu ermöglichen.
  • TLS-Zertifikate werden über Cert-Manager verwaltet.
  • Private Schlüssel für WireGuard, Noise und DERP werden als Secrets generiert.

Wichtige Einstellungen
#

  • Server URL: Die öffentliche URL des Headscale-Servers steht als server_url in values.yaml. Sie muss exakt der Adresse entsprechen, unter der die Knoten den Server erreichen — ein späterer Wechsel zwingt jeden Knoten zur Neuanmeldung.
  • Datenbank: Headscale verwendet eine SQLite-Datenbank, die auf einem persistenten Volume gespeichert wird.
  • Zertifikate: TLS-Zertifikate für den Ingress werden automatisch von cert-manager ausgestellt.
  • DNS: Knoten im Tailnet erhalten Namen unterhalb der in base_domain gesetzten Domain. Sie darf sich nicht mit einer real aufgelösten Domain überschneiden.
  • Adress-Präfixe: fd7a:115c:a1e0::/48 (v6) und 100.64.0.0/23 (v4) — nicht auf ein eigenes Präfix ändern. Headscale unterstützt nur Teilmengen der Standardbereiche (100.64.0.0/10, fd7a:115c:a1e0::/48) und warnt bei allem anderen beim Start mit „This is NOT a supported configuration". Der Grund ist praktisch: Seit Headscale 0.29.0 (PR #3036) expandiert * als src/dst in der ACL auf genau diese Standardbereiche statt auf 0.0.0.0/0/::/0. Liegt das eigene v6-Präfix außerhalb, enthält der kompilierte Paketfilter keine passende v6-Quelle und jedes IPv6-Paket wird am Ingress des Subnet-Routers verworfen — bei v4 fällt es nicht auf, solange das Präfix eine Teilmenge von 100.64.0.0/10 ist. Hier doppelt abgesichert, da die ACL zusätzlich group:/tag: statt * verwendet. Im CDP-Cluster war mit fd72:a312:5995::/48 und "src":["*"] genau dieser Ausfall eingetreten; dort wurde am 31.08.2026 auf fd7a:115c:a1e0:2::/64 migriert. Getestet mit Headscale 0.29.3.
  • ACL-Policy: Liegt in der Datenbank, nicht in der Konfigurationsdatei (policy.mode: database). Änderungen an config-patch.yaml werden durch ArgoCD zwar ins Secret synchronisiert, aber nicht wirksam — dafür ist ein expliziter headscale policy set nötig. Siehe ACL-Policy setzen.
  • DERP: Es werden die öffentlichen DERP-Server von Tailscale verwendet. Ein eigener DERP-Service ist konfiguriert, aber nicht als Server aktiviert. Der STUN-Dienst läuft über einen LoadBalancer-Service.
  • Speicher: Konfiguration und SQLite-Datenbank liegen auf einem Longhorn-Volume. Diese Datenbank ist der gesamte Zustand des Tailnets — geht sie verloren, müssen sich alle Knoten neu anmelden.

ACL-Policy setzen
#

policy.mode steht auf database: Die wirksame Policy liegt in der Headscale-Datenbank, nicht in der Konfigurationsdatei. config-patch.yaml patcht das Secret headscale, das im Pod als /etc/headscale/policy.json gemountet wird — Headscale liest diese Datei aber weder beim Start noch bei einem Neustart ein.

Ein Commit an config-patch.yaml ändert am laufenden System also nichts. ArgoCD meldet Synced, das Secret ist aktuell, die Datei im Pod ist aktuell — und die Policy im Cluster ist trotzdem die alte. Das Repo ist für die ACL nicht die Wahrheit.

Nach jeder Änderung an config-patch.yaml ist deshalb dieser Schritt nötig:

1kubectl exec -n headscale deploy/headscale -- headscale policy set -f /etc/headscale/policy.json

Vorher sicherstellen, dass ArgoCD das Secret bereits synchronisiert hat — sonst schreibt der Befehl den alten Stand erneut in die Datenbank. Der Pod muss dafür nicht neu gestartet werden.

Wirksamen Stand ansehen:

1kubectl exec -n headscale deploy/headscale -- headscale policy get

Auf Drift prüfen — die Ausgabe von policy get gegen den policy.json-Block in config-patch.yaml halten. Weichen sie ab, wurde eine Änderung committet, aber nie gesetzt.

Verifizieren, dass die Clients die neue Policy haben. Der kompilierte Paketfilter erscheint binnen Sekunden auf den Knoten; ein Subnet-Router eignet sich am besten dafür:

1kubectl exec -n wireguard deploy/wireguard-tailscale -c tailscale -- tailscale debug netmap | jq ".PacketFilter[].Srcs"

Installation
#

Die Anwendung wird mittels Kustomize und Helm durch ArgoCD im Kubernetes-Cluster bereitgestellt. Die Konfiguration befindet sich im apps/headscale-Verzeichnis. Eine manuelle Installation kann mit folgendem Befehl durchgeführt werden:

1kubectl kustomize --enable-helm apps/headscale | kubectl apply -n headscale -f -

Abhängigkeiten
#

  • Ein laufender Kubernetes-Cluster.
  • Ein Ingress-Controller (z.B. Traefik) für den externen Zugriff.
  • Eine Zertifikatsmanagement-Lösung (z.B. cert-manager) zur Bereitstellung von TLS-Zertifikaten.
  • MetalLB für LoadBalancer-IP-Zuweisung.
  • Longhorn als Storage-Provider für die PersistentVolumes.
Fabrice Kirchner
Autor
Fabrice Kirchner
stolzer Vater, Nerd, Admin