SSD Nodes Learn Hosting plans →
Anleitungen Matt ConnorVon Matt Connor

Headscale-Login über authentik mit OIDC

So verbindest du Headscale mit authentik: Provider und Redirect-URI, der oidc-Block in config.yaml (Version 0.29.4), allowed_groups, Key-Expiry und die Fallstricke danach.

Headscale-Login über authentik: das Ergebnis vorweg

Headscale kann die Anmeldung neuer Knoten an authentik übergeben, per OpenID Connect (OIDC, ein Anmeldeverfahren auf Basis von OAuth 2.0). Der Nutzer tippt dann tailscale up --login-server https://headscale.example.net, öffnet den ausgegebenen Link im Browser und landet auf dem Anmeldeformular deines eigenen authentik. Dafür brauchst du genau zwei Dinge: eine Application mit OAuth2/OIDC-Provider in authentik und einen oidc:-Block in /etc/headscale/config.yaml.

Wenn Headscale bei dir läuft, dann meistens aus einem Grund: kein fremder Dienst soll entscheiden, wer in deinem Netz ist. Vorab verteilte Auth-Keys passen schlecht zu diesem Grund. Ein Key ist ein Geheimnis in einer Chat-Nachricht. Er läuft irgendwann ab oder eben nicht, und er sagt nichts darüber, wer ihn eingelöst hat. Mit OIDC übernimmt deine eigene Nutzerverwaltung diese Entscheidung. Die Grundlage dafür ist Headscale als selbst gehosteter Tailscale-Koordinationsserver, die Gegenseite ist authentik als eigener SSO-Server auf dem VPS.

Gegen welche Version ist das hier geschrieben

Die Schlüssel im oidc:-Block sind über die Releases gewandert. oidc.expiry, oidc.strip_email_domain und oidc.map_legacy_users gab es früher, in aktuellen Versionen sind sie entfernt. Ein Konfigurationsblock ohne Versionsangabe ist deshalb eine Anleitung mit Verfallsdatum. Ein falscher Schlüsselname ist dabei schlimmer als ein fehlender, weil Headscale die Zeile je nach Fall stillschweigend ignoriert und du den Fehler erst beim ersten Login siehst.

Alles hier ist gegen Headscale 0.29.4 geschrieben, veröffentlicht am 23. September 2026, und gegen authentik 2026.x. Prüfe zuerst, was bei dir läuft.

headscale version

Weicht deine Version ab, vergleiche jeden Schlüssel mit der config-example.yaml aus demselben Git-Tag. Diese Datei ist die einzige vollständige Referenz für die Konfiguration, und sie liegt im Repository neben dem Code, den du betreibst.

Schritt 1: Application und Provider in authentik anlegen

In authentik gehören beide Objekte zusammen. Die Application ist das, was Nutzer sehen und worauf Zugriffsregeln greifen. Der Provider ist der OIDC-Teil mit Client ID, Secret und Endpunkten. Der Assistent legt beides in einem Durchgang an. Melde dich an der Admin-Oberfläche an und öffne Applications > Applications, dann den Knopf für eine neue Application mit Provider.

  • Name: Headscale. Den Slug brauchst du gleich, nimm headscale.
  • Provider type: OAuth2/OpenID Connect.
  • Authorization flow: einen deiner bestehenden Flows. Üblich ist eine explizite Zustimmung beim ersten Login.
  • Client type: Confidential. Headscale ist ein Server und kann ein Secret halten.
  • Redirect URI: Typ Strict, Wert https://headscale.example.net/oidc/callback.
  • Signing Key: ein vorhandenes Zertifikat auswählen. Encryption Key bleibt leer, denn verschlüsselte ID-Token versteht Headscale nicht.

Client ID und Client Secret zeigt authentik nach dem Speichern in den Provider-Details. Beide Werte brauchst du im nächsten Schritt.

