In dieser Anleitung zeige ich, wie man Headscale mit Docker, Caddy, Cloudflare DNS Plugin und Headplane WebUI installiert. Headscale ist eine selbstgehostete Open-Source-Implementierung des Tailscale-Kontrollservers. Tailscale ist ein benutzerfreundliches VPN, das auf WireGuard basiert und eine einfache, sichere Netzwerkverbindung zwischen Geräten ermöglicht, ohne komplexe Konfigurationen. Es nutzt eine Zero-Trust-Architektur, um den Netzwerkverkehr zu sichern und erleichtert den Zugriff auf Ressourcen, unabhängig vom Standort der Benutzer. Zusätzlich integrieren wir in dieser Anleitung die Headplane-Web-UI und richten eine OIDC-Authentifizierung (OpenID Connect) ein. Außerdem zeige ich, wie ihr Cloudflare DNS verwendet, um lokale SSL-Zertifikate mithilfe der DNS-01-Challenge zu erzeugen.
Datum | Änderungen |
---|---|
14.10.2024 | Erstellung dieser Anleitung |
16.10.2024 | Syntax Fehler im Caddyfile Danke @pkirsche |
21.10.2024 | DERP Server aktiviert Danke @phil3741 |
31.10.2024 | ACL aktiviert Danke @phil3741 |
05.11.2024 | Headplane Docker image v0.3.3 angepasst |
1. Grundvoraussetzung
- Docker & Docker Compose v2 (Debian / Ubuntu)
- Ein VPS mit fester IP und diese sollte per A-Record auf zwei Subdomains zeigen (FQDN)
- Port 80, 443, 443udp offen + SSH
- Cloudflare Account (free Tier)
- Pocket ID Optional aber klar empfohlen
2. Vorwort zur 2. Version dieses Setups
Mit dieser Anleitung möchte ich euch mein aktuelles Setup vorstellen – eine Lösung, die für mich im Homelab-Bereich als “Best Practice” gilt. Es gibt sicher viele andere Wege, ein ähnliches Ziel zu erreichen, aber dieses Setup hat sich für mich als besonders effizient und sicher erwiesen.
Ein zentrales Anliegen bei meinem Homelab ist die Sicherheit. Daher halte ich alle Ports an meinem Router (Fritz!Box) geschlossen. Das bedeutet, dass keine Dienste direkt von außen zugänglich sind. Ich benutze Proxmox für die Virtualisierung und Docker Compose für das verwalten der Container, wobei diese ausschließlich auf einer lokalen VM laufen.
Die Herausforderung besteht darin, diese Dienste auch von unterwegs sicher zu erreichen. Dafür habe ich Headscale und Caddy auf einem Cloud-VPS (bei Ionos, für nur 1 € pro Monat) eingerichtet. Diese Kombination erlaubt mir, über ein Cloudflare-DNS-Plugin meine Docker-Container auch von außerhalb mit SSL-Zertifikaten zu erreichen – und das nur über eine VPN-Verbindung. Für mich war es dabei besonders wichtig, dass die Dienste wirklich nur über VPN zugänglich sind.
Zusätzlich betreibe ich einen separaten VPS bei Hetzner für die wenigen Dienste, die ich öffentlich zugänglich machen möchte. Auf diesem VPS läuft Traefik in Kombination mit CrowdSec, um diese Dienste sicher ins Internet zu stellen.
In dieser Anleitung soll es vor allem darum gehen, wie ihr Headscale mit Caddy und der DNS-01-Challenge für SSL-Zertifikate konfigurieren könnt – eine Lösung, die sich für mich als ideal herausgestellt hat.
Ein besonderer Schritt in dieser Konfiguration ist die Integration von OIDC (OpenID Connect) mit Pocket ID. Durch die Verwendung von OIDC könnt ihr eine zusätzliche Sicherheitsebene einbauen, indem ihr den Zugriff auf die Admin-Oberfläche und andere sensible Bereiche durch eine externe Authentifizierungslösung steuert.
So sieht es dann aus:
3. Domain konfigurieren
Als Erstes kümmern wir uns darum, dass die Domain korrekt konfiguriert ist. Falls ihr noch keinen Cloudflare Account habt, legt euch einen an. Wichtig zu wissen: Die Domain muss nicht zwingend bei Cloudflare selbst registriert sein. Wenn sie bei einem anderen Anbieter liegt, müssen wir lediglich die Nameserver auf die Cloudflare-Nameserver anpassen.
Das Vorgehen, um die Nameserver zu ändern, variiert je nach Domain-Registrar. Aber ich gehe davon aus, dass diejenigen, die sich an diese Anleitung herantrauen, auch damit zurechtkommen. Beachtet bitte, dass es bis zu 48 Stunden dauern kann, bis die DNS-Nameserver vollständig aktualisiert sind.
Ihr könnt den Status schnell überprüfen, indem ihr den folgenden Befehl ausführt:
# Ersetze domain.com mit deiner Domain dig domain.com NS
Ausgabe sollte dann die Cloudflare Nameserver anzeigen.
Wenn die Nameserver korrekt auf Cloudflare zeigen, könnt ihr mit dem Einrichten fortfahren.
Loggt euch in euren Cloudflare-Account ein. Oben seht ihr die Option “Website hinzufügen” – klickt darauf und fügt eure Domain hinzu.
Gebt eure Domain ein und bestätigt das Hinzufügen.
Wie schon erwähnt, reicht der Free-Tier-Plan von Cloudflare vollkommen aus, um unser Ziel zu erreichen. Damit habt ihr nun die Grundlage, um Cloudflare als DNS-Anbieter zu nutzen und den nächsten Schritt anzugehen.
Nachdem ihr die DNS-Einträge für eure Subdomains eingerichtet habt, müsst ihr den Cloudflare-Wizard abschließen, um die volle Kontrolle über eure DNS-Einträge zu erhalten.
Arbeitet euch Schritt für Schritt durch den Wizard:
- Bestätigt, dass die Nameserver korrekt auf Cloudflare zeigen.
- Überprüft die DNS-Einträge, die automatisch übernommen oder von euch manuell hinzugefügt wurden.
- Konfiguriert weitere Einstellungen, falls erforderlich (für unser Setup reicht jedoch in der Regel die Standardkonfiguration aus).
Sobald ihr den Wizard vollständig durchlaufen habt, solltet ihr die Verwaltung eurer DNS-Einträge komplett über Cloudflare übernehmen können. Damit habt ihr die Basis geschaffen, um eure Subdomains sauber zu steuern und zu verwalten.
3.1 DNS API Token erstellen
Nun erstellen wir ein DNS API Token, das wir später für die automatisierte Verwaltung der DNS-Einträge benötigen.
Dazu klickt ihr oben rechts auf euer Profilbild bzw. eure E-Mail-Adresse und wählt im Menü API-Token aus. Anschließend klickt ihr auf Token erstellen. Im nächsten Schritt scrollt ihr nach unten und wählt Benutzerdefiniertes Token erstellen aus.
Für unser Setup sind die folgenden Berechtigungen erforderlich:
- Zone > Zone > Lesen
- Zone > DNS > Bearbeiten
Diese Berechtigungen geben dem Token die nötigen Rechte, um die DNS-Einträge zu lesen und zu ändern, ohne zu viele unnötige Freiheiten zu gewähren.
Sobald ihr das Token erstellt habt, notiert euch dieses sicher, da wir es im weiteren Verlauf der Anleitung brauchen werden.
3.2 Subdomains erstellen
Für unser Setup benötigen wir zwei Subdomains, die beide per A-Record auf dieselbe IP-Adresse zeigen. In diesem Beispiel nennen wir sie headscale.fqdn.de und hs.fqdn.de. Diese Subdomains werden später für den Zugriff auf Headscale genutzt.
Geht dafür ins Cloudflare Dashboard und wählt eure Domain aus. Navigiert anschließend zum Reiter DNS und fügt dort die notwendigen DNS Records hinzu:
- Klickt auf “Record hinzufügen”.
- Wählt A als Record-Typ aus.
- Tragt bei Name die erste Subdomain ein, z.B.
headscale
. - Bei Inhalt gebt ihr die IP-Adresse eures VPS an.
- Wiederholt diesen Vorgang für die zweite Subdomain, also
hs
.
3.2.1 Warum wir zwei Subdomains verwenden und welche Funktionen sie erfüllen.
Die erste Subdomain, hs.fqdn.de, dient als Control-Server-Domain. Über diese Domain wird nicht nur die Kommunikation mit den Clients abgewickelt, sondern sie ist auch der Zugangspunkt für das Admin-Dashboard. Wenn ihr das Dashboard aufrufen möchtet, ist dies unter hs.fqdn.de/admin erreichbar.
Die zweite Subdomain, headscale.fqdn.de, ist für einen speziellen Zweck vorgesehen: den Magic DNS Server. Magic DNS ermöglicht es, Geräte in eurem Tailnet einfacher über benutzerfreundliche Namen statt IP-Adressen anzusprechen, was die Verwaltung und den Zugriff auf die Geräte erheblich erleichtert. Weitere Details dazu findet ihr direkt in der offiziellen Dokumentation.
Mit diesen beiden Subdomains habt ihr also eine klare Trennung: Eine für die Verwaltung und Steuerung und eine für den DNS-Dienst.
4. Container Setup
Im nächsten Schritt legen wir die notwendige Ordnerstruktur an und erstellen unser eigenes Caddy Docker Image, da wir das Cloudflare-DNS-Plugin integrieren müssen, um später SSL-Zertifikate über die DNS-01-Challenge zu erhalten. Zusätzlich nehmen wir auch die Konfiguration von Headscale vor.
4.1 Ordnerstruktur erstellen
mkdir -p /opt/containers/headscale/{headscale/{config,data},caddy/{config,data}}
4.2 Caddyfile erstellen
Um das Caddyfile zu bearbeiten, öffnet ihr es mit:
nano /opt/containers/headscale/caddy/Caddyfile
Tragt die folgende Konfiguration ein:
https://hs.fqdn.de { reverse_proxy /admin* http://headplane:3000 reverse_proxy * http://headscale:8080 # HSTS aktivieren, um HTTPS zu erzwingen (max-age: 1 Jahr, inklusive Subdomains) header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" } } # Service: DNS-Test https://webserver.fqdn.de { # Reverse Proxy zur internen Headscale-IP reverse_proxy http://100.64.0.2:9999 # HSTS aktivieren, um HTTPS zu erzwingen (max-age: 1 Jahr, inklusive Subdomains) header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" } # Cloudflare E-Mail und API token tls deine@email.de { dns cloudflare {env.CLOUDFLARE_API_TOKEN} } }
4.2.1 Anpassungen
- Passe deine Domain an: Ersetze
https://hs.fqdn.de
durch deine Headscale-Domain. - Passe die Webserver-Domain an: Ersetze
https://webserver.fqdn.de
durch deine Webserver-Domain. Wird in Schritt 9. klarer (dient erstmal als Platzhalter) - Interne Headscale-Domain: Passe die interne Headscale-IP oder Domain an (dies kommt später in der Konfiguration).
- Email anpassen: Ersetze
deine@email.de
durch deine eigene Email-Adresse, die du für TLS-Zertifikate verwenden möchtest.
4.3 Docker Compose Datei
Bearbeite die Docker Compose-Datei:
nano /opt/containers/headscale/docker-compose.yml
Tragt die folgende Konfiguration ein:
--- services: headscale: image: headscale/headscale:0.23.0 #Pinned!!! container_name: headscale command: serve restart: unless-stopped volumes: - ./headscale/config:/etc/headscale - ./headscale/data:/var/lib/headscale headplane: container_name: headplane image: ghcr.io/tale/headplane:0.3.3 restart: unless-stopped volumes: - ./headscale/data:/var/lib/headscale - ./headscale/config:/etc/headscale - ./headscale/config/config.yaml:/etc/headscale/config.yml - /var/run/docker.sock:/var/run/docker.sock:ro ports: - '3000:3000' environment: # This is always required for Headplane to work COOKIE_SECRET: 'sicher' HEADSCALE_URL: 'https://hs.fqdn.de' HEADSCALE_INTEGRATION: 'docker' HEADSCALE_CONTAINER: 'headscale' DISABLE_API_KEY_LOGIN: 'false' HOST: '0.0.0.0' PORT: '3000' CONFIG_FILE: '/etc/headscale/config.yml' # Only set this to false if you aren't behind a reverse proxy COOKIE_SECURE: 'true' # DEBUG: 'true' # This NEEDS to be set with OIDC, regardless of what's in the config # This needs to be a very long-lived (999 day) API key used to create # shorter ones for OIDC and allow the OIDC functionality to work ROOT_API_KEY: 'auchsicher' # Overrides the configuration file values if they are set in config.yaml # If you want to share the same OIDC configuration you do not need this #OIDC_CLIENT_ID: 'client-id' #OIDC_ISSUER: 'https://pocket.id' #OIDC_CLIENT_SECRET: '123456' caddy: build: context: . dockerfile: Dockerfile restart: unless-stopped ports: - "80:80" - "443:443" - "443:443/udp" environment: - ACME_AGREE=true - CLOUDFLARE_API_TOKEN=dein-api-token - CLOUDFLARE_EMAIL=deinecloudflare@mail.de volumes: - ./caddy/config:/config - ./caddy/data:/data - ./caddy/Caddyfile:/etc/caddy/Caddyfile
- COOKIE_SECRET: ‘sicher’
- HEADSCALE_URL: ‘https://hs.fqdn.de’
- ROOT_API_KEY: ‘auch-sicher’
- **CLOUDFLARE_API_TOKEN= aus Schritt 3.1
- **CLOUDFLARE_EMAIL=
4.4 Dockerfile erstellen
nano /opt/containers/headscale/Dockerfile
# Dockerfile zur Erstellung eines benutzerdefinierten Caddy-Images mit Cloudflare DNS-Plugin # Stage 1: Build Caddy mit Cloudflare-Plugin FROM caddy:builder AS builder # Hinzufügen des Cloudflare DNS-Plugins zu Caddy RUN xcaddy build \ --with github.com/caddy-dns/cloudflare # Stage 2: Verwende das offizielle Caddy-Image FROM caddy:latest # Kopiere das Caddy-Binary aus der Build-Phase COPY --from=builder /usr/bin/caddy /usr/bin/caddy
4.5 Headscale Config
Um die aktuelle Headscale-Konfigurationsdatei zu erhalten, laden wir die Vorlage direkt aus dem Headscale GitHub-Repository herunter. Diese Datei wird als Basis dienen, um die notwendigen Anpassungen für unser Setup vorzunehme
sudo curl -o /opt/containers/headscale/headscale/config/config.yaml https://raw.githubusercontent.com/juanfont/headscale/refs/heads/main/config-example.yaml
Nun hast du die aktuelle Headscale-Konfigurationsdatei auf deinem Server gespeichert und kannst sie für deine Anforderungen anpassen.
nano /opt/containers/headscale/headscale/config/config.yaml
- *server_url: https://hs.fqdn.de
- listen_addr: 0.0.0.0:8080
- base_domain: headscale.fqdn.de
- oidc: ist Optional <- gehe ich später drauf ein
4.5.1 Derp Server (optional)
Was ist ein DERP-Server?
Ein DERP-Server (Designated Encrypted Relay for Packets) ist ein spezieller Server, der in Headscale und Tailscale verwendet wird, um Verbindungen zwischen Geräten herzustellen, insbesondere bei Netzwerkkonfigurationsproblemen wie NAT-Traversal. DERP-Server übernehmen zwei Hauptaufgaben:
- Direkte Verbindungsverhandlung: Sie helfen bei der Aushandlung direkter Verbindungen zwischen Geräten innerhalb eines Tailnets (eines privaten Netzwerks in Tailscale).
- Relay-Server: Wenn eine direkte Verbindung aufgrund von Netzwerkeinschränkungen wie Firewalls oder strikten NAT-Typen nicht möglich ist, agiert der DERP-Server als Relay und leitet den verschlüsselten Datenverkehr zwischen den Geräten weiter.
Wichtig ist, dass DERP-Server Daten nur weiterleiten, ohne sie entschlüsseln zu können. Die Verschlüsselung erfolgt mithilfe des WireGuard-Protokolls, und die privaten Schlüssel verbleiben immer auf den Endgeräten.
DERP-Server ist optional
In der Regel benötigen Geräte nur kurz einen DERP-Server, um die direkte Verbindung zu einem anderen Gerät zu initialisieren. Sobald diese direkte Verbindung steht, wird der DERP-Server nicht mehr verwendet. Daher ist ein DERP-Server in vielen Netzwerken optional. In Netzwerken mit guter Konnektivität oder wenn die NAT-Traversal problemlos funktioniert, kann die direkte Peer-to-Peer-Verbindung ohne Relay durch den DERP-Server hergestellt werden.
Was passiert, wenn kein DERP-Server eingerichtet wird?
Wenn du keinen eigenen DERP-Server konfigurierst, versuchen die Geräte zunächst, direkte Verbindungen zueinander herzustellen. Gelingt dies, wird kein DERP-Server benötigt. Sollte jedoch eine direkte Verbindung aufgrund von Netzwerkbeschränkungen wie strikten Firewalls oder komplexen NAT-Konfigurationen scheitern, springt ein DERP-Server als Relay ein, um den Datenverkehr zwischen den Geräten weiterzuleiten.
In der Standardkonfiguration von Tailscale und Headscale werden automatisch die offiziellen DERP-Server von Tailscale verwendet, wenn eine direkte Verbindung nicht möglich ist. Das bedeutet, dass du keine zusätzlichen Schritte unternehmen musst, um die Verbindung sicherzustellen – selbst ohne eigenen DERP-Server. Die offiziellen DERP-Server von Tailscale sind geografisch verteilt, um eine hohe Verfügbarkeit und geringe Latenz zu gewährleisten.
Tippt folgenden Befehl auf einer Node im Headscale Netzwerk ein:
tailscale netcheck
Ausgabe:
Um euren eigenen DERP-Server zu aktivieren machen wir folgendes:
nano /opt/containers/headscale/headscale/config/config.yml
[...] derp: server: # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place enabled: true # Region ID to use for the embedded DERP server. # The local DERP prevails if the region ID collides with other region ID coming from # the regular DERP config. region_id: 999 # Region code and name are displayed in the Tailscale UI to identify a DERP region region_code: "headscale" region_name: "Headscale Embedded DERP" # Listens over UDP at the configured address for STUN connections - to help with NAT traversal. # When the embedded DERP server is enabled stun_listen_addr MUST be defined. # # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/ stun_listen_addr: "0.0.0.0:3478" # Private key used to encrypt the traffic between headscale DERP # and Tailscale clients. # The private key file will be autogenerated if it's missing. # private_key_path: /var/lib/headscale/derp_server_private.key # This flag can be used, so the DERP map entry for the embedded DERP server is not written automatically, # it enables the creation of your very own DERP map entry using a locally available file with the parameter DERP.paths # If you enable the DERP server and set this to false, it is required to add the DERP server to the DERP map using DERP.paths automatically_add_embedded_derp_region: true # For better connection stability (especially when using an Exit-Node and D NS is not working), # it is possible to optionally add the public IPv4 and IPv6 address to the Derp-Map using: ipv4: 80.1.2.3.4 #Public IPv4 Adresse #ipv6: 2001:db8::1 # List of externally available DERP maps encoded in JSON urls: []
So sollte deine Config im Bereich derp
aussehen
- enabled: true
- ipv4= deine-IPv4-Adresse
- ipv6= # Ist optional, geht auch nur mit IPv4
- urls: [] (wichtig, nicht nur leer lassen sondern die eckigen Klammern setzen)
4.5.2 Docker Compose anpassen
services: headscale: image: headscale/headscale:0.23.0 #Pinned!!! container_name: headscale command: serve restart: unless-stopped volumes: - ./headscale/config:/etc/headscale - ./headscale/data:/var/lib/headscale ports: – "3478:3478/udp"
- ports: – “3478:3478/udp” eintragen
4.5.3 Firewall konfigurieren
Danach den Port 3478/udp
auch am Server öffnen.
In den VPS Firewall Einstellungen im Browser und/oder
iptables -A INPUT -p udp -m udp –dport 3478 -j ACCEPT
am Server.
Dann einmal den Server neustarten.
docker compose -f /opt/containers/headscale/docker-compose.yml down && docker compose -f /opt/containers/headscale/docker-compose.yml up -d
Jetzt nochmal
tailscale netcheck
Sollte jetzt so aussehen:
4.6 Access Control Lists
Um die ACL wie gewohnt in der Headplane-UI weiterzubearbeiten, müssen wir Folgendes tun:
nano /opt/containers/headscale/headscale/config/config.yaml
[...] policy: # The mode can be "file" or "database" that defines # where the ACL policies are stored and read from. mode: database [...]
Hier mode: database
einstellen
Container einmal neustarten.
docker compose -f /opt/containers/headscale/docker-compose.yml down && docker compose -f /opt/containers/headscale/docker-compose.yml up -d
In der Headplane UI auf ACLs https://hs.fqdn/admin/acls wechseln und folgendes Template eintragen:
{ "acls": [ { "action": "accept", "src": ["*"], "dst": ["*:*"] } ], "ssh": [ { "action": "check", "src": ["*"], "dst": ["*"], "users": ["*"] } ] }
Mit diesen Einstellungen ist nun auch Tailscale SSH möglich. Das bedeutet, ihr könnt euch per SSH auf eure Maschinen verbinden, ohne SSH-Keys austauschen zu müssen. Voraussetzung dafür ist, dass ihr euch im Tailnet befindet.
Eure SSH-Konfiguration (/etc/ssh/sshd_config
) und die Schlüsseldateien (~/.ssh/authorized_keys
) werden dabei nicht verändert. Das heißt, andere SSH-Verbindungen zum selben Host, die nicht über Tailscale laufen, bleiben weiterhin möglich.
Um Tailscale SSH zu aktivieren, führt folgenden Befehl aus:
tailscale up --ssh
5. Container starten
Nachdem du alle Konfigurationsdateien und dein benutzerdefiniertes Caddy Docker Image vorbereitet hast, ist es an der Zeit, das Setup zu bauen und auszuführen.
5.1 Navigiere zu deinem Projektverzeichnis
Das Verzeichnis sollte deine docker-compose.yml
und den Dockerfile
enthalten. Wechsle mit dem folgenden Befehl in das entsprechende Verzeichnis:
cd /opt/containers/headscale
5.2 Erstelle das benutzerdefinierte Caddy-Image mit Docker Compose
Verwende docker compose build
, um dein Caddy-Image zu bauen. Dieser Befehl nutzt den Dockerfile in deinem Projektverzeichnis, um ein Image zu erstellen, das das Cloudflare-DNS-Plugin enthält:
docker compose -f /opt/containers/headscale/docker-compose.yml build --no-cache
5.3 Starte den Container
Um das Caddy-Image im detached Mode zu starten, verwende den folgenden Befehl:
docker compose -f /opt/containers/headscale/docker-compose.yml up -d
Dieser Befehl startet deinen Caddy-Container im Hintergrund, basierend auf dem lokal erstellten Image.
5.4 Verifiziere deinen Build
Um sicherzustellen, dass das Cloudflare DNS Plugin korrekt in dein Caddy-Image integriert wurde, kannst du die installierten Caddy-Module überprüfen.
Verwende dazu den folgenden Befehl, um die installierten Module in deinem Caddy-Container aufzulisten und nach dem Cloudflare-DNS-Plugin zu suchen:
docker compose exec caddy caddy list-modules | grep cloudflare
Wenn alles richtig funktioniert, solltest du sehen, dass das Cloudflare DNS Plugin in der Liste der installierten Module auftaucht.
5.5 Docker Logs auslesen
Nun, da alles im Hintergrund läuft, sollten wir kurz die Logs überprüfen, um sicherzustellen, dass alles korrekt funktioniert.
docker compose -f /opt/containers/headscale/docker-compose.yml logs -f
In meiner Produktionsumgebung sieht das folgendermaßen aus:
Headplane
Wie zu sehen ist, wurde sowohl die Headscale-Konfiguration erfolgreich geladen als auch OIDC aktiviert.
Headscale
Der Headscale-Container unterscheidet zwischen verschiedenen Log-Leveln:
- INF für Infos,
- WRN für Warnungen,
- FTL für fatale Fehler.
Solange kein FTL-Eintrag auftaucht, läuft der Container korrekt.
6. Web GUI
Nun sollte dein Headscale-Server erfolgreich laufen. Als nächstes greifen wir auf das Headplane User Interface zu. Dieses User Interface befindet sich zwar noch in einer frühen Alpha-Phase, ist jedoch schon gut nutzbar. Sollten Probleme auftreten, können wir immer auf die CLI zurückgreifen, was die Situation weniger kritisch macht.
Wenn du bereits Tailscale genutzt hast, wirst du dich in dieser Umgebung schnell zurechtfinden, da das Interface stark an das originale Tailscale Web-GUI erinnert.
6.1 API Key generieren
Um das Headplane UI nutzen zu können, benötigen wir zunächst einen API Key für die Authentifizierung. Diesen Key erstellen wir mit dem folgenden Befehl:
Führe den folgenden Befehl auf deinem Server aus, um einen neuen API Key zu generieren:
docker exec headscale headscale apikeys create --expiration 24h
Der Output sieht dann so oder so ähnlich aus. Jetzt surfen wir auf https://hs.fqdn.de/admin
und sollten dort folgendes sehen:
Wenn alles geklappt hat solltet ihr auf dem Dashboard angekommen sein:
Bei euch werden dann wahrscheinlich noch keine Geräte zu sehen sein.
7. Web GUI per OIDC sichern (Optional)
Ihr könnt die Web-GUI entweder per API-Token oder komfortabel per OIDC (OpenID Connect) absichern. In diesem Beispiel verwende ich Pocket-ID, da es sowohl in der Einrichtung als auch im täglichen Gebrauch sehr einfach und benutzerfreundlich ist. Die Anleitung zur Installation von Pocket-ID findet ihr hier.
Erstellt zunächst einen neuen OIDC-Client in eurer Pocket-ID-Instanz. Wählt einen passenden Namen und tragt bei den Callback-URLs Folgendes ein:
https://hs.fqdn.de/admin/oidc/callback # Ersetzt 'hs.fqdn.de' durch eure eigene Domain!
Um den OIDC-Login zu aktivieren, benötigt ihr noch einen Root-API-Key. Den erstellt ihr mit folgendem Befehl:
docker exec headscale headscale apikeys create --expiration 999d
Dieser API-Key wird später benötigt.
Nun passen wir die docker-compose.yml Datei entsprechend an, um den OIDC-Login zu ermöglichen. Hier müssen der generierte API-Key sowie die Client-ID, das Client-Secret und die OIDC-Instanz von Pocket-ID eingetragen werden:
# Hier kommt der gerade generierte API Key rein ROOT_API_KEY: 'auchsicher' # OIDC-Konfiguration # Overrides the configuration file values if they are set in config.yaml # If you want to share the same OIDC configuration you do not need this # Hier Client ID, Secret und Domain von eurer Pocket-ID Instanz OIDC_CLIENT_ID: 'client-id' OIDC_ISSUER: 'https://pocket.id' OIDC_CLIENT_SECRET: '123456' # Optional: API-Key-Login für die Web-GUI deaktivieren DISABLE_API_KEY_LOGIN: 'true'
Nach den Anpassungen in der docker-compose.yml Datei muss der Headscale-Container neu gestartet werden:
docker compose -f /opt/containers/headscale/docker-compose.yml restart
Nun solltet ihr euch über die Web-GUI ganz bequem per OIDC einloggen können. Wenn alles korrekt eingerichtet ist, wird der OIDC-Login-Prozess nahtlos funktionieren.
8. Geräte / Clients hinzufügen
Jetzt, wo der gesamte Setup-Prozess abgeschlossen ist, kannst du den großen Vorteil dieses Setups nutzen: Mit nur wenigen Klicks lassen sich verschiedene Geräte wie PCs, Macs, Server, Smartphones und mehr in dein WireGuard VPN integrieren. Zudem können über Caddy ganz einfach SSL-Zertifikate generiert werden, um sicherzustellen, dass deine Dienste nur über das VPN erreichbar sind und gleichzeitig verschlüsselt übertragen werden.
8.1 Windows-Client
8.1.1. Tailscale-Client installieren
Lade den offiziellen Windows Tailscale-Client von der Tailscale-Website herunter und installiere ihn auf deinem Windows-PC.
8.1.2. Headscale-URL konfigurieren
Um deinen Tailscale-Client mit deinem Headscale-Server zu verbinden, öffne eine Eingabeaufforderung oder Powershell und führe folgenden Befehl aus:
tailscale login --login-server https://hs.fqdn.de
Daraufhin öffnet sich ein Browserfenster, in dem du den Registrierungsprozess abschließen kannst, um das Windows-Gerät mit deinem Headscale-Server zu verbinden.
Registriere dein Gerät in der Web UI unter “Add Device -> Register Machine Key”. Füge den mkey:ff...
aus dem Browserfenster ein und wähle den gewünschten user
.
Man kann aber auch über Add Device -> Generate Pre-Auth Key -> Create Auth Key
schon Keys vordefinieren. Das sieht dann so aus:
tailscale up --login-server https://hs.fqdn.de --authkey 835d7b43da44f08c6c6706c48f639c77c0f3bb315dd5c02a
8.1.3. OIDC
Das angenehmste Verfahren wäre wahrscheinlich über OIDC. Ich benutze dafür Pocket-ID.
Dazu erstellt ihr einen neuen OIDC Client für Headscale mit der Callback URL
https://hs.fqdn/oidc/callback
Notiert euch Client ID und Secret und öffnet die Headscale config.yaml
nano /opt/containers/headscale/headscale/config/config.yaml
Löscht den Kommentar für die folgenden Parameter:
oidc: only_start_if_oidc_is_available: true # Hier kommt eure Pocket-ID Domain issuer: "https://pocket.id" # Client ID aus Pocket ID client_id: "xyz123" # Client Secret aus Pocket ID client_secret: "asdf123" # [...] scope: ["openid", "profile", "email", "custom"] extra_params: # Domain auf Pocket ID anpassen domain_hint: https://pocket.id
Startet Headscale neu
docker compose -f /opt/containers/headscale/docker-compose.yml restart
Dann einfach per
tailscale up --login-server=https://hs.fqdn.de
anmelden und dann solltet ihr folgendes sehen:
Klickt auf den Link und dann per Passkey bei Pocket ID einloggen und dann:
Nun habt ihr euer Gerät mit Headscale verbunden.
8.2. Android Client
8.2.1 Android-Client mit eigenem Headscale-Server verbinden
Lade den offiziellen Tailscale Android-Client aus dem Google Play Store oder F-Droid herunter und installiere ihn.
In der Headplane UI werden oben rechts unter Downloads
auch alle Clients gelistet
8.2.2 Headscale-URL konfigurieren
- Öffne die Tailscale-App und gehe zum Einstellungsmenü (oben rechts).
- Wähle Accounts.
- Tippe oben rechts auf das Kebab-Menü (die drei Punkte).
- Wähle Use an alternate server.
- Gib die URL deines Headscale-Servers ein, z. B.
https://headscale.example.com
. - Folge den Anweisungen auf dem Bildschirm, um die Verbindung herzustellen.
Für iOS erspare ich mir die erklärung da es quasi genauso geht, ich aber auch kein Gerät zum testen hier habe.
9. Dienste mit SSL erreichbar machen
Headscale bietet von Haus aus die Magic DNS Funktion an wodurch jedem Gerät eine eindeutige Domain zugewiesen wird, allerdings unterstützt bis jetzt noch keine DNS01 Challenge.
Caddy sorgt dafür, dass deine internen Dienste verschlüsselt und nur via VPN erreichbar sind, und zwar durch die Nutzung der DNS-01-Challenge mit Cloudflare. Dabei werden für alle deine Dienste automatisch SSL-Zertifikate ausgestellt.
Die Schritte, um sicherzustellen, dass deine internen Dienste nur über das VPN und mit SSL-Zertifikaten erreichbar sind, sind
- Wir richten einen internen Services mit Docker ein. Hier zum Testen einfach einen kleinen nginx Webserver.
- Caddy übernimmt das Routing der Anfragen und kümmert sich gleichzeitig um die Bereitstellung der SSL-Zertifikate mithilfe von Cloudflare DNS. Dienste wie z.B.
https://webserver.fqdn.de
werden nur über das VPN erreichbar sein und dank Caddy auch mit gültigen SSL-Zertifikaten geschützt.
ACHTUNG!!
Headscale/Caddy Server ins Mesh-VPN einbinden
Es ist wichtig, dass auch dein Headscale/Caddy Server im Headscale-Mesh-VPN eingebunden ist. Andernfalls wird der nächste Schritt, bei dem SSL-Zertifikate für deine internen Dienste ausgestellt werden und der Zugriff via VPN erfolgt, nicht funktionieren.
Warum ist das wichtig?
Der Headscale-Server selbst muss Teil des Mesh-Netzwerks sein, um die Kommunikation zwischen den Geräten sicherzustellen und die internen Dienste über das VPN erreichbar zu machen. Nur so kann sichergestellt werden, dass alle Geräte – einschließlich des Headscale- und Caddy-Servers – über das VPN miteinander kommunizieren können.
Um dein Setup zu testen und sicherzustellen, dass alles wie geplant funktioniert, kannst du einen A-Record bei Cloudflare anlegen, der auf die VPN-IP deines Headscale/Caddy-Servers zeigt.
In diesem Fall verwenden wir die VPN-IP-Adresse, die der Headscale-Server über Tailscale erhalten hat.
Bei mir läuft der Server auf 100.64.0.6 -> headscale-server
9.1 Warum die VPN-IP verwenden?
Die Verwendung der VPN-IP ist entscheidend, denn sie fungiert als VPN-Reverse-Proxy. Das bedeutet, dass alle Anfragen, die an den von dir konfigurierten DNS-Namen gesendet werden, über das VPN an deinen Headscale/Caddy-Server weitergeleitet werden. Auf diese Weise kannst du SSL-Zertifikate für deine internen Dienste ausstellen, selbst wenn diese nur im internen Netzwerk verfügbar sind. So erreichst du zwei wichtige Ziele:
- Sicherheit: Deine internen Dienste sind nur über das VPN erreichbar, und die Kommunikation ist durch SSL verschlüsselt.
- Zugriffskontrolle: Nur Geräte, die über das VPN verbunden sind, haben Zugriff auf diese Dienste.
9.1.1 Das Cloudflare DNS Plugin im Caddy Webserver
Um diese Anfragen korrekt zu routen, wurde im Caddy-Webserver das Cloudflare DNS-Plugin installiert. Dieses Plugin ermöglicht es, die DNS-Anfragen direkt über Cloudflare zu leiten, was die Verwaltung und Sicherheit deiner internen Dienste vereinfacht. In Kombination mit der DNS-01-Challenge von Let’s Encrypt können so auch SSL-Zertifikate für interne Domains bezogen werden, was in einer herkömmlichen Umgebung ohne öffentliche Erreichbarkeit oft nicht möglich ist.
Mit diesem Setup kannst du also sicherstellen, dass deine internen Dienste verschlüsselt und gut geschützt sind.
9.2 Alternative: Public IP nutzen
Wenn du möchtest, dass bestimmte Dienste öffentlich im Internet erreichbar sind, kannst du stattdessen die Public IP-Adresse deines Servers verwenden:
- Lege einen weiteren A-Record mit der öffentlichen IP-Adresse deines Servers an.
- Die Dienste, die du öffentlich verfügbar machen möchtest, werden dann über die Public IP ins Netz gestellt und können weiterhin mit SSL-Zertifikaten abgesichert werden.
Beide Varianten funktionieren problemlos. Du kannst somit flexibel entscheiden, welche Dienste du nur über das VPN zugänglich machen möchtest und welche auch öffentlich erreichbar sein sollen.
9.3 Setup testen
Also stelle ich bei Cloudflare folgdendes ein:
Auf deinem Server, der bereits im Headscale-VPN verbunden ist und auf dem Docker läuft, öffnest du das Terminal und führst folgenden Befehl aus, um die Headscale-IP des Servers zu erhalten:
9.3.1 Headscale-IP des Test-Servers herausfinden
# Holt euch zu erst eure Headscale IP tailscale ip
9.3.2 Webserver-Container starten
Als Nächstes startest du einen einfachen Nginx-Webserver-Container. Hier wird ein Webserver gestartet, der auf Port 8182 lauscht:
# Startet einen Test-Webserver mit Nginx docker run -d --name webserver -p 8182:80 nginxdemos/hello
9.3.3 Caddy-Konfiguration anpassen
Nun musst du auf deinem Headscale/Caddy-Server die Caddy-Konfiguration anpassen, um die Anfragen an den Webserver, der im VPN läuft, weiterzuleiten.
Öffne das Caddyfile auf deinem Headscale-Server und trage die IP und den Port des Webservers ein, die du im ersten Schritt ermittelt hast:
# Beispiel für das Caddyfile auf dem Headscale-Server # Service: dns test https://webserver.fqdn.de { # Reverse Proxy zur Headscale-IP des Webservers und dem zugehörigen Port reverse_proxy http://100.64.0.2:8182 }
INFO
Ersetze 100.64.0.2
durch die Headscale-IP deines Testservers.
9.3.4 Caddy neu starten
Nachdem du die Änderungen im Caddyfile vorgenommen hast, musst du den Caddy-Server neu starten, um die neue Konfiguration zu laden:
# Neustart des Caddy-Servers auf dem Headscale-Server docker compose -f /opt/containers/headscale/docker-compose.yml restart caddy
Surfe deine Domain an -> https://webserver.fqdn.de
Sieht bei mir so aus:
Wenn alles korrekt eingerichtet wurde und du mit deinem Headscale-VPN verbunden bist, solltest du die Nginx “Hello World”-Seite sehen. Diese Seite ist nur über das VPN erreichbar, da der Headscale-Server die Verbindung über das VPN-Netzwerk weiterleitet.
9.3.5 Container stoppen und entfernen
Zum Abschluss kannst du den Test-Webserver-Container wieder stoppen und entfernen:
# Stoppen und Entfernen des Webservers docker stop webserver && docker rm webserver
Damit hast du erfolgreich einen VPN-geschützten Webservice über Caddy und Headscale bereitgestellt und abgesichert!
10. Exit Nodes und Subnet Routes
10.1 Was sind Exit Nodes?
Um nicht nur ein reines VPN zu haben, sondern auch den gesamten Internetverkehr eines Geräts über eine bestimmte IP-Adresse zu routen, können wir einen der Tailscale-Clients als Exit Node einrichten. Eine Exit Node ermöglicht es, den öffentlichen Internetverkehr eines Geräts sicher durch ein anderes Gerät im Netzwerk zu leiten.
Das bedeutet, dass der gesamte Traffic – sowohl interner als auch externer Internetverkehr – über die IP des Exit Nodes geroutet wird.
Um ein ein Gerät zu verbinden und es selbst zur Exit Node zu machen:
tailscale up --login-server https://hs.fqdn.de --advertise-exit-node
Wenn das Gerät bereits verbunden ist kannst du die Exit Node ganz einfach so setzen:
tailscale set --advertise-exit-node
Jetzt sagen wir dem Betriebssystem noch das wir wirklich den Traffic weiterleiten wollen.
echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/99-vpn.conf echo 'net.ipv6.conf.all.forwarding = 1' >> /etc/sysctl.d/99-vpn.conf sysctl -p /etc/sysctl.d/99-vpn.conf
10.2 Was sind Subnet Routes?
In manchen Fällen ist es nicht möglich oder sinnvoll, den Tailscale-Client auf jedem Gerät in einem Netzwerk zu installieren, z.B. bei Druckern oder IOT. Hier kommt der Subnet Router ins Spiel.
Ein Subnet Router agiert als Gateway und ermöglicht es, Geräte im Netzwerk (ohne Tailscale-Installation) über das Tailscale-Netzwerk (Tailnet) zu erreichen.
Nachdem du IP-Forwarding aktiviert hast, kannst du Tailscale mit dem --advertise-routes
-Flag konfigurieren, um Subnetze in deinem Netzwerk zu eröffnen. Das Kommando sieht folgendermaßen aus:
tailscale up --advertise-routes=192.0.2.0/24,198.51.100.0/24
Ersetze die Beispiel-Subnetze durch die richtigen für dein Netzwerk.
Auf Android, iOS, macOS, tvOS und Windows werden neue Subnet-Routen automatisch erkannt und genutzt.
Standardmäßig sehen Linux-Geräte nur Tailscale-IP-Adressen. Um auf Linux-Geräten die automatische Erkennung neuer Subnet-Routen zu aktivieren, starte Tailscale mit dem --accept-routes
-Flag:
tailscale up --accept-routes
Mit Tailscale kannst du Subnet-Router einrichten, um Subnetze zu bewerben und damit den Zugriff auf Netzwerkinfrastrukturen zu ermöglichen, ohne Tailscale auf jedem Gerät installieren zu müssen.
10.3 Erfahrungen mit Subnet-Routes
Man könnte leicht einen ganzen Artikel über Subnet-Router schreiben, aber hier reiße ich das Thema nur kurz an. Wenn du dich tiefer damit beschäftigen möchtest, findest du alle Details in der offiziellen Tailscale-Dokumentation:
Ich persönlich nutze Subnet-Routes, um zum Beispiel meinen Drucker ins Tailnet einzubinden.
Meine Erfahrungen mit der Tailscale-App auf Android zeigen jedoch, dass sie sehr energieintensiv sein kann. Bei durchgehender Nutzung (24/7) war die App bei mir der Hauptverbraucher und hat die Akkulaufzeit meines Geräts stark reduziert.
Um das Problem zu lösen, habe ich folgenden Ansatz gewählt: Wie in meiner Grafik oben zu sehen, nutze ich einen VPS bei Hetzner, der mit einem Traefik-Crowdsec-Stack läuft. Auf diesem Server habe ich einen WG-Easy-Container installiert, der Teil meines Tailnets ist. Durch die Verwendung eines Subnet-Routers in meinem Heimnetzwerk (--advertise-routes=192.168.178.0/24
) und der Aktivierung dieser Route auf dem Hetzner-Server (--accept-routes
), kann ich mich nun über die WireGuard-App ins Tailnet einwählen.
Das spart enorm viel Akku, da die WireGuard-App hervorragend optimiert ist. Ein zusätzlicher Tipp: Noch besser als die offizielle WireGuard-App finde ich WG Tunnel. Diese App ist über F-Droid und den Play Store verfügbar und liefert eine ausgezeichnete Nutzererfahrung.
Dieser Ansatz bietet eine energieeffiziente Lösung für die Nutzung von Tailscale auf mobilen Geräten.
Super Anleitung. Habe alles gut umsetzen können.
Was ich mich frage ist bei “9.1.1 Das Cloudflare DNS Plugin im Caddy Webserver” hast du dich für eine DNS-01-Challenge um ein gültiges SSL-Zertifikat zu bekommen.
Wenn es aber der Caddy Server ist denn wir im Schritt “4.2 Caddyfile erstellen” eingerichtet haben, ist der leider im öffentlichem Internet erreichbar und nicht nur über die VPN.
Man müsste also eigentlich einen zweiten Caddy Server für den Testserver bzw. alle anderen Servern die man nur im Tailnet haben will erstellen.
Falls ich falsch liege, kannst du mich gerne aufklären.
Auch meinerseits vielen Dank für die Anleitung.
Das Setup mit den sauber getrennten Diensten für private und öffentliche Dienste gefällt mir an sich supi.
Für mich kommt aber an dieser Stelle wieder eine Frage hoch, welche mir schon öfter durch den Kopf ging.
Welchen Vorteil bringt die Nutzung von Cloudflare als DNS-Server?
Denn anscheinend wird dieser in fast allen von mir gesehenen Tutorials etc verwendet.
Ich finde überall nur, dass dieser wohl besonders schnell sein soll.
Ist der Geschwindigkeitsvorteil so enorm oder gibt es noch andere gute Gründe für Cloudflare?
Sorry für die doofe Frage.
Follow-Up:
Pocket-ID läuft nun auch bei mir, bin begeistert 😀 Danke!
Bezüglich headplane gibts jetzt bereits Version 0.3.3, die problemlos läuft und bessere ACL Editierung in der UI ermöglicht
Super danke! Habe es testweise mal ausprobiert und läuft. Hoffe auf mehr Caddy-Anleitungen (z.B. Seafile, verschiedene Matrix-Server, Joplin, Owntracks), da mir Caddy eher gefällt als Traefik.
Bezüglich Proxmox wäre eine Anleitung auch mal interessant.
Gibt es einen Grund, warum du einen Extraserver hierfür benutzt hast und es nicht in deinem bestehenden Traefiksetup eingebaut hast?
Einen Zusatz würd ich der Anleitung noch geben:
Die ACLs waren bei mir nach der Installation leer, das heißt standardmäßig konnte ich nichts erreichen innerhalb meines Tailnets.
Ich musste einiges herum suchen um die ACLs erstmal komplett zu öffnen, bzw die ACLs in der Web-Oberfläche bearbeitbar zu machen (was ja schon sehr praktisch ist). Folgende Einstellungen:
1./opt/containers/headscale/headscale/config/config.yaml:
policy.mode = database
2.Docker container neu starten:
docker compose -f /opt/containers/headscale/docker-compose.yml down && docker compose -f /opt/containers/headscale/docker-compose.yml up -d
3.In der Headplane UI auf ACLs (https://<HEADPLANE>/admin/acls) wechseln und folgendes Template eintragen:
{
“acls”: [
{
“action”: “accept”,
“src”: [“*”],
“dst”: [“*:*”]
}
],
“ssh”: [
{
“action”: “accept”,
“src”: [“*”],
“dst”: [“*”],
“users”: [“*”]
}
]
}
4.Save
5.Nun sollte alles innerhalb des tailnets erlaubt sein, anpassen nach belieben
Danke für diese wunderbare Anleitung!
Einziges Problem dass ich momentan noch habe ist der embedded DERP server. Der funktioniert bei mir in dem Setup garnicht (obwohl er es in einer Headscale Instanz ohne Docker/Caddy funktioniert hat). Hab einiges mit den Ports herumgespielt aber es will und will nicht laufen. Muss man da quasi ein eigenes DERP docker image starten?
Hammer Anleitung, vielen Dank! Hab mir extra für dieses Setup auch einen VPS bei IONOS geholt und arbeite mich gerade durch die Anleitung. Bisher klappt alles, ich freue mich schon aufs fertige Setup.
Vielen Dank für deine Mühen und Zeit mit der Anleitung, wirklich top!