Headscale auf Debian 13 — Tailscale-Coordinator selbst hosten

Headscale auf Debian 13 selbst hosten: Open-Source-Coordinator für Tailscale-VPN. Volle Kontrolle über euer Mesh-Netzwerk ohne Drittanbieter-Abhängigkeit.

Headscale auf Debian 13 selbst hosten: Open-Source-Coordinator für Tailscale-VPN. Volle Kontrolle über euer Mesh-Netzwerk ohne Drittanbieter-Abhängigkeit.

💡 Hinweis: Dieses Tutorial setzt voraus, dass du einen normalen Benutzer mit sudo-Rechten verwendest — wie im Tutorial Debian 13 Server absichern beschrieben. Außerdem solltest du einen Reverse Proxy mit Nginx eingerichtet haben.

Einleitung — Was ist Headscale?

Wer Tailscale genutzt hat, weiß: Es ist die wahrscheinlich einfachste Mesh-VPN-Lösung überhaupt. Geräte verbinden sich automatisch, NAT-Traversal funktioniert, Routing wird automatisch berechnet. Aber: Der Coordination Server läuft bei Tailscale Inc. Eure Metadaten — wer mit wem reden will, wann, wie lange — gehen über deren Infrastruktur.

Headscale ist ein Open-Source-Reimplementation des Tailscale-Coordinators. Ihr hostet ihn selbst — die Tailscale-Clients verbinden sich nicht mehr zu controlplane.tailscale.com, sondern zu eurem eigenen Server. Resultat: Volle Kontrolle, keine Drittanbieter-Abhängigkeit, kostenlos für unbegrenzt viele Nodes.

Die eigentliche Datenübertragung zwischen Geräten passiert weiterhin direkt (Peer-to-Peer) — der Coordinator vermittelt nur Schlüssel und Routen. Eure Daten gehen nie über den Headscale-Server.

Was Headscale kann

  • Vollständig kompatibel mit den offiziellen Tailscale-Clients (Linux, macOS, Windows, iOS, Android)
  • Mesh-VPN wie bei Tailscale — direkte Verbindungen, NAT-Traversal
  • Magic DNS — Geräte per Hostnamen erreichbar
  • ACLs für feingranulare Zugriffsregeln
  • Subnet Routing — Komplette LAN-Subnetze freigeben
  • Exit-Nodes — Internet-Traffic über bestimmtes Gerät routen
  • Multi-User — Mit Namespaces für getrennte Netze
  • Web-UI über Drittanbieter wie headscale-ui oder headplane

Headscale vs. Tailscale Cloud

FeatureHeadscaleTailscale Cloud
Setup-KomplexitätMittel-HochTrivial
Kontrolle über CoordinatorVollKeine
KostenSelbsthost-KostenFree bis 100 Geräte, dann $$$
Geräte-LimitUnbegrenzt100 (Free)
Web-Admin-UINur DrittanbieterEingebaut
Tailnet-LockNein (geplant)Ja
MagicDNSJaJa
ACLsJaJa
Beste Wahl fürPrivacy-Fanatiker, große SetupsSchnell-Setups, Standard-User

⚠️ Wichtig: Headscale braucht einen öffentlich erreichbaren Server mit gültigem SSL-Zertifikat. Tailscale-Clients akzeptieren keine self-signed Certificates oder IP-Adressen. Eine Domain ist Pflicht.

In diesem Guide richten wir Headscale auf Debian 13 ein, verbinden mehrere Clients und konfigurieren ACLs.


Voraussetzungen

  • Debian 13 Server mit öffentlicher IP — siehe Debian 13 Server-Guide
  • Eine Domain (z.B. headscale.example.com)
  • Nginx als Reverse Proxy
  • certbot installiert
  • Mindestens 1 GB RAM (Headscale ist sehr leichtgewichtig)

Firewall & DNS

sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 3478/udp   # STUN für NAT-Traversal
sudo ufw status

DNS-A-Record für headscale.example.com setzen.


Schritt 1: Headscale installieren

Wir nutzen die offiziellen .deb-Pakete:

