SSD Nodes Learn Hosting plans →
Gidsen Matt ConnorDoor Matt Connor

Van Tailscale naar Headscale: je tailnet verhuizen

Verhuis je tailnet van Tailscale naar Headscale zonder een node kwijt te raken: inventaris, ACL-policy omzetten, pre-auth keys, node voor node omzetten en terugvallen.

Wat deze verhuizing precies inhoudt

Migreren van Tailscale naar Headscale betekent één ding: je nodes praten straks met jouw eigen controlserver in plaats van met die van Tailscale. De WireGuard-tunnels tussen je machines veranderen niet en de client blijft dezelfde tailscale-binary. Er is geen importknop en geen migratietool. Je registreert elke node opnieuw met tailscale logout en daarna tailscale up --login-server, en alles wat in de admin console van Tailscale stond bouw je met de hand opnieuw op.

Deze stappen gaan uit van een Headscale-server die al draait en bereikbaar is op een publieke URL met een geldig TLS-certificaat (transport layer security). Het opzetten daarvan staat in de handleiding voor Headscale op een eigen VPS. Twijfel je nog of je zelf een controlserver wilt draaien, kijk dan eerst naar de andere opties naast Tailscale. Na de cutover ben jij degene die de back-ups, de updates en de uptime van dat ene stukje infrastructuur regelt.

Pin een release en lees de documentatie van die tag

Headscale verschuift per minor release welke Tailscale-functies werken. Pin dus een vaste tag en lees de documentatie uit die tag, niet de nieuwste pagina op de website. Hieronder is dat v0.29.3, uitgebracht op 29 juli 2026.

Twee punten uit de release notes van v0.29.3 raken precies deze klus. Getagde nodes die na tailscale logout als verlopen bleven hangen en zich niet opnieuw konden aanmelden, zijn gerepareerd. Datzelfde geldt voor onterechte 401 registration timed out-fouten wanneer de registratie afrondde terwijl de request-context al verlopen was. Een oudere Headscale draaien tijdens een migratie betekent dat je die twee bugs tegenkomt op het moment dat je ze het minst kunt gebruiken.

Let ook op de minimale clientversie. Headscale v0.29.3 ondersteunt Tailscale-clients vanaf v1.80.0 en weigert /key-verzoeken van clients onder die drempel. Een NAS of router met een client van jaren geleden moet je dus eerst bijwerken, want anders registreert die node zich nooit.

Wat Headscale wel en niet overneemt van Tailscale

De featurelijst in docs/about/features.md van v0.29.3 is hiervoor de eerlijkste bron, want de maintainers vinken daar per functie aan wat af is. Aangevinkt staan onder meer: node-registratie via webauthenticatie en via pre-auth keys, MagicDNS met split DNS en search domains, Taildrop en Taildrive, tags, subnet routers, exit nodes, route filtering met Via, dual stack IPv4 en IPv6, ephemeral nodes, een ingebouwde DERP-server (designated encrypted relay for packets, de relay die verkeer doorgeeft als twee nodes geen directe verbinding krijgen), peer relays, policy met ACLs (access control lists) en grants, Tailscale SSH, en registratie via OIDC (OpenID Connect).

