Headscale zelf hosten als Tailscale-controlserver
Beheer uw eigen Tailscale-controlserver op een VPS. Installeer headscale via de officiële .deb, stel server_url in en voeg uw eerste node toe.
Wat headscale is
Headscale is een zelfgehoste implementatie van de Tailscale-controlserver. De machine die uw privénetwerk coördineert, is daardoor een VPS die u zelf beheert. Het is een communityproject en wordt niet beheerd door Tailscale Inc. Elke machine gebruikt nog steeds de officiële tailscale-client, die u met één flag naar uw server laat wijzen: --login-server.
De controlserver bepaalt welke apparaten tot het netwerk behoren. De server wijst elk node een adres uit 100.64.0.0/10 toe, verspreidt openbare sleutels en geeft nodes door waar ze elkaar kunnen vinden. De tunnels blijven WireGuard-tunnels die rechtstreeks tussen nodes worden opgebouwd. Verkeer tussen twee van uw machines gaat niet via de headscale-server, tenzij er geen directe verbinding kan worden opgebouwd en de nodes terugvallen op een relay.
Headscale ondersteunt per instance één tailnet (één Tailscale-netwerk). Volgens het project is dit geschikt voor persoonlijk gebruik of een kleine organisatie. Met drie of vier machines is een gewone WireGuard-VPN op een VPS die u zelf beheert minder software om te beheren en minder software die kan uitvallen. Headscale is nuttig wanneer u niet langer voor elke nieuwe laptop handmatig een [Peer]-blok wilt schrijven. Zie voor een bredere vergelijking van de twee modellen hoe WireGuard en Tailscale van elkaar verschillen.
Wat u nodig hebt voordat u installeert
- Een VPS met Ubuntu 24.04, een openbaar IPv4-adres en sudo-toegang. Als de server nieuw is, doorloop dan eerst de eerste tien minuten op een nieuwe VPS.
- Een DNS A-record dat naar dat adres verwijst. In deze handleiding wordt
headscale.example.comgebruikt. - Een tweede domein of subdomein voor MagicDNS. In deze handleiding wordt
tailnet.example.netgebruikt. Dit mag niet hetzelfde domein zijn als het domein inserver_url. - Eén clientcomputer om toe te voegen, met Linux, macOS, Windows, Android of iOS.
headscale installeren vanuit de officiële .deb
Het project publiceert .deb-pakketten op de GitHub-releasespagina. In juli 2026 is de huidige release 0.29.3. Controleer eerst uw architectuur, omdat de bestandsnaam deze bevat.
sudo apt update
sudo apt install -y wget
dpkg --print-architectureDit geeft amd64 weer op een gewone x86-VPS en arm64 op een plan van het type Ampere of Graviton. Plaats het antwoord in de onderstaande variabele.
HEADSCALE_VERSION="0.29.3"
HEADSCALE_ARCH="amd64"
wget --output-document=headscale.deb \\
"https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb"
sudo apt install -y ./headscale.deb
headscale version./ vóór de bestandsnaam is vereist. Zonder deze optie zoekt apt naar een pakket met de naam headscale.deb in uw repositories en mislukt de opdracht.
Het pakket maakt de systeemgebruiker headscale aan, schrijft een standaard-/etc/headscale/config.yaml en installeert een systemd-unit. Het start de service niet, en dat is de juiste volgorde. De meegeleverde configuratie verwijst server_url naar http://127.0.0.1:8080. Dit is geen adres dat een van uw clients kan bereiken. Een service die nu wordt gestart, zou dus onjuist zijn, ook als deze zou starten. Als u op dit punt sudo systemctl is-active headscale uitvoert, wordt inactive weergegeven. Dat is verwacht gedrag en geen fout.
Configureer server_url voordat u de service start
Bewerk /etc/headscale/config.yaml met sudo nano /etc/headscale/config.yaml of pas dezelfde drie wijzigingen toe met sed. Bewaar een kopie van het origineel, omdat het bestand lang is en veel opmerkingen bevat. Het is uw beste naslagwerk voor de overige instellingen.
sudo cp /etc/headscale/config.yaml /etc/headscale/config.yaml.orig
sudo sed -i 's|^server_url:.*|server_url: https://headscale.example.com|' /etc/headscale/config.yaml
sudo sed -i 's|^listen_addr:.*|listen_addr: 127.0.0.1:8080|' /etc/headscale/config.yaml
sudo sed -i 's|^ base_domain:.*| base_domain: tailnet.example.net|' /etc/headscale/config.yaml
sudo grep -E '^(server_url|listen_addr):|^ base_domain:' /etc/headscale/config.yamlserver_url is het adres dat headscale in elke clientregistratie schrijft. Clients maken daarna altijd verbinding met exact die tekenreeks. Gebruik daarom de publieke naam met https:// ervoor, en nooit 127.0.0.1.
listen_addr bepaalt waarop het proces luistert. Laat dit op loopback staan. Een reverse proxy op dezelfde server beëindigt TLS (transport layer security) en stuurt verzoeken hiernaartoe door. Niets buiten de server hoeft daarom poort 8080 te bereiken.
base_domain is het MagicDNS-achtervoegsel: het domein waaronder uw nodes namen krijgen. Dit moet een volledig gekwalificeerde domeinnaam zonder afsluitende punt zijn. Het moet verschillen van het domein in server_url, omdat de twee naamruimten anders zouden botsen.
Laat de databasesectie ongewijzigd. De standaardinstelling gebruikt SQLite op /var/lib/headscale/db.sqlite, in een map die door het pakket is aangemaakt en waarvan het pakket eigenaar is. SQLite is voldoende voor een tailnet van deze omvang.
headscale starten en aantonen dat het actief is
sudo systemctl enable --now headscale
sudo systemctl is-active headscale
curl -sS -o /dev/null -w '%{http_code}\\n' http://127.0.0.1:8080/healthis-active geeft active weer en curl geeft 200 weer. enable --now voert beide delen uit: het start de service en zorgt dat deze na een herstart automatisch start.
Als is-active failed weergeeft, leest u het journal met sudo journalctl -u headscale -n 50 --no-pager. Een fout in deze fase wordt vrijwel altijd veroorzaakt door het configuratiebestand. headscale verwerkt het volledige bestand voordat het een socket opent. Een verkeerde inspringing of een onbekende sleutel stopt het proces daarom voordat er iets op een poort luistert. Corrigeer het bestand en voer daarna sudo systemctl restart headscale uit. Voor elke latere configuratiewijziging is dezelfde herstart nodig. Clients maken daarna automatisch opnieuw verbinding. Als systemd-units nieuw voor u zijn, behandelt uw eigen services en timers uitvoeren met systemd de hier gebruikte opdrachten.
Controleer de statusbestanden terwijl u nog in de shell werkt:
stat -c '%U %n' /var/lib/headscale/db.sqlite /var/lib/headscale/noise_private.keyBeide regels beginnen met headscale, de gebruiker zonder beheerdersrechten die het pakket heeft aangemaakt. noise_private.key is de identiteit van de server voor de clients. Bewaar dit bestand. Als u het verwijdert, genereert headscale een nieuwe identiteit en moet elk node zich opnieuw registreren.
TLS vóór headscale plaatsen
Clients moeten server_url via HTTPS kunnen bereiken. Caddy is de kortste route, omdat het certificaat automatisch aanvraagt en vernieuwt.
sudo apt install -y caddyVervang /etc/caddy/Caddyfile door het blok uit de headscale-documentatie:
headscale.example.com {
reverse_proxy 127.0.0.1:8080 {
header_up True-Client-IP {remote_host}
header_up X-Real-IP {remote_host}
}
}sudo caddy validate --adapter caddyfile --config /etc/caddy/Caddyfile
sudo systemctl restart caddy
sudo systemctl is-active caddyvalidate geeft adapted config to JSON weer wanneer het bestand correct wordt verwerkt. Een waarschuwing dat het bestand niet is geformatteerd, is alleen cosmetisch. Vanaf uw laptop moet curl -sS -o /dev/null -w '%{http_code}\\n' https://headscale.example.com/health ook 200 weergeven. Met die ene controle weet u zeker dat DNS, de firewall, het certificaat en de proxy samenwerken.
Hier zit het proxyprobleem waardoor mensen een avond verliezen. De Tailscale-controlverbinding is een HTTP-upgrade. Deze wordt gestart met POST in plaats van GET. De waarde van de header Upgrade is tailscale-control-protocol. Caddy geeft dit zonder extra configuratie door. nginx doet dat niet. Een nginx-front-end heeft daarom de volgende upgrade-map nodig:
map $http_upgrade $connection_upgrade {
default keep-alive;
'' close;
}
server {
listen 443 ssl;
server_name headscale.example.com;
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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_buffering off;
proxy_pass http://127.0.0.1:8080;
}
}Laat u deze regels weg, dan blijven gewone aanvragen slagen. Daarom geeft /health 200 terug en lijkt alles goed te werken. De langdurige controlverbinding wordt echter nooit tot stand gebracht. Uw nodes registreren zich vervolgens en blijven offline. Als u voor nginx kiest, behandelt Certbot op Ubuntu 24.04 met nginx het certificaatgedeelte.
Welke poorten u in UFW moet openen
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verbosePoort 443 verwerkt alle clientcommunicatie. Poort 80 is alleen nodig voor de ACME (automatic certificate management environment)-HTTP-challenge en de omleiding naar HTTPS. Caddy heeft deze poort ook nodig om überhaupt een certificaat te verkrijgen.
Poort 8080 blijft gesloten. listen_addr is 127.0.0.1:8080. De proxy bereikt headscale daarom via de loopbackinterface en er is geen firewallregel nodig. Als u poort 8080 naar internet openstelt, krijgen clients een onbeveiligd besturingskanaal en levert dat niets op. Houd er rekening mee dat de meeste providers in hun configuratiescherm een tweede firewall gebruiken die losstaat van UFW. Een poort kan dus op de server open zijn, maar aan de rand van het netwerk toch gesloten blijven. Basisprincipes van de UFW-firewall op een VPS behandelt de syntaxis van regels uitgebreider.
Een gebruiker en een preauth-sleutel maken
sudo headscale users create alice
sudo headscale users listDe opdracht headscale is een client. Deze communiceert via de Unix-socket op /var/run/headscale/headscale.sock met de actieve daemon. De socket heeft modus 0770 en is eigendom van de groep headscale. Dit heeft twee gevolgen. De opdracht mislukt als de service is gestopt. Dat is een andere reden waarom de volgorde in deze handleiding belangrijk is. Voor de opdracht is ook sudo nodig, tenzij u uw eigen account aan de groep headscale toevoegt.
users list geeft naast elke naam een ID weer. U hebt dat nummer nodig, omdat de key-opdracht een numerieke gebruikers-ID en geen naam gebruikt.
sudo headscale preauthkeys create --user 1 --expiration 24hDe key wordt slechts eenmaal weergegeven. Kopieer deze nu. Een preauth-sleutel kan slechts eenmaal worden gebruikt en is één uur geldig, tenzij u dit anders instelt. Daarom is het verstandig om --expiration 24h in te stellen zolang u nog test. Voeg --reusable toe voor een sleutel waarmee u meerdere machines inschrijft. Behandel deze sleutel als een wachtwoord, omdat iedereen die de sleutel bezit, uw netwerk kan toetreden.
Verbind uw eerste client met --login-server
Op de machine die u wilt toevoegen:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server https://headscale.example.com --auth-key 'hskey-auth-PASTE-YOUR-KEY-HERE'
tailscale status
tailscale ip -4tailscale ip -4 toont het adres dat headscale heeft toegewezen, bijvoorbeeld 100.64.0.1. Ga terug naar de server. sudo headscale nodes list toont het knooppunt met de ID, de gebruiker en de online status.
De waarde van --login-server moet exact overeenkomen met server_url, inclusief het schema en zonder afsluitende slash. De waarden worden als tekenreeksen vergeleken. Bij een afwijking registreert de client zich bij het ene adres en krijgt deze vervolgens de opdracht om met een ander adres te communiceren.
Een machine waarop u eerder bij de gehoste service van Tailscale bent aangemeld, behoudt die aanmelding. Voer daarop eerst sudo tailscale logout uit en voer vervolgens tailscale up uit met --login-server.
Als u --auth-key weglaat, toont de client een URL. Open deze URL. De pagina toont de identifier voor die registratiepoging. U keurt deze vervolgens op de server goed:
sudo headscale auth register --user alice --auth-id PASTE-THE-ID-FROM-THE-PAGEDeze methode is handiger voor uw eigen laptop. Preauth-sleutels zijn beter voor alles wat u scriptt, omdat er dan geen persoon hoeft toe te kijken.
DERP en wat verkeer doorstuurt wanneer een direct pad mislukt
DERP (designated encrypted relay for packets) is het terugvalpad. Wanneer twee nodes geen directe WireGuard-verbinding kunnen openen, meestal omdat beide achter strikte NAT (network address translation) staan, sturen ze pakketten via een relay. De relay bevat geen sleutels en kan uw verkeer dus niet lezen. De relay ziet wel welke nodes met elkaar communiceren en hoeveel gegevens worden verzonden.
Het is belangrijk om te weten wat de standaardconfiguratie doet. Headscale wordt geleverd met een verwijzing naar https://controlplane.tailscale.com/derpmap/default, met auto_update_enabled: true en update_frequency: 3h. Uw control plane blijft daardoor van u, terwijl de relays van Tailscale zijn. Voor de meeste gebruikers is dat een redelijke afweging. Als dat voor u niet geldt, kunt u zelf een relay uitvoeren.
Als u zelf een relay wilt uitvoeren, stelt u enabled: true in onder derp.server in config.yaml, start u headscale opnieuw en opent u de STUN-poort (session traversal utilities for NAT) met sudo ufw allow 3478/udp. Het configuratiebestand vermeldt deze vereiste duidelijk: server_url moet https gebruiken, omdat DERP TLS vereist. Als u de lijst derp.urls leegmaakt, verwijdert u de relays van Tailscale uit de map. Als u dat doet zonder een werkende ingebedde relay, kunnen nodeparen die niet rechtstreeks verbinding kunnen maken helemaal geen verbinding meer maken.
Vanaf een client toont tailscale netcheck de latentie naar elke relayregio die deze kent. tailscale status markeert elke peer als direct met een adres of als relay met een regiocode. Een peer die op relay blijft staan, wijst op een NAT-probleem en niet op een probleem met headscale.
Waarom wordt een node als offline weergegeven?
De proxy verwijdert de upgrade. Dit is de meest voorkomende oorzaak. De kenmerkende situatie is dat al het andere goed werkt: /health retourneert 200, headscale nodes list toont de node en de node komt nooit online. De beheerverbinding is een POST met Upgrade: tailscale-control-protocol. Een proxy die deze verbinding niet doorstuurt, verbreekt het enige kanaal dat de status van de node rapporteert. Vergelijk uw nginx-configuratie met het bovenstaande map-blok of schakel over naar Caddy om de proxy als oorzaak uit te sluiten.
server_url is gewijzigd nadat de nodes zich hadden geregistreerd. Nodes blijven verbinding maken met de waarde die ze tijdens de registratie hebben ontvangen. Als u deze waarde hebt gewijzigd, voert u op elke node sudo tailscale up --login-server https://headscale.example.com --force-reauth uit.
De client draait niet. Voer op de node sudo systemctl is-active tailscaled en sudo journalctl -u tailscaled -n 50 --no-pager uit. Een client die uw domein niet kan omzetten of bereiken, logt zijn nieuwe verbindingspogingen daar.
De key is verlopen. Dit wordt in de volgende sectie behandeld.
Voer sudo journalctl -u headscale -f uit op de VPS en start tailscaled opnieuw op de client om de serverzijde tijdens uw test te monitoren. Een node die headscale bereikt, produceert onmiddellijk logregels. Stilte betekent dat het verzoek niet aankomt. Controleer daarom eerst DNS, de firewall en de proxy voordat u headscale controleert.
Verloop van sleutels en de node die weken later niet meer werkt
Er bestaan 2 afzonderlijke verlooptijden. Als u deze door elkaar haalt, kost dat tijd.
Preauth-sleutels verlopen bewust snel. De standaardwaarde is 1 uur en 1 gebruik. Als tailscale up de sleutel weigert, genereert u een nieuwe sleutel op de server in plaats van iets op de client aan te passen.
Node-sleutels zijn het langdurige onderdeel. De sectie node van config.yaml stelt expiry: 0 in, en 0 betekent dat er geen standaardverloopdatum is: een geregistreerde node blijft geldig totdat u deze laat verlopen. Nodes met een tag verlopen nooit. Stel expiry: 180d in als u wilt dat registraties na verloop van tijd vervallen. Houd rekening met wat u hiermee instelt: elke node zonder tag heeft dan volgens dat schema sudo tailscale up --login-server https://headscale.example.com --force-reauth nodig, en een headless server waarop niemand opnieuw authenticatie uitvoert, valt vanzelf van het netwerk.
Doe dit handmatig wanneer iemand een laptop verliest. sudo headscale nodes list geeft u de ID. Vervolgens meldt sudo headscale nodes expire -i 3 die node af en verwijdert sudo headscale nodes delete -i 3 deze volledig uit het netwerk.
Back-ups en upgrades
/var/lib/headscale en /etc/headscale vormen samen de volledige server. Stop de service voordat u deze kopieert, omdat SQLite mogelijk nog schrijfbewerkingen uitvoert en een database die tijdens belasting wordt gekopieerd inconsistent kan zijn.
sudo systemctl stop headscale
sudo tar czf /root/headscale-state.tgz -C /var/lib headscale
sudo tar czf /root/headscale-config.tgz -C /etc headscale
sudo systemctl start headscale
sudo chmod 600 /root/headscale-*.tgzVerplaats beide bestanden van de server. Ze bevatten de private keys en alle registraties. Behandel ze daarom met dezelfde zorg als de server zelf. In restic-back-ups vanaf een VPS leest u hoe u dit volgens een schema en versleuteld uitvoert.
Upgrades volgen opnieuw de installatieprocedure: download de nieuwe .deb en sudo apt install ./headscale.deb, start de service opnieuw en voer de controles is-active en /health opnieuw uit. Sinds 0.29 is het upgradepad strikt. Het overslaan van een minorversie wordt geblokkeerd. Een downgrade naar een oudere minorversie wordt ook geblokkeerd. Ga telkens één minorversie verder, maak vóór elke stap een back-up en lees eerst de release notes van die versie. In die release is namelijk ook het gedrag van het ACL-beleid gewijzigd en zijn meerdere configuratiesleutels verplaatst.
FAQ
Waarom start headscale niet direct nadat ik de .deb heb geïnstalleerd?
Het pakket installeert de unit, maar laat de service gestopt. Bovendien is de standaard /etc/headscale/config.yaml een sjabloon en geen werkende configuratie. Bewerk eerst server_url, listen_addr en base_domain. Voer daarna sudo systemctl enable --now headscale uit en controleer het resultaat met sudo systemctl is-active headscale. Als het nog steeds mislukt, vermeldt sudo journalctl -u headscale -n 50 --no-pager het probleem. In dit stadium is dat vrijwel altijd een YAML-fout, omdat headscale het volledige bestand parseert voordat het een poort opent.
Moet ik de normale Tailscale-client nog steeds op mijn machines installeren?
Ja. Headscale vervangt alleen de control server. Elke node gebruikt de officiële client van Tailscale. U verwijst die client met sudo tailscale up --login-server https://headscale.example.com naar uw server. Die flag is beschikbaar in de standaardclient. U hoeft daarom niets te patchen of opnieuw te bouwen.
Loopt mijn verkeer via de headscale-server?
Meestal niet. Headscale coördineert het netwerk en verstrekt cryptografische sleutels en adressen. Het datapad gebruikt WireGuard rechtstreeks tussen uw nodes. Verkeer neemt alleen een omweg wanneer twee nodes elkaar niet rechtstreeks kunnen bereiken en terugvallen op een DERP-relay. Met de meegeleverde configuratie zijn dat de openbare relays van Tailscale. Voer tailscale status uit op een node om te zien of een bepaalde peer direct is of zich op een relay bevindt.
Waarom blijft mijn node offline nadat deze is geregistreerd?
Een node die in headscale nodes list verschijnt maar nooit online komt, heeft meestal de control connection bij de reverse proxy verloren. Die verbinding is een HTTP-upgrade die als POST wordt verzonden met de header Upgrade: tailscale-control-protocol. nginx laat deze verbinding vallen tenzij u het blok map $http_upgrade $connection_upgrade en de bijbehorende regels proxy_set_header toevoegt. Caddy stuurt de verbinding zonder extra configuratie door. Daarmee kunt u snel testen of de proxy het probleem veroorzaakt.
Heb ik een domeinnaam en TLS nodig voor headscale?
In de praktijk wel. Clients maken verbinding met de tekenreeks die u in server_url opgeeft. Certificaten worden uitgegeven voor namen en niet voor kale IP-adressen. Bovendien vermeldt het configuratiebestand dat DERP TLS vereist. Met een domein en Caddy hebt u dit in ongeveer vijf minuten ingericht. U krijgt dan een HTTPS-endpoint dat zichzelf vernieuwt. Als u de control server via gewone HTTP uitvoert, verloopt alle communicatie van clients ermee onversleuteld via internet.