HEADSCALE_VERSION=$(curl -s https://api.github.com/repos/juanfont/headscale/releases/latest | grep tag_name | cut -d '"' -f 4)
echo "Installiere Headscale $HEADSCALE_VERSION"

cd /tmp
wget "https://github.com/juanfont/headscale/releases/download/${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION#v}_linux_amd64.deb"
sudo dpkg -i "headscale_${HEADSCALE_VERSION#v}_linux_amd64.deb"

Headscale wird automatisch als systemd-Service eingerichtet, aber noch nicht gestartet — wir müssen erst die Konfiguration anpassen.

headscale version

Sollte eine Versionsnummer ausgeben.


Schritt 2: Konfiguration

sudo nano /etc/headscale/config.yaml

Wichtige Einstellungen anpassen:

# Eure öffentliche Server-URL (mit https!)
server_url: https://headscale.example.com

# IP-Adresse für Headscale (intern, hinter Nginx)
listen_addr: 127.0.0.1:8080

# Metrics-Endpoint
metrics_listen_addr: 127.0.0.1:9090

# gRPC für CLI-Verwaltung
grpc_listen_addr: 127.0.0.1:50443
grpc_allow_insecure: <strong>false</strong>

# Datenbank — SQLite für kleine Setups, PostgreSQL für große
database:
  type: sqlite
  sqlite:
    path: /var/lib/headscale/db.sqlite

# Private Key — wird automatisch generiert
noise:
  private_key_path: /var/lib/headscale/noise_private.key

# Tailnet-Adressbereich (RFC1918, nicht euer LAN!)
prefixes:
  v4: 100.64.0.0/10
  v6: fd7a:115c:a1e0::/48

# DERP-Server (für NAT-Traversal)
derp:
  server:
    enabled: <strong>false</strong>
    region_id: 999
    region_code: "headscale"
    region_name: "Headscale Embedded DERP"
    stun_listen_addr: "0.0.0.0:3478"
  urls:
    - https://controlplane.tailscale.com/derpmap/default
  paths: []
  auto_update_enabled: <strong>true</strong>
  update_frequency: 24h

# Magic DNS
dns:
  magic_dns: <strong>true</strong>
  base_domain: example.com
  override_local_dns: <strong>true</strong>
  nameservers:
    global:
      - 1.1.1.1
      - 8.8.8.8

# Logs
log:
  level: info
  format: text

# OIDC (optional — falls Authentik vorhanden)
# oidc:
#   issuer: "https://auth.example.com/application/o/headscale/"
#   client_id: "EURE_CLIENT_ID"
#   client_secret: "EUER_CLIENT_SECRET"

# Policy / ACLs (kommen später)
policy:
  mode: file
  path: "/etc/headscale/acl.yaml"

# Unix-Socket für lokale Verwaltung
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"

⚠️ Wichtig: Der Adressbereich 100.64.0.0/10 ist Tailscale-Standard — nicht euer LAN! Verwendet niemals einen Bereich, der mit eurem normalen Netz kollidiert (192.168.x.x, 10.x.x.x, 172.16-31.x.x).

Verzeichnisse anlegen

sudo mkdir -p /var/lib/headscale /var/run/headscale
sudo chown -R headscale:headscale /var/lib/headscale /var/run/headscale

Schritt 3: Headscale starten

sudo systemctl enable --now headscale
sudo systemctl status headscale

Logs mitlesen:

sudo journalctl -u headscale -f

Bei listening and serving HTTP on 127.0.0.1:8080 ist Headscale bereit.

Funktion testen

sudo headscale version
sudo headscale users list

Sollte eine leere User-Liste zeigen.


Schritt 4: Nginx Reverse Proxy

sudo nano /etc/nginx/sites-available/headscale.example.com

Inhalt:

server {
    listen 80;
    listen [::]:80;
    server_name headscale.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name headscale.example.com;

    ssl_certificate /etc/letsencrypt/live/headscale.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/headscale.example.com/privkey.pem;

    # Lange Timeouts für persistente Tailscale-Verbindungen
    proxy_read_timeout 1d;
    proxy_send_timeout 1d;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # WebSocket / Long-Polling Support
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $http_connection;

        proxy_buffering off;
        proxy_redirect off;
    }
}

Aktivieren und SSL holen:

sudo ln -s /etc/nginx/sites-available/headscale.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d headscale.example.com

Test:

curl https://headscale.example.com/health

Sollte OK zurückgeben.

Geschafft! Headscale läuft öffentlich erreichbar mit SSL. Jetzt verbinden wir die ersten Clients.


Schritt 5: Ersten User anlegen