Niet aangevinkt staan er drie, elk met een open issue erbij: Funnel (#1040), Serve, en network flow logs (#1687). Daarnaast staat er expliciet dat OIDC-groepen niet in ACLs gebruikt kunnen worden.

Belangrijker is wat er helemaal niet in die lijst voorkomt. Tailnet lock, de admin console en app connectors worden er in geen enkele vorm genoemd. Reken er dus niet op. Gebruik je nu tailnet lock, dan verlies je die laag bij de overstap, en het loont om eerst te lezen wat tailnet lock nu eigenlijk tegenhoudt voordat je besluit of je zonder kunt. Voor de admin console is het antwoord de headscale-CLI op de server, of een los webproject van derden dat je er zelf bij zet en zelf beveiligt. Wil je iets publiek bereikbaar maken zonder Funnel, zet dan een reverse proxy met TLS voor die dienst; het verschil tussen Serve en Funnel maakt duidelijk welke van de twee je eigenlijk miste.

Ook de policy zelf kent grenzen. Device postures met de velden postures en srcPosture werken niet, IP sets werken niet, en van de autogroups worden alleen autogroup:internet, autogroup:member, autogroup:tagged, autogroup:self, autogroup:nonroot en autogroup:danger-all ondersteund.

Maak eerst een inventaris van je tailnet

Uit een opgezegd Tailscale-account haal je niets meer terug, dus leg alles vast zolang het nog draait. Begin op een machine die al in de tailnet zit.

tailscale status
tailscale status --json > ~/tailnet-inventaris.json
tailscale netcheck

tailscale status toont per node het Tailscale-IP, de machinenaam, de eigenaar en de verbindingsstatus. De JSON-variant bevat hetzelfde in een vorm die je kunt bewaren en na de cutover naast de nieuwe situatie kunt leggen. Bewaar dat bestand buiten de tailnet, want tijdens de verhuizing is precies die tailnet even onbetrouwbaar.

Haal daarna deze vijf dingen uit de admin console en zet ze in dezelfde map:

  • Het tailnet policy file van de pagina Access controls, in huJSON (JSON met commentaar en komma's aan het eind toegestaan). Dit wordt straks je Headscale-policy.
  • De machinelijst, inclusief welke nodes getagd zijn en met welke tag.
  • Welke node de exit node is, welke nodes subnet routes adverteren, en om welke prefixes het gaat.
  • De DNS-instellingen: nameservers, split DNS, search domains, en of MagicDNS aanstaat.
  • De gebruikers met hun e-mailadres, want die namen komen terug in de policy.

Noteer ten slotte elke plek buiten Tailscale waar een *.ts.net-naam staat: SSH-configs, monitoring, cronjobs, backupscripts, reverse proxies. Die namen veranderen allemaal, en dat is de stap die het vaakst wordt overgeslagen.

Zet het ACL-bestand om naar een Headscale-policy

Headscale gebruikt hetzelfde huJSON-formaat als Tailscale, dus het grootste deel van je bestand kun je letterlijk overnemen. Er blijven twee soorten aanpassingen over.

Gebruikers worden anders aangeduid. In Tailscale schrijf je alice@example.com. In de Headscale-policy is een gebruiker de gebruikersnaam met een apenstaartje erachter, dus alice@. Die naam moet gelijk zijn aan de user die je zo aanmaakt.

En alles wat niet ondersteund wordt, moet eruit: postures, srcPosture, IP sets, en autogroups buiten de lijst hierboven. Laat je ze staan, dan keurt headscale policy check het bestand af en heb je niets herladen.

{
  "groups": {
    "group:beheer": ["alice@", "bob@"]
  },
  "tagOwners": {
    "tag:router": ["group:beheer"],
    "tag:web": ["group:beheer"]
  },
  "acls": [
    {
      "action": "accept",
      "src": ["group:beheer"],
      "dst": ["tag:web:80,443"]
    },
    {
      "action": "accept",
      "src": ["autogroup:member"],
      "dst": ["autogroup:internet:*"]
    }
  ],
  "autoApprovers": {
    "routes": {
      "192.168.1.0/24": ["tag:router"]
    },
    "exitNode": ["tag:router"]
  }
}

Zet dat bestand op de server en wijs het aan in /etc/headscale/config.yaml:

policy:
  mode: file
  path: /etc/headscale/policy.json

Controleer het bestand voordat je iets herlaadt, en herlaad daarna pas:

headscale policy check -f /etc/headscale/policy.json
sudo systemctl reload headscale
headscale policy get

headscale policy check leest het bestand en meldt fouten zonder iets te wijzigen. headscale policy get drukt af welke policy de server op dit moment werkelijk gebruikt, en dat is het enige echte bewijs dat je reload is aangekomen. Wijkt die uitvoer af van je bestand, dan staat policy.mode waarschijnlijk nog op database in plaats van op file.

Users en pre-auth keys aanmaken

headscale users create alice --email alice@example.com
headscale users list

headscale users list toont per gebruiker een numerieke ID, en die heb je nodig: headscale preauthkeys create neemt met --user een ID, geen naam.

headscale preauthkeys create --user 1 --expiration 24h
headscale preauthkeys create --user 1 --expiration 24h --tags tag:router
headscale preauthkeys list

De standaardgeldigheid is één uur. Voor een migratie die je over een avond of een weekend uitsmeert is dat te kort, dus zet --expiration bewust. Met --reusable werkt één sleutel voor meerdere nodes, wat handig is voor een reeks machines van dezelfde gebruiker en tegelijk riskanter, omdat die sleutel dan rondgaat. Met --ephemeral maak je nodes die zichzelf opruimen zodra ze offline gaan: goed voor containers, slecht voor je VPS.

Een pre-auth key is een geheim waarmee iemand een machine aan jouw netwerk hangt. Plak hem dus niet in een chatbericht of een gedeeld document, maar geef hem door via je wachtwoordmanager.

Node voor node omzetten met tailscale logout en tailscale up

Per node zijn het twee commando's.

sudo tailscale logout
sudo tailscale up --login-server https://headscale.example.com --authkey <pre-auth-key>

tailscale logout verbreekt de verbinding en laat de huidige login vervallen, zodat de node zich opnieuw moet aanmelden. Let op bij ephemeral nodes: die verdwijnen bij een logout meteen uit je tailnet. --login-server geeft de basis-URL van een controlserver in plaats van https://controlplane.tailscale.com. Dat is de hele migratie, per machine.

Een getagde node registreer je met de bijbehorende sleutel plus de tags:

sudo tailscale up --login-server https://headscale.example.com --authkey <sleutel> --advertise-tags tag:router

Controleer daarna aan beide kanten:

tailscale status
tailscale netcheck
headscale nodes list

tailscale status hoort de andere nodes te tonen met hun nieuwe adressen, en headscale nodes list hoort de node bij de juiste gebruiker te laten zien. Staat de node wel op de server maar krijg je geen verbinding met peers, lees dan het rapport van tailscale netcheck: dat toont of UDP werkt, wat voor NAT (network address translation) ertussen zit, en de latency naar de DERP-relays. Een node die alleen via DERP praat werkt wel, maar is trager dan een directe verbinding.

Wil je zonder pre-auth key werken, dan draait de node tailscale up --login-server <url> zonder --authkey en keur je de aanvraag op de server goed met de ID die de client afdrukt:

headscale auth register --user alice --auth-id <auth-id>

Subnet routes en exit node opnieuw goedkeuren

Adverteren gebeurt op de node, goedkeuren op de server. Beide moet je opnieuw doen, want een goedkeuring uit de Tailscale-console komt niet mee.

Op de subnet router:

sudo tailscale set --advertise-routes=192.168.1.0/24

Op de exit node:

sudo tailscale set --advertise-exit-node

Op de server:

headscale nodes list-routes
headscale nodes approve-routes --identifier 1 --routes 192.168.1.0/24

--identifier is de ID uit headscale nodes list. Staat de autoApprovers uit het voorbeeld hierboven in je policy en is de node met tag:router geregistreerd, dan is de goedkeuring al gebeurd en zie je de route meteen als goedgekeurd in headscale nodes list-routes.

Aan de clientkant moet je routes nog accepteren en de exit node kiezen:

sudo tailscale set --accept-routes
sudo tailscale set --exit-node=<naam of Tailscale-IP>
sudo tailscale set --exit-node=

Het laatste commando, met een lege waarde, zet het gebruik van een exit node weer uit. Dat heb je nodig op het moment dat je de exit node zelf gaat omzetten, want zolang clients hun verkeer nog door die node sturen valt hun internet weg zodra hij uitlogt. De configuratie op de node zelf verandert verder niet: wat je eerder instelde om al je verkeer via een VPS te laten lopen blijft geldig, net als het adverteren van een privérange vanaf een subnet router. Alleen de goedkeuring verhuist van de console naar je eigen CLI of policy. IP-forwarding is een instelling van het besturingssysteem en geen Tailscale-instelling, dus die blijft staan zoals hij stond.

MagicDNS: je namen veranderen wel

Onder Tailscale heet een machine web.tailnet-naam.ts.net. Onder Headscale wordt dat web.<base_domain>, met het domein dat jij in config.yaml zet.

dns:
  magic_dns: true
  base_domain: intern.example.com
  override_local_dns: true
  nameservers:
    global:
      - 1.1.1.1
      - 1.0.0.1
    split: {}
  search_domains: []
  extra_records: []

base_domain moet een FQDN zijn zonder punt aan het eind, en de documentatie is stellig: het moet een ander domein zijn dan dat van server_url. De hostnaam wordt dan hostname.base_domain. Onder nameservers.split zet je split DNS per domein, en extra_records vervangt de statische records die je eventueel in de Tailscale-console had staan. Alleen A- en AAAA-records worden door de Tailscale-client verwerkt.

Ieder script, iedere SSH-config, iedere monitoringcheck en iedere reverse proxy die een *.ts.net-naam noemt, breekt op het moment dat je die node omzet. Zoek ze dus op voordat je begint. Werkt de tunnel na de cutover wel maar de naamsopvraging niet, dan zit het probleem vrijwel altijd aan de resolverkant en niet bij WireGuard. De stappen om DNS over een WireGuard-tunnel weer werkend te krijgen gelden hier één op één.

De volgorde die je niet moet omdraaien

Migreer op volgorde van hoeveel pijn het doet als een node onbereikbaar raakt.

  • Begin met iets wegwerpbaars: een test-VM of een laptop die naast je staat en die je fysiek kunt bereiken.
  • Daarna de gewone clients: laptops, telefoons, desktops.
  • Daarna de diensten waar mensen op wachten, buiten de drukke uren.
  • Daarna de subnet router en de exit node, want die twee raken het verkeer van alle andere nodes tegelijk.
  • Als laatste de machine waarvandaan je de rest beheert.

De harde regel: knip nooit het laatste pad naar een VPS op afstand door. Bereik je een server alleen via de tailnet, dan is tailscale logout op die machine het commando waarmee je jezelf buitensluit, en dan is er niets meer waarmee je het tweede commando kunt geven. Zorg eerst voor een tweede weg naar binnen: SSH op het publieke IP-adres vanaf een adres dat je firewall toelaat, of de console van je provider. Test die weg terwijl de tailnet nog werkt. Voer de omzetting daarna uit via dat tweede pad en niet via de tunnel die je op het punt staat te verbreken.

Terugvallen als er iets misgaat

Houd je Tailscale-account in de lucht totdat elke node opnieuw geregistreerd is en je hem daadwerkelijk bereikt hebt. Zeg niets op, verwijder geen machines en downgrade je plan niet zolang de migratie loopt. Een opgezegd account is geen rollback meer, dat is een herinstallatie.

Terugvallen op één node is dezelfde handeling andersom:

sudo tailscale logout
sudo tailscale up

Zonder --login-server gebruikt de client weer de standaard controlserver op https://controlplane.tailscale.com en loopt de gewone login. De node verschijnt terug in je Tailscale-console, met een nieuw Tailscale-IP en onder de ACL-regels die daar nog staan.

Kijk voor je begint ook welke profielen een machine al kent:

tailscale switch --list

Dat toont de accounts die de client kent, en met tailscale switch <naam> wissel je ertussen. Op een testmachine waar je beide kanten naast elkaar wilt bekijken is dat prettiger dan steeds opnieuw uitloggen.

Plan ten slotte wat je met de sleutels en de gegevens doet. Bij Tailscale hield de controlserver van iemand anders de node-keys bij. Nu doe jij dat, inclusief de back-up van de Headscale-database en de policy. Dat verschuift je aanvalsoppervlak, dus het is de moeite waard om te weten welke sleutels een controlserver eigenlijk wel en niet in handen heeft voordat je die rol overneemt. Concludeer je halverwege dat een eigen controlserver meer werk is dan het oplevert, leg dan kale WireGuard naast een tailnet: zonder controlserver vervalt dit hele probleem, en krijg je handmatig sleutelbeheer terug.

FAQ

Moet ik de Tailscale-client opnieuw installeren voor Headscale?

Nee. Headscale praat met dezelfde officiële Tailscale-client. Alleen de controlserver verandert, via tailscale up --login-server. Er geldt wel een ondergrens: Headscale v0.29.3 ondersteunt clients vanaf v1.80.0 en weigert /key-verzoeken van clients onder de minimale capability-versie. Werk oude clients op een NAS of router dus eerst bij, anders registreert die node zich niet.

Werken Funnel en Serve op Headscale?

Volgens de featurelijst van v0.29.3 niet. Funnel staat daar als openstaand met issue #1040, Serve staat er eveneens als openstaand met een eigen issue, en network flow logs ook. Wil je een dienst publiek bereikbaar maken, zet dan een reverse proxy met TLS voor die dienst op een machine met een publiek adres, in plaats van op Funnel te wachten.

Kan ik mijn Tailscale-ACL letterlijk kopiëren naar Headscale?

Grotendeels wel, want Headscale gebruikt hetzelfde huJSON-formaat. Twee dingen moet je wijzigen. Gebruikers heten in de Headscale-policy alice@ in plaats van alice@example.com, en niet-ondersteunde onderdelen moeten eruit: postures, srcPosture, IP sets, en autogroups buiten de ondersteunde set. Draai headscale policy check -f /etc/headscale/policy.json voordat je herlaadt, want dat commando leest het bestand en wijzigt niets.

Wat gebeurt er met tailnet lock en de admin console?

De featurelijst van v0.29.3 noemt tailnet lock, de admin console en app connectors niet, in geen enkele vorm. Ze horen dus niet bij wat Headscale implementeert. Beheer gaat met de headscale-CLI op de server, of met een webproject van derden dat je er zelf bij zet en zelf afschermt. Gebruik je nu tailnet lock, reken er dan op dat die extra ondertekeningslaag na de overstap weg is.