Zum Typ der Redirect-URI: seit authentik 2026.5 wählst du pro URI aus, ob sie zeichengenau gilt (Strict) oder als Muster (Regex). In älteren Versionen wurde jede eingetragene URI automatisch als Authorization-URI behandelt. Nimm Strict. Ein Muster, das mehr erlaubt als nötig, ist an dieser Stelle eine offene Tür, weil es bestimmt, wohin authentik einen Autorisierungscode ausliefert.

Der Pfad /oidc/callback ist nicht frei wählbar. Headscale baut die Redirect-URI aus dem eigenen server_url und hängt /oidc/callback an. Steht dort noch http://127.0.0.1:8080 aus der Beispieldatei, schickt Headscale genau diesen Wert an authentik, und authentik lehnt ab, weil es diese URI nicht kennt. Der öffentliche Name deines Servers muss in server_url stehen, mit https://.

Welcher Issuer, und warum der Schrägstrich am Ende zählt

authentik gibt jedem Provider einen eigenen Issuer, abgeleitet vom Slug der Application. Bei Slug headscale lautet er https://auth.example.net/application/o/headscale/.

Der Wert in der Headscale-Konfiguration muss zeichengenau dem iss im Token entsprechen, inklusive des Schrägstrichs am Ende. Frage ihn deshalb ab, statt ihn zu tippen.

curl -s https://auth.example.net/application/o/headscale/.well-known/openid-configuration \
  | jq -r '.issuer, .authorization_endpoint'

Die erste Zeile der Ausgabe ist der Wert für issuer. Bekommst du HTML statt JSON, stimmt der Slug nicht oder die Application ist nicht gespeichert. Dieser eine Befehl erspart dir später die Suche nach einem Fehler, der aussieht wie ein Netzwerkproblem.

Wer darf Knoten registrieren

Die Frage hat zwei Ebenen, und du solltest beide setzen.

In authentik entscheidet die Bindung an der Application, wer sie überhaupt benutzen darf. Ohne Bindung ist eine Application für alle Nutzer offen. Lege in den Details deiner Application eine Bindung auf eine Gruppe an, zum Beispiel headscale-users. Alle anderen bekommen die Anwendung dann nicht einmal zu sehen.

In Headscale filtert allowed_groups ein zweites Mal, auf Basis des Claims groups im ID-Token. Das ist keine doppelte Arbeit ohne Zweck: die Bindung schützt den Provider, allowed_groups schützt Headscale auch dann noch, wenn jemand in authentik die Bindung versehentlich entfernt.

Das Standard-Mapping für den Scope profile liefert in authentik die Gruppenmitgliedschaft mit, als Liste von Gruppennamen. Deshalb reicht scope: ["openid", "profile", "email"] für den Filter aus. Wer eine eigene Claim-Form braucht, legt unter Customization > Property Mappings ein Scope Mapping an und wählt es im Provider unter Scopes aus. Ein üblicher Ausdruck dafür ist return {"groups": [group.name for group in request.user.ak_groups.all()]}. Wichtig ist dabei die Reihenfolge: authentik kennt nur Scopes, für die ein Mapping existiert. Ein zusätzliches "groups" in der Headscale-Konfiguration bringt dir keinen Claim, solange es in authentik kein passendes Scope Mapping gibt.

authentik trägt Gruppennamen so ein, wie sie in der Oberfläche stehen. Keycloak stellt seinen Gruppen ein / voran, authentik nicht. Schreibe also headscale-users, nicht /headscale-users. Eine Anleitung, die du aus einem Keycloak-Kontext kopiert hast, scheitert genau an diesem Zeichen.

Schritt 2: Der oidc-Block in /etc/headscale/config.yaml

server_url: https://headscale.example.net

oidc:
  only_start_if_oidc_is_available: true
  issuer: "https://auth.example.net/application/o/headscale/"
  client_id: "<Client ID aus authentik>"
  client_secret_path: "/etc/headscale/oidc_client_secret"
  scope: ["openid", "profile", "email"]
  email_verified_required: true
  pkce:
    enabled: true
    method: S256
  allowed_groups:
    - "headscale-users"
  use_expiry_from_token: false