Headscale-User sind Namespaces — keine Logins. Sie gruppieren Geräte:

sudo headscale users create marius

User-Liste prüfen:

sudo headscale users list

Ihr seht jetzt euren neuen User mit einer ID.


Schritt 6: Ersten Tailscale-Client verbinden

Tailscale-Client auf eurem Laptop / Server installieren — z.B. auf einem zweiten Linux-System:

curl -fsSL https://tailscale.com/install.sh | sudo sh

Dann statt der Tailscale-Cloud auf eure Headscale-Instanz zeigen lassen:

sudo tailscale up --login-server=https://headscale.example.com

Tailscale öffnet eine URL in einer Form, die wie folgt aussieht:

To authenticate, visit:
  https://headscale.example.com/register/nodekey:abc123...

Diese URL öffnet ihr nicht im Browser — sondern kopiert den nodekey:abc123...-Teil und registriert ihn auf dem Headscale-Server:

sudo headscale nodes register --user marius --key nodekey:abc123...

Auf dem Client läuft Tailscale jetzt los.

Status prüfen

Auf dem Headscale-Server:

sudo headscale nodes list

Sollte euren neuen Node zeigen.

Auf dem Client:

tailscale status
tailscale ip

Eure Tailscale-IP sollte aus dem 100.64.0.0/10-Bereich kommen.


Schritt 7: Pre-Authenticated Keys für einfaches Onboarding

Statt jeden Node manuell zu registrieren, könnt ihr Pre-Auth-Keys generieren:

sudo headscale preauthkeys create --user marius --reusable --expiration 24h

Output:

abc123def456...

Auf jedem neuen Client:

sudo tailscale up --login-server=https://headscale.example.com --auth-key=abc123def456...

Damit wird kein Browser-Flow mehr gebraucht.

💡 Tipp: Mit --ephemeral als zusätzlichem Flag wird das Gerät beim Disconnect automatisch wieder gelöscht. Perfekt für CI-Runner, Container, kurzlebige VMs.


Schritt 8: Magic DNS testen

Wenn magic_dns: true in der Config gesetzt ist, sind eure Geräte per Hostname erreichbar:

ping marius-laptop

Wenn das nicht klappt:

sudo tailscale up --accept-dns

oder explizit setzen:

sudo headscale nodes rename --identifier 1 marius-laptop

Schritt 9: ACLs konfigurieren

Standardmäßig dürfen alle Tailscale-Geräte mit allen anderen reden. Mit ACLs kontrolliert ihr das.

sudo nano /etc/headscale/acl.yaml

Beispiel — strikte Trennung zwischen Servern und Clients:

groups:
  group:admins:
    - marius
  group:family:
    - frau
    - kind1

tagOwners:
  tag:server:
    - group:admins
  tag:work:
    - group:admins

acls:
  # Admins dürfen alles
  - action: accept
    src:
      - group:admins
    dst:
      - "*:*"

  # Familie darf nur auf bestimmte Geräte (z.B. Plex-Server)
  - action: accept
    src:
      - group:family
    dst:
      - tag:server:32400

Headscale neu laden:

sudo systemctl reload headscale

ACL-Test:

sudo headscale policy check

⚠️ Wichtig: ACLs sind default-deny, sobald sie aktiv sind. Wenn ihr eine fehlerhafte ACL einspielt und niemand mehr verbinden kann, könnt ihr die Datei löschen oder leeren — dann fällt Headscale auf „alle dürfen alles“ zurück.


Schritt 10: Subnet-Routing — komplettes LAN freigeben

Ein Tailscale-Node kann als Gateway für ein ganzes Subnetz fungieren — perfekt für Homelab-Zugriff.

Auf dem Gateway-Node (z.B. ein Raspberry Pi im LAN):

sudo tailscale up --login-server=https://headscale.example.com \
  --advertise-routes=192.168.1.0/24

Auf dem Headscale-Server die Route freischalten:

sudo headscale nodes list
sudo headscale routes list
sudo headscale routes enable -r ROUTE_ID

Andere Tailscale-Clients akzeptieren die Route mit:

sudo tailscale up --accept-routes

Jetzt erreicht jeder Tailscale-Client euer komplettes LAN — ohne Portfreigaben.


Backups

