Installer Headscale pour auto-héberger votre Tailscale
Déployez votre serveur de contrôle Tailscale sur un VPS : installez headscale avec le paquet .deb officiel, renseignez server_url et joignez votre premier nœud.
Présentation de headscale
Headscale est une implémentation auto-hébergée du serveur de contrôle de Tailscale. La machine qui coordonne votre réseau privé est donc un VPS que vous possédez. Il s’agit d’un projet communautaire qui n’est pas géré par Tailscale Inc. Chaque machine exécute toujours le client officiel tailscale, configuré pour utiliser votre serveur avec un seul flag : --login-server.
Le serveur de contrôle connaît les machines qui appartiennent au réseau. Il attribue à chaque nœud une adresse dans 100.64.0.0/10, distribue les clés publiques et indique aux nœuds comment se trouver entre eux. Les tunnels utilisent toujours WireGuard et sont établis directement entre les nœuds. Le trafic entre deux de vos machines ne passe pas par le serveur headscale, sauf si un chemin direct ne peut pas être établi et que les nœuds utilisent un relay. En assurant vous-même ce rôle de coordination, vous changez l’entité qui l’exerce, mais pas ses capacités. Il est donc utile de comprendre ce qu’un serveur de contrôle peut et ne peut pas atteindre dans ce modèle avant de considérer ce changement comme un gain de sécurité en soi.
Chaque instance de headscale sert un seul tailnet, c’est-à-dire un seul réseau Tailscale. Le projet le présente comme adapté à un usage personnel ou à une petite organisation. Avec trois ou quatre machines, un VPN WireGuard simple sur un VPS que vous possédez demande moins de logiciels à exploiter et offre moins de possibilités de panne. Headscale devient intéressant lorsque vous ne voulez plus écrire manuellement un bloc [Peer] pour chaque nouvel ordinateur portable. Le coût pousse souvent à étudier cette solution en premier. Il est donc utile de lire ce que couvre réellement l’offre gratuite hébergée avant de prendre un serveur, car quelques machines personnelles tiennent généralement dans ses limites. Si vous avez déjà dépassé ce plafond, comparez le calcul avec le prix des offres payantes, facturées par utilisateur et non par appareil, car un foyer utilisant un seul compte peut rester peu coûteux même lorsque le nombre d’appareils augmente. Si vous voulez un control plane auto-hébergé, mais préférez utiliser votre propre client et une interface web pour gérer les peers plutôt qu’un remplacement direct du client Tailscale, NetBird sur un seul VPS est une alternative à étudier. Pour une comparaison plus large des deux modèles, consultez les différences entre WireGuard et Tailscale.
Prérequis avant l’installation
- Un VPS sous Ubuntu 24.04 avec une adresse IPv4 publique et un accès sudo. Si le serveur est neuf, suivez d’abord les dix premières minutes sur un nouveau VPS.
- Un enregistrement DNS A qui pointe vers cette adresse. Ce guide utilise
headscale.example.com. - Un deuxième domaine ou sous-domaine pour MagicDNS. Ce guide utilise
tailnet.example.net. Il ne doit pas être identique au domaine indiqué dansserver_url. - Une machine cliente à joindre, sous Linux, macOS, Windows, Android ou iOS.
Installer headscale depuis le paquet .deb officiel
Le projet publie des paquets .deb sur sa page des releases GitHub. En juillet 2026, la version actuelle est 0.29.3. Vérifiez d’abord votre architecture, car le nom du fichier l’indique.
sudo apt update
sudo apt install -y wget
dpkg --print-architectureCette commande affiche amd64 sur un VPS x86 standard et arm64 sur une offre de type Ampere ou Graviton. Placez le résultat dans la variable ci-dessous.
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 versionLe ./ placé devant le nom du fichier est requis. Sans lui, apt recherche dans vos dépôts un paquet nommé headscale.deb et échoue.
Le paquet crée un utilisateur système headscale, écrit une configuration par défaut /etc/headscale/config.yaml, installe une unité systemd, puis active et démarre le service. headscale fonctionne donc déjà lorsque apt se termine, et sudo systemctl is-active headscale affiche active. Il utilise toutefois la configuration fournie, qui définit server_url sur http://127.0.0.1:8080, une adresse qu’aucun de vos clients ne peut atteindre. Rien n’est encore exposé, car le listen_addr fourni écoute également sur l’interface loopback, mais n’ajoutez aucun client avant d’avoir défini server_url et redémarré le service, ce que font les deux étapes suivantes.
Configurez server_url avant qu’un client ne rejoigne le réseau
Modifiez /etc/headscale/config.yaml avec sudo nano /etc/headscale/config.yaml, ou appliquez les mêmes trois changements avec sed. Conservez une copie de l’original, car le fichier est long et largement commenté. Il constitue la meilleure référence pour les autres paramètres.
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 est l’adresse que headscale inscrit dans l’enregistrement de chaque client. Les clients se connectent ensuite toujours à cette chaîne exacte. Elle doit donc contenir le nom public précédé de https://, et jamais 127.0.0.1.
listen_addr indique l’adresse sur laquelle le processus écoute. Laissez-la sur l’interface loopback. Un reverse proxy installé sur le même serveur termine le TLS (Transport Layer Security), puis lui transmet les requêtes. Rien en dehors du serveur ne doit donc accéder au port 8080.
base_domain est le suffixe MagicDNS, c’est-à-dire le domaine sous lequel vos nœuds reçoivent leur nom. Il doit s’agir d’un nom de domaine pleinement qualifié, sans point final. Il doit aussi être différent du domaine indiqué dans server_url, sinon les deux espaces de noms entreraient en conflit.
Ne modifiez pas la section de la base de données. La configuration par défaut utilise SQLite dans /var/lib/headscale/db.sqlite, un répertoire créé et détenu par le paquet. SQLite suffit pour un tailnet de cette taille.
Redémarrer headscale et vérifier qu’il fonctionne
sudo systemctl restart headscale
sudo systemctl is-active headscale
curl -sS -o /dev/null -w '%{http_code}\\n' http://127.0.0.1:8080/healthis-active affiche active et curl affiche 200. Le redémarrage charge vos modifications. headscale lit sa configuration une seule fois, au démarrage. Le processus lancé par le paquet continue donc à distribuer le server_url fourni tant que vous ne le redémarrez pas. sudo systemctl enable --now headscale ne suffit pas : le paquet a déjà activé et démarré le service, cette commande ne change donc rien et l’ancienne valeur reste active. Le démarrage automatique est déjà configuré, et sudo systemctl is-enabled headscale affiche enabled pour le confirmer.
Si is-active affiche autre chose que active, ou si curl n’affiche pas 200, consultez le journal avec sudo journalctl -u headscale -n 50 --no-pager. À ce stade, l’échec vient presque toujours du fichier de configuration, car headscale analyse tout le fichier avant d’ouvrir un socket. Une indentation incorrecte ou une clé inconnue arrête donc le processus avant qu’il n’écoute sur un port. Corrigez le fichier, puis exécutez sudo systemctl restart headscale. Toute modification ultérieure de la configuration nécessite le même redémarrage. Les clients se reconnectent ensuite automatiquement. Si les unités systemd sont nouvelles pour vous, exécuter vos propres services et timers avec systemd présente les commandes utilisées ici.
Vérifiez les fichiers d’état pendant que vous êtes dans le shell :
stat -c '%U %n' /var/lib/headscale/db.sqlite /var/lib/headscale/noise_private.keyLes deux lignes commencent par headscale, l’utilisateur non privilégié créé par le paquet. noise_private.key est l’identité du serveur auprès de ses clients. Conservez-le. Si vous le supprimez, headscale en génère un nouveau et chaque nœud doit s’enregistrer à nouveau.
Configurer TLS devant headscale
Les clients doivent accéder à server_url en HTTPS. Caddy est la solution la plus simple, car il demande et renouvelle automatiquement le certificat.
sudo apt install -y caddyRemplacez /etc/caddy/Caddyfile par le bloc fourni dans la documentation de headscale :
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 affiche adapted config to JSON lorsque le fichier est correctement analysé. Un avertissement indiquant que le fichier n’est pas formaté est sans conséquence. Depuis votre ordinateur portable, curl -sS -o /dev/null -w '%{http_code}\\n' https://headscale.example.com/health doit également afficher 200. Ce contrôle unique confirme que le DNS, le pare-feu, le certificat et le proxy fonctionnent ensemble.
Voici le détail du proxy qui peut faire perdre une soirée. La connexion de contrôle Tailscale utilise une mise à niveau HTTP. Elle est établie avec une requête POST et non GET. La valeur de l’en-tête Upgrade est tailscale-control-protocol. Caddy transmet cette requête sans configuration supplémentaire. Ce n’est pas le cas de nginx. Un frontal nginx doit donc utiliser la map de mise à niveau suivante :
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;
}
}Si vous omettez ces lignes, les requêtes ordinaires continuent de fonctionner. C’est pourquoi /health renvoie 200 et que tout semble correct. Pourtant, la connexion de contrôle persistante ne s’établit jamais. Vos nœuds s’enregistrent, puis restent hors ligne. Si vous choisissez nginx, l’article Certbot sur Ubuntu 24.04 avec nginx couvre la partie certificat.
Ports à ouvrir dans UFW
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verboseLe port 443 transporte toutes les communications des clients. Le port 80 sert uniquement au challenge HTTP d’ACME (environnement de gestion automatique des certificats) et à la redirection vers HTTPS. Caddy en a également besoin pour obtenir un certificat.
Le port 8080 reste fermé. listen_addr est 127.0.0.1:8080, donc le proxy accède à headscale via l’interface loopback et aucune règle de pare-feu n’est nécessaire. Ouvrir le port 8080 sur Internet fournirait aux clients un canal de contrôle en clair, sans apporter aucun avantage. N’oubliez pas que la plupart des fournisseurs proposent un second pare-feu dans leur panneau de contrôle, distinct d’UFW. Un port peut donc être ouvert sur le serveur tout en restant fermé en périphérie du réseau. Bases du pare-feu UFW sur un VPS décrit plus en détail la syntaxe des règles.
Créer un utilisateur et une clé de préauthentification
sudo headscale users create alice
sudo headscale users listLa commande headscale est un client. Elle communique avec le daemon en cours d’exécution via le socket Unix situé à /var/run/headscale/headscale.sock. Ce socket est en mode 0770 et appartient au groupe headscale. Cela entraîne deux conséquences. La commande échoue si le service est arrêté, ce qui explique aussi pourquoi l’ordre des étapes de ce guide est important. Elle nécessite également sudo, sauf si vous ajoutez votre propre compte au groupe headscale.
users list affiche un identifiant à côté de chaque nom. Vous avez besoin de ce numéro, car la commande de création de clé accepte un ID utilisateur numérique, pas un nom.
sudo headscale preauthkeys create --user 1 --expiration 24hLa clé n’est affichée qu’une seule fois. Copiez-la maintenant. Une clé de préauthentification est utilisable une seule fois et reste valide pendant une heure, sauf indication contraire. Il est donc utile de définir --expiration 24h pendant les tests. Ajoutez --reusable pour créer une clé permettant l’inscription de plusieurs machines. Traitez cette clé comme un mot de passe, car toute personne qui la détient peut rejoindre votre réseau.
Connecter votre premier client avec --login-server
Sur la machine que vous voulez rattacher :
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 affiche l’adresse attribuée par headscale, par exemple 100.64.0.1. De retour sur le serveur, sudo headscale nodes list affiche le nœud avec son ID, son utilisateur et son état en ligne.
La valeur de --login-server doit correspondre exactement à server_url, schéma inclus et sans slash final. Les valeurs sont comparées comme des chaînes. Une différence signifie que le client s’enregistre auprès d’une adresse, puis reçoit l’ordre de communiquer avec une autre.
Une machine qui était déjà connectée au service hébergé de Tailscale conserve cette connexion. Exécutez d’abord sudo tailscale logout sur cette machine, puis tailscale up avec --login-server.
Si vous omettez --auth-key, le client affiche une URL. Ouvrez-la : la page affiche l’identifiant de cette tentative d’enregistrement, que vous approuvez sur le serveur :
sudo headscale auth register --user alice --auth-id PASTE-THE-ID-FROM-THE-PAGECette méthode est plus pratique sur votre propre portable. Les clés de pré-authentification conviennent mieux aux tâches automatisées, car aucune intervention humaine n’est nécessaire. Une fois le VPS lui-même devenu un nœud, il peut également transporter le trafic Internet de vos autres machines. C’est la configuration du nœud de sortie, à ceci près que vous approuvez la route annoncée sur le serveur avec la commande headscale, et non dans une console d’administration hébergée. Si vous voulez accéder à un réseau privé situé derrière ce VPS, plutôt que fournir une sortie vers Internet, la même étape d’approbation permet d’annoncer ce sous-réseau au reste de votre tailnet. Publier une application depuis un nœud, au lieu d’y faire transiter des réseaux entiers, est encore une autre tâche. serve et funnel sont les deux méthodes permettant de le faire, mais elles s’appuient toutes deux sur les certificats et le mécanisme d’ingress de Tailscale. Considérez-les donc comme des fonctionnalités du tailnet hébergé, et non comme des fonctions fournies par headscale.
DERP et relais du trafic en cas d’échec de la connexion directe
DERP (designated encrypted relay for packets) est le chemin de secours. Lorsque deux nœuds ne peuvent pas établir de connexion WireGuard directe, généralement parce qu’ils se trouvent tous les deux derrière un NAT strict (network address translation), ils envoient les paquets via un relais. Le relais ne détient aucune clé et ne peut donc pas lire votre trafic. Il voit en revanche quels nœuds communiquent et quelle quantité de données circule.
Il faut bien comprendre le comportement de la configuration par défaut. Headscale est livré avec une configuration qui pointe vers https://controlplane.tailscale.com/derpmap/default, avec auto_update_enabled: true et update_frequency: 3h. Votre plan de contrôle vous appartient donc, tandis que vos relais sont gérés par Tailscale. Pour la plupart des utilisateurs, ce compromis est acceptable. Si ce n’est pas votre cas, exécutez votre propre relais.
Pour exécuter votre propre relais, définissez enabled: true sous derp.server dans config.yaml, redémarrez headscale et ouvrez le port STUN (session traversal utilities for NAT) avec sudo ufw allow 3478/udp. Le fichier de configuration indique clairement cette exigence : server_url doit utiliser https, car DERP nécessite TLS. Vider la liste derp.urls retire les relais Tailscale de la carte. Si vous le faites sans relais intégré fonctionnel, les paires de nœuds qui ne peuvent pas se connecter directement ne pourront plus se connecter du tout.
Depuis un client, tailscale netcheck affiche la latence vers chaque région de relais connue. tailscale status indique pour chaque pair s’il est direct avec une adresse ou relay avec un code de région. Un pair bloqué sur relay signale un problème de NAT, pas un problème de headscale. Un pair qui est direct mais reste lent relève d’un autre diagnostic ; la cause habituelle est alors le MTU, et non le tunnel lui-même.
Pourquoi un nœud apparaît-il hors ligne ?
Le proxy supprime la mise à niveau. C’est le cas le plus fréquent. Tout le reste semble fonctionner : /health renvoie 200, headscale nodes list affiche le nœud, mais celui-ci ne passe jamais en ligne. La connexion de contrôle est un POST contenant Upgrade: tailscale-control-protocol. Un proxy qui ne transmet pas cette requête supprime l’unique canal qui signale l’état du nœud. Comparez votre configuration nginx avec le bloc map ci-dessus, ou utilisez Caddy pour écarter le proxy comme cause.
server_url a changé après l’enregistrement des nœuds. Les nœuds continuent de se connecter à la valeur qui leur a été fournie lors de l’enregistrement. Si vous l’avez modifiée, exécutez sudo tailscale up --login-server https://headscale.example.com --force-reauth sur chaque nœud.
Le client n’est pas en cours d’exécution. Sur le nœud, sudo systemctl is-active tailscaled et sudo journalctl -u tailscaled -n 50 --no-pager. Un client qui ne peut pas résoudre votre domaine ou y accéder consigne ses nouvelles tentatives à cet emplacement.
La clé a expiré. Ce point est traité dans la section suivante.
Pour surveiller le serveur pendant vos tests, exécutez sudo journalctl -u headscale -f sur le VPS et redémarrez tailscaled sur le client. Un nœud qui atteint headscale produit immédiatement des lignes dans les journaux. L’absence de messages signifie que la requête n’arrive pas au serveur. Vérifiez donc le DNS, le firewall et le proxy avant d’examiner headscale.
Expiration des clés et nœud qui cesse de fonctionner plusieurs semaines plus tard
Il existe deux types d’expiration distincts. Les confondre fait perdre du temps.
Les clés de pré-authentification expirent rapidement, par conception. La valeur par défaut est d’une heure et d’une utilisation. Si tailscale up refuse la clé, générez-en une nouvelle sur le serveur au lieu de modifier quoi que ce soit sur le client.
Les clés des nœuds constituent la partie à long terme. La section node de config.yaml définit expiry: 0, et 0 signifie qu’il n’y a pas d’expiration par défaut : un nœud enregistré reste valide jusqu’à son expiration. Les nœuds marqués n’expirent jamais. Définissez expiry: 180d si vous voulez que les enregistrements expirent automatiquement, et mesurez bien les conséquences : chaque nœud non marqué devra alors exécuter sudo tailscale up --login-server https://headscale.example.com --force-reauth selon cette périodicité, et un serveur sans interface graphique sur lequel personne ne se réauthentifie sera automatiquement retiré du réseau.
Faites-le manuellement lorsqu’une personne perd un ordinateur portable. sudo headscale nodes list vous donne l’ID, puis sudo headscale nodes expire -i 3 déconnecte ce nœud et sudo headscale nodes delete -i 3 le supprime entièrement du réseau.
Sauvegardes et mises à niveau
/var/lib/headscale et /etc/headscale constituent à eux deux l’ensemble du serveur. Arrêtez le service avant de les copier, car SQLite peut avoir des écritures en cours et une base de données copiée pendant son utilisation peut être incohérente.
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-*.tgzCopiez les deux fichiers hors du serveur. Ils contiennent les clés privées et toutes les inscriptions. Ils doivent donc être protégés avec le même soin que le serveur lui-même. sauvegardes restic depuis un VPS explique comment automatiser cette opération et chiffrer les sauvegardes.
Les mises à niveau reprennent les étapes de l’installation : téléchargez le nouveau .deb et sudo apt install ./headscale.deb, puis redémarrez le service et exécutez de nouveau les vérifications is-active et /health. Depuis la version 0.29, le chemin de mise à niveau est strict. Le saut d’une version mineure est bloqué, tout comme le retour vers une version mineure plus ancienne. Passez d’une version mineure à la suivante, sauvegardez les données avant chaque étape et lisez d’abord les notes de version correspondantes, car cette version a également modifié le comportement de la stratégie ACL et déplacé plusieurs clés de configuration.
FAQ
headscale est-il déjà actif juste après l’installation du fichier .deb ?
Oui. Le paquet active et démarre le service dès son installation, avec le /etc/headscale/config.yaml fourni, qui écoute uniquement sur la loopback et définit server_url sur http://127.0.0.1:8080. Cette adresse est inutilisable par les clients. Modifiez donc server_url, listen_addr et base_domain, puis exécutez sudo systemctl restart headscale et vérifiez avec sudo systemctl is-active headscale. La valeur doit être restart : headscale ne lit le fichier qu’à son démarrage, et enable --now sur un service déjà actif ne change rien. Si le service échoue après le redémarrage, sudo journalctl -u headscale -n 50 --no-pager indique le problème. À ce stade, il s’agit presque toujours d’une erreur YAML, car headscale analyse l’intégralité du fichier avant d’écouter sur un port.
Dois-je quand même installer le client Tailscale standard sur mes machines ?
Oui. Headscale remplace uniquement le serveur de contrôle. Chaque nœud exécute le client officiel de Tailscale, et vous le pointez vers votre serveur avec sudo tailscale up --login-server https://headscale.example.com. Ce flag existe dans le client standard. Aucun correctif ni recompilation n’est donc nécessaire.
Mon trafic passe-t-il par le serveur headscale ?
En général, non. Headscale coordonne le réseau et distribue les clés et les adresses, tandis que le chemin des données passe directement par WireGuard entre vos nœuds. Le trafic ne fait un détour que lorsque deux nœuds ne peuvent pas communiquer directement et utilisent un relais DERP. Avec la configuration fournie, ces relais sont ceux, publics, de Tailscale. Exécutez tailscale status sur un nœud pour vérifier si un pair donné est direct ou passe par un relay.
Pourquoi mon nœud reste-t-il hors ligne après son enregistrement ?
Un nœud qui apparaît dans headscale nodes list mais ne passe jamais en ligne a généralement perdu sa connexion de contrôle au niveau du reverse proxy. Cette connexion est une mise à niveau HTTP envoyée avec une requête POST et l’en-tête Upgrade: tailscale-control-protocol. nginx l’abandonne si vous n’ajoutez pas le bloc map $http_upgrade $connection_upgrade et les lignes proxy_set_header correspondantes. Caddy la transmet sans configuration supplémentaire. Il permet donc de vérifier rapidement si le proxy est en cause.
Ai-je besoin d’un nom de domaine et de TLS pour headscale ?
En pratique, oui. Les clients se connectent à la chaîne indiquée dans server_url, les certificats sont émis pour des noms et non pour des adresses IP seules, et le fichier de configuration précise que DERP nécessite TLS. Un domaine associé à Caddy se configure en environ cinq minutes et fournit un endpoint HTTPS dont les certificats sont renouvelés automatiquement. Si vous exécutez le serveur de contrôle en HTTP simple, les échanges de chaque client avec celui-ci traversent Internet en clair.