only_start_if_oidc_is_available: true lässt Headscale beim Start abbrechen, wenn die Discovery-URL des Issuers nicht erreichbar ist. Das ist gewollt. Ein Headscale, das ohne funktionierende Anmeldung hochkommt, verschiebt den Fehler nur auf die nächste Person, die sich anmelden will.

pkce.enabled mit method: S256 schaltet Proof Key for Code Exchange ein, eine Absicherung des Autorisierungscodes gegen Abfangen unterwegs. authentik unterstützt das, also gibt es keinen Grund, darauf zu verzichten.

email_verified_required steht in aktuellen Versionen standardmäßig auf true. Headscale übernimmt eine E-Mail-Adresse dann nur, wenn das Claim email_verified wahr ist. Sieh nach, welchen Wert dein authentik hier schickt, bevor du diesen Schalter anfasst.

Zusätzlich gibt es allowed_domains für die E-Mail-Domain und allowed_users für einzelne Adressen. Nimm den Filter, den du auch pflegen wirst. Mehrere Filter, die sich gegenseitig widersprechen, sind später schwerer zu durchschauen als ein einziger.

Welche OIDC-Claims Headscale liest
  • email: die E-Mail-Adresse des Nutzers, bei email_verified_required: true nur als verifizierte Adresse.
  • name: der Anzeigename, zum Beispiel Sam Smith.
  • preferred_username: der Nutzername in Headscale.
  • picture: die URL zu einem Avatar.
  • iss und sub: zusammen die stabile Kennung des Nutzers, gespeichert als provider_identifier.
  • groups: wird ausschließlich für den Filter allowed_groups benutzt.

Das Client Secret gehört nicht in die config.yaml

Eine Konfigurationsdatei wird kopiert, in ein Repository gelegt und in Support-Anfragen eingefügt. Das Secret sollte diesen Weg nicht mitgehen. Headscale liest es deshalb auch aus einer eigenen Datei. client_secret und client_secret_path schließen sich gegenseitig aus, setze also nur einen der beiden Schlüssel.

sudo install -m 600 -o headscale -g headscale /dev/null /etc/headscale/oidc_client_secret
printf '%s' 'SECRET_AUS_AUTHENTIK' | sudo tee /etc/headscale/oidc_client_secret > /dev/null
sudo ls -l /etc/headscale/oidc_client_secret

Die letzte Zeile muss -rw------- und den Eigentümer headscale zeigen. Steht dort root root, kann der Dienst die Datei nicht lesen, sofern er nicht als root läuft. printf '%s' statt echo ist Absicht: echo hängt einen Zeilenumbruch an, und ein Secret mit Zeilenumbruch ist ein anderes Secret als das in authentik.

So landet das Secret allerdings in der Shell-History. Setze ein Leerzeichen vor den Befehl, wenn deine Shell HISTCONTROL=ignorespace auswertet, oder schreibe die Datei direkt mit einem Editor. Im Container-Setup ist die Umgebungsvariable HEADSCALE_OIDC_CLIENT_SECRET der bequemere Weg, weil das Secret dann aus dem Orchestrator kommt und nie im Image liegt.

Neu starten und prüfen, bevor jemand zu tippen anfängt

sudo systemctl restart headscale
sudo systemctl is-active headscale
sudo journalctl -u headscale -n 30 --no-pager
curl -s https://headscale.example.net/health

is-active muss active melden. Mit only_start_if_oidc_is_available: true ist ein fehlgeschlagener Start die ehrliche Antwort auf eine falsche Issuer-URL, und das Journal nennt die Adresse, die nicht geantwortet hat. Der Aufruf von /health liefert eine kurze JSON-Antwort mit dem Status des Dienstes. Antwortet er nicht über HTTPS, liegt der Fehler in deinem Reverse Proxy und nicht in der OIDC-Konfiguration.

Schritt 3: Anmelden mit tailscale up --login-server

Auf dem Rechner, der in das Tailnet soll, läuft der normale Tailscale-Client. Er bekommt nur eine andere Adresse für die Koordination.

sudo tailscale up --login-server https://headscale.example.net
sudo tailscale status

Der erste Befehl gibt eine URL aus und wartet. Öffne sie im Browser. Headscale leitet sofort zu authentik weiter, du meldest dich dort an, stimmst beim ersten Mal der Anwendung zu, und danach bestätigt eine Seite von Headscale die Registrierung des Knotens. Im Terminal läuft tailscale up durch, und tailscale status listet den Knoten mit einer Adresse aus 100.64.0.0/10.

Auf dem Server siehst du dasselbe von der anderen Seite.

headscale users list
headscale nodes list

Der Nutzer wurde beim ersten Login automatisch angelegt. Du rufst kein headscale users create mehr auf, und du gibst nichts frei. Genau das ist der Unterschied zum Betrieb ohne OIDC, bei dem du jede Registrierung von Hand bestätigst, mit headscale auth register --auth-id <ID> --user <USER>. Dieser Befehl hat in 0.29 das ältere headscale nodes register ersetzt.

Den Nutzernamen nimmt Headscale aus preferred_username, den Anzeigenamen aus name, die Adresse aus email. Hier wirkt eine dokumentierte Einschränkung: ein Nutzername in Headscale braucht mindestens zwei Zeichen, beginnt mit einem Buchstaben, erlaubt Ziffern, Bindestriche, Punkte und Unterstriche und höchstens ein @. Schickt dein authentik etwas anderes, kann die Anmeldung an dieser Prüfung scheitern. Korrigiere das dann im Mapping in authentik, nicht in Headscale.

Server und Exit Nodes, die keinen Browser haben

Ein OIDC-Login braucht einen Browser. Für einen Knoten im Rechenzentrum, den niemand interaktiv bedient, bleiben Pre-Auth-Keys der richtige Weg, und sie funktionieren mit aktivem OIDC unverändert weiter.

headscale users list
headscale preauthkeys create --user 3 --expiration 24h

--user erwartet seit 0.28 die numerische ID aus headscale users list, nicht den Namen. Diese Umstellung ist der häufigste Grund, warum ein kopierter Befehl aus einer älteren Anleitung fehlschlägt. Ohne --expiration gilt ein Key eine Stunde und nur einmal. Auf dem Knoten selbst:

sudo tailscale up --login-server https://headscale.example.net --authkey <KEY>

Eine klare Trennung hilft dir später: Menschen kommen über authentik, Maschinen über Keys eines eigenen Nutzers. Dann weißt du bei jedem Knoten in der Liste, warum er im Netz ist.

Was mit Knoten passiert, die schon per Pre-Auth-Key drin sind

Die Knoten selbst merken nichts. Sie behalten ihre Schlüssel, ihre Adresse und ihre Verbindungen. Die Frage ist der Nutzer, an dem sie hängen.

Seit 0.24 identifiziert Headscale einen OIDC-Nutzer über die Claims iss und sub, nicht über die E-Mail-Adresse. Diese Kennung steht als provider_identifier in der Tabelle users. Ein Nutzer, den du früher mit headscale users create alice angelegt hast, hat dort keinen Wert. Meldet sich dieselbe Person jetzt über authentik an, entsteht deshalb ein zweiter Eintrag mit provider_identifier, und die alten Knoten bleiben am alten Eintrag. Die automatische Zuordnung über den Namen gab es einmal als oidc.map_legacy_users, in 0.29 ist dieser Schlüssel entfernt.

Für die Umstellung hast du damit zwei ehrliche Wege. Entweder die alten Knoten bleiben am alten Nutzer und melden sich beim nächsten Neuaufsetzen über OIDC neu an, was aufräumt und nichts kaputt macht. Oder du setzt den provider_identifier in der Datenbank selbst, was die Headscale-Dokumentation für den Wechsel des Providers ausdrücklich nennt. Das ist ein direkter Schreibzugriff: Dienst stoppen, Datenbank kopieren, dann schreiben. Den richtigen Wert erfährst du nicht durch Raten, sondern indem du eine Testperson über authentik anmeldest und den dabei erzeugten Datensatz ansiehst.

Ablauf der Knoten und Re-Authentifizierung

Ein Schalter entscheidet hier über den Alltag deiner Nutzer. Die Laufzeit einer Registrierung steht in node.expiry und nicht mehr in oidc.expiry.

node:
  expiry: 30d

Mit 0 schaltest du den Ablauf ab. Läuft ein Knoten ab, verschwindet er aus dem Tailnet, bis sich jemand neu anmeldet.

sudo tailscale login --login-server https://headscale.example.net

Die Alternative use_expiry_from_token: true bindet die Laufzeit an das Access Token des IdP. Das klingt nach der saubereren Lösung, ist in der Praxis aber eine Falle: ein Access Token lebt oft nur Minuten. Ohne passend verlängerte Token-Laufzeiten in authentik meldet sich dein Team dann mehrmals am Tag neu an. Die Headscale-Dokumentation weist auf genau diesen Punkt hin. Lass den Schalter aus, bis du die Laufzeiten in authentik bewusst gesetzt hast.

Wichtig für das Bild im Kopf: der Ablauf eines Knotens ist keine Browser-Sitzung. Das Ende einer authentik-Sitzung wirft niemanden aus dem Netz, weil der Datenpfad WireGuard zwischen den Knoten ist. Er läuft weiter, bis Headscale den Knoten für abgelaufen erklärt und die anderen Knoten diese Information erhalten.

Wenn die Anmeldung scheitert: wo du nachsiehst

Die Fehler verteilen sich auf zwei Logs, und fast immer weiß eines von beiden die Antwort.

In authentik findest du sie unter Events. Eine abgelehnte Autorisierung steht dort mit Aktion und Kontext, und eine nicht passende Redirect-URI erscheint als Konfigurationsfehler. Der Browser zeigt in diesem Fall eine Fehlerseite von authentik, nicht das Anmeldeformular. Vergleiche dann Zeichen für Zeichen: Schema, Hostname, Port und den Pfad /oidc/callback.

In Headscale liest du mit sudo journalctl -u headscale -f mit, während sich jemand anmeldet. Ein Nutzer, der nicht in allowed_groups steht, kommt bis zum Anmeldeformular und bekommt trotzdem keinen Knoten registriert. Prüfe dann zuerst, ob das Claim groups überhaupt ankommt, über die Claim-Vorschau am Provider in authentik oder indem du allowed_groups testweise auskommentierst. Kommt die Anmeldung ohne den Filter durch, fehlt der Claim und nicht die Berechtigung.

Ein dritter Fall sieht wie ein Fehler aus und ist keiner: der Link aus tailscale up gilt nur kurz. Wer ihn kopiert, in eine Nachricht schreibt und eine halbe Stunde später öffnet, muss tailscale up einfach erneut ausführen.

Bedrohungsmodell in einem Absatz

OIDC ändert, wer beitreten darf. Es ändert nicht, was verschlüsselt wird. Die privaten WireGuard-Schlüssel entstehen auf den Knoten und verlassen sie nicht, Headscale verteilt nur öffentliche Schlüssel und Adressen. Neu ist, dass authentik jetzt auf dem Weg in dein Netz liegt: wer dort Nutzer anlegen oder Gruppen ändern kann, kann Knoten in dein Tailnet bringen, und ab da entscheidet nur noch deine Policy, was diese Knoten erreichen. Sichere authentik also mindestens so gut ab wie Headscale, mit einem zweiten Faktor für alle, die in allowed_groups stehen. Was bei einem übernommenen Koordinationsserver noch trägt, beschreibt wer bei Tailscale welche Schlüssel in der Hand hält. Die Gegenmaßnahme im Original heißt Tailnet Lock, und die hat Headscale nicht.