Headscale speichert minimal Daten, aber kritisch:

  • /etc/headscale/config.yaml — Konfiguration
  • /etc/headscale/acl.yaml — ACLs
  • /var/lib/headscale/db.sqlite — User, Nodes, Pre-Auth-Keys
  • /var/lib/headscale/noise_private.key — Krypto-Schlüssel

Backup-Script

sudo nano /usr/local/bin/headscale-backup.sh

Inhalt:

#!/bin/bash
set -e

BACKUP_DIR="/var/backups/headscale"
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
RETENTION_DAYS=14

mkdir -p "$BACKUP_DIR"

tar -czf "$BACKUP_DIR/headscale-$TIMESTAMP.tar.gz" \
  /etc/headscale \
  /var/lib/headscale 2>/dev/null

find "$BACKUP_DIR" -type f -mtime +$RETENTION_DAYS -delete

echo "Backup completed: $TIMESTAMP"
sudo chmod +x /usr/local/bin/headscale-backup.sh

Mit systemd-Timer automatisieren — siehe restic-Tutorial.

⚠️ Best Practice: Wenn die noise_private.key weg ist, müssen sich alle Clients neu registrieren. Sichert sie verschlüsselt an einem zweiten Ort.


Updates

HEADSCALE_VERSION=$(curl -s https://api.github.com/repos/juanfont/headscale/releases/latest | grep tag_name | cut -d '"' -f 4)
cd /tmp
wget "https://github.com/juanfont/headscale/releases/download/${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION#v}_linux_amd64.deb"

# Backup vor dem Update!
sudo /usr/local/bin/headscale-backup.sh

sudo dpkg -i "headscale_${HEADSCALE_VERSION#v}_linux_amd64.deb"
sudo systemctl restart headscale

💡 Empfehlung: Headscale ist Pre-1.0 (aktuell ~0.23.x) und Releases können Breaking Changes enthalten. Lest die Release-Notes, bevor ihr updatet.


Zusammenfassung & Checkliste

SchrittStatus
Debian 13 + öffentliche IP + Domain
Nginx als Reverse Proxy installiert
Headscale .deb installiert
config.yaml angepasst
Headscale-Service läuft
Nginx Reverse Proxy + SSL eingerichtet
/health Endpoint via HTTPS erreichbar
Erster User angelegt
Erster Client erfolgreich verbunden
Pre-Auth-Key generiert
Magic DNS funktioniert
ACLs definiert (optional)
Subnet-Routing eingerichtet (optional)
Backup-Script eingerichtet

Troubleshooting

Client kann sich nicht verbinden

  • Headscale-URL korrekt (mit https://)?
  • curl https://headscale.example.com/health von außen erreichbar?
  • SSL-Zertifikat valide? openssl s_client -connect headscale.example.com:443

„node has no IP addresses“

Adressbereich-Konflikt mit dem Client-Netz oder vergessen, in der Config einen Bereich zu setzen. prefixes-Section in config.yaml prüfen.

Magic DNS funktioniert nicht

  • Auf Client: sudo tailscale up --accept-dns
  • In config.yaml: magic_dns: true und base_domain gesetzt?
  • Lokales DNS-Override aktiv? (override_local_dns: true)

„Failed to register node“

  • Pre-Auth-Key abgelaufen? sudo headscale preauthkeys list --user marius
  • Key bereits verwendet und nicht reusable?

NAT-Traversal scheitert (P2P-Verbindung kommt nicht)

  • UDP-Port 3478 (STUN) offen?
  • Mindestens ein Client hat keine direkte Internetverbindung? Dann läuft Traffic über DERP-Relay (langsamer, aber funktioniert)
  • Eigenen DERP-Server hosten — siehe derp.server.enabled: true

Performance schlecht

  • Verbindungen laufen über DERP statt P2P? tailscale netcheck auf Client zeigt es
  • Headscale-Server in Region weit weg? CDN-DERP nutzen oder eigenen DERP hosten

Nächste Schritte

  • Eigenen DERP-Server hosten — Für noch mehr Privacy
  • OIDC mit Authentik — User-Login statt Pre-Auth-Keys
  • Web-UI über headplane — Schöne Verwaltungsoberfläche
  • Subnet-Router HA — Zwei Gateways im LAN für Failover
  • Exit-Node — Internet-Traffic über vertrauenswürdiges Gerät routen

Habt ihr Fragen oder Probleme? Schreibt es in die Kommentare — ich helfe gerne!

Kommentar hinterlassen