Dazu eine nüchterne Einordnung: Headscale ist eine eigenständige Nachimplementierung des Koordinationsservers durch die Community, kein Produkt von Tailscale. Es gibt dafür keinen Support und keine Zusage, dass jede neue Client-Version passt. Wenn du noch abwägst, hilft was Tailscale technisch überhaupt ist beim Vergleich, und bei der Wahl des IdP Keycloak, authentik und Zitadel im Vergleich.

FAQ

Warum leitet authentik nach dem Login nicht zu Headscale zurück?

Die Redirect-URI passt nicht. Headscale baut sie aus dem eigenen server_url plus /oidc/callback, also muss in authentik genau https://headscale.example.net/oidc/callback als Redirect URI vom Typ Strict eingetragen sein. Steht in der Headscale-Konfiguration noch http://127.0.0.1:8080 aus der Beispieldatei, sendet Headscale diesen Wert, und authentik kennt ihn nicht. Im Ereignis-Log von authentik erscheint das als Konfigurationsfehler, der Browser zeigt eine Fehlerseite statt des Anmeldeformulars.

Was passiert mit Knoten, die ich vorher mit einem Pre-Auth-Key registriert habe?

Sie laufen unverändert weiter, inklusive Schlüssel und Adresse. Anders ist nur die Zuordnung zum Nutzer: Headscale identifiziert OIDC-Nutzer über die Claims iss und sub, gespeichert als provider_identifier in der Tabelle users. Ein per CLI angelegter Nutzer hat dort keinen Wert, deshalb entsteht beim ersten Login über authentik ein neuer Eintrag, und die alten Knoten hängen weiter am alten. Die automatische Zuordnung über den Namen (oidc.map_legacy_users) ist in 0.29 entfernt. Entweder du meldest die betroffenen Knoten über OIDC neu an, oder du setzt den provider_identifier bei gestopptem Dienst und mit Kopie der Datenbank selbst.

Muss ich mich neu anmelden, wenn meine authentik-Sitzung endet?

Nein, solange use_expiry_from_token auf false steht. Dann gilt die Registrierung eines Knotens so lange, wie node.expiry sagt, zum Beispiel 30d, und 0 schaltet den Ablauf ganz ab. Setzt du use_expiry_from_token: true, übernimmt Headscale die Laufzeit des Access Tokens, und das lebt bei vielen Providern nur Minuten. Ohne angepasste Token-Laufzeiten in authentik führt das zu ständigen Neuanmeldungen. Eine fällige Re-Authentifizierung erledigt sudo tailscale login --login-server https://headscale.example.net.

Kann ich authentik-Gruppen in den Headscale-ACLs benutzen?

Nein. Gruppen aus dem IdP dienen nur dem Filter allowed_groups, also der Frage, wer sich anmelden darf. In der Policy definierst du Gruppen selbst und trägst die Nutzer dort ein. Ein OIDC-Nutzer wird dabei über seine Kennung mit angehängtem @ referenziert, und den genauen Wert liest du in der Spalte provider_identifier der Tabelle users ab. Diese Einschränkung ist in der Headscale-Dokumentation ausdrücklich genannt und keine Fehlkonfiguration bei dir.

Ist Headscale ein offizielles Tailscale-Produkt?

Nein. Headscale ist eine unabhängige Nachimplementierung des Koordinationsservers, geschrieben von der Community. Die Clients sind die echten Tailscale-Clients, der Server dahinter ist es nicht. Daraus folgen zwei praktische Punkte: es gibt keinen kommerziellen Support, und Funktionen des Originals fehlen teilweise, Tailnet Lock zum Beispiel. Plane Updates deshalb bewusst und lies die Release Notes, bevor du ein Major-Update einspielst.