Installer Headscale pour auto-héberger Tailscale
Déployez votre serveur de contrôle Tailscale sur un VPS avec le paquet .deb officiel. Définissez server_url avant le démarrage, puis connectez votre premier nœud.
Ce qu’est headscale
Headscale est une implémentation auto-hébergée du serveur de contrôle Tailscale. La machine qui coordonne votre réseau privé est donc un VPS qui vous appartient. 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 sait quels appareils 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. Les tunnels restent des tunnels WireGuard, établis directement entre les nœuds. Le trafic entre deux de vos machines ne passe pas par le serveur headscale, sauf si une liaison directe ne peut pas être établie et que les nœuds doivent utiliser un relay. Assurer vous-même ce rôle de coordination change 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 dessert un seul tailnet (un seul réseau Tailscale). Le projet le présente comme une solution adaptée à un usage personnel ou à une petite organisation. Avec trois ou quatre machines, un VPN WireGuard classique sur un VPS qui vous appartient demande moins de logiciels à exécuter 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 les utilisateurs à se renseigner, mais il est utile de vérifier ce que couvre réellement l’offre gratuite hébergée avant de prendre en charge un serveur, car quelques machines personnelles y tiennent généralement. Si vous avez déjà dépassé cette limite, 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 longtemps après que le nombre d’appareils a cessé d’être déterminant. Si vous voulez un control plane auto-hébergé, mais préférez utiliser votre propre client et disposer d’une interface web pour gérer les peers plutôt que d’un remplacement direct de Tailscale, NetBird sur un seul VPS est l’alternative à étudier. Pour une comparaison plus générale des deux modèles, consultez les différences entre WireGuard et Tailscale.
Ce qu’il vous faut 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 de type A pointant vers cette adresse. Ce guide utilise
headscale.example.com. - Un second 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 à connecter, sous Linux, macOS, Windows, Android ou iOS.
Installer headscale depuis le paquet .deb officiel
Le projet publie des paquets .deb sur sa page GitHub Releases. En juillet 2026, la version actuelle est 0.29.3. Vérifiez d’abord votre architecture, car elle figure dans le nom du fichier.
sudo apt update
sudo apt install -y wget
dpkg --print-architectureCette commande affiche amd64 sur un VPS x86 standard et arm64 sur une instance 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 obligatoire. Sans lui, apt recherche un paquet nommé headscale.deb dans vos dépôts et échoue.
Le paquet crée un utilisateur système headscale, écrit une configuration par défaut /etc/headscale/config.yaml et installe une unité systemd. Il ne démarre pas le service, ce qui est l’ordre correct. La configuration fournie lie server_url à http://127.0.0.1:8080, qui n’est pas une adresse accessible par vos clients. Démarrer le service à ce stade serait donc incorrect, même s’il parvenait à démarrer. L’exécution de sudo systemctl is-active headscale à ce stade affiche inactive. C’est normal, et non une erreur.
Configurez server_url avant de démarrer le service
Modifiez /etc/headscale/config.yaml avec sudo nano /etc/headscale/config.yaml, ou appliquez les trois mêmes modifications avec sed. Conservez une copie de l’original, car le fichier est long et comporte de nombreux commentaires. C’est la meilleure référence dont vous disposez 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 écrit dans chaque inscription de client. Les clients utilisent ensuite cette chaîne exacte pour se connecter. Elle doit donc être 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 loopback. Un reverse proxy installé sur le même serveur termine le chiffrement TLS (Transport Layer Security), puis lui transmet les requêtes. Aucun élément extérieur au serveur n’a donc besoin d’accéder au port 8080.
base_domain est le suffixe MagicDNS, c’est-à-dire le domaine sous lequel vos nœuds reçoivent leurs noms. Il doit s’agir d’un nom de domaine pleinement qualifié, sans point final, et il doit ê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 valeur par défaut utilise SQLite dans /var/lib/headscale/db.sqlite, un répertoire créé et possédé par le paquet. SQLite suffit pour un tailnet de cette taille.
Démarrer headscale et vérifier qu’il fonctionne
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 affiche active et curl affiche 200. enable --now effectue les deux opérations : il démarre le service et l’active pour le démarrage après un reboot.
Si is-active affiche failed, 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 l’intégralité du 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.
Profitez de votre présence dans le shell pour vérifier les fichiers d’état :
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 package. noise_private.key est l’identité du serveur auprès de ses clients. Conservez-le. Si vous le supprimez, headscale en génère une nouvelle et chaque nœud doit s’enregistrer de nouveau.
Placer TLS devant headscale
Les clients doivent accéder à server_url en HTTPS. Caddy est la solution la plus simple, car il demande et renouvelle le certificat automatiquement.
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 est une mise à niveau HTTP. Elle est initiée 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. nginx ne le fait pas. Un front end nginx doit donc utiliser la configuration 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, alors que la connexion de contrôle persistante ne s’établit jamais. Vos nœuds s’enregistrent, puis restent hors ligne. Si vous choisissez nginx, Certbot sur Ubuntu 24.04 avec nginx couvre la partie certificats.
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 ACME (automatic certificate management environment) et à la redirection vers HTTPS. Caddy en a aussi besoin pour obtenir un certificat.
Le port 8080 reste fermé. listen_addr est 127.0.0.1:8080 : le proxy accède donc à headscale via l’interface loopback, sans qu’aucune règle de pare-feu soit nécessaire. Ouvrir le port 8080 sur Internet fournirait aux clients un canal de contrôle en clair, sans apporter aucun avantage. Notez que la plupart des fournisseurs proposent un second pare-feu dans leur panneau de contrôle, séparé d’UFW. Un port peut donc être ouvert sur le serveur tout en restant fermé en périphérie du réseau. Principes du pare-feu UFW sur un VPS présente 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 /var/run/headscale/headscale.sock, dont le mode est 0770 et qui 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 dans 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é attend l’ID numérique de l’utilisateur, et non son 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 à usage unique et reste valide pendant une heure, sauf indication contraire. Il est donc préférable de définir --expiration 24h pendant les tests. Ajoutez --reusable pour créer une clé qui inscrit 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 joindre :
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 de connexion.
La valeur de --login-server doit correspondre exactement à server_url, schéma inclus et sans slash final. La comparaison se fait sous forme de chaînes de caractères. Une différence signifie que le client s’enregistre auprès d’une adresse, puis reçoit l’instruction de communiquer avec une autre.
Une machine précédemment connectée au service hébergé de Tailscale conserve cette connexion. Exécutez d’abord sudo tailscale logout sur cette machine, puis exécutez tailscale up avec --login-server.
Si vous omettez --auth-key, le client affiche une URL à la place. 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 ordinateur portable. Les clés de préauthentification conviennent mieux aux opérations automatisées, car aucune personne n’a besoin de surveiller la procédure. Une fois que le VPS est lui-même un nœud, il peut également transporter le trafic Internet de vos autres machines. Il s’agit de la configuration du nœud de sortie, avec une différence : 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 disposer d’une sortie vers Internet, la même étape d’approbation couvre l’annonce de 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 pour cela, 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 du chemin direct
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 (network address translation) strict, ils envoient plutôt les paquets via un relais. Le relais ne détient aucune clé et ne peut donc pas lire votre trafic. Il voit toutefois quels nœuds communiquent et quelle quantité de données circule.
Il faut bien comprendre le fonctionnement 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 control plane vous appartient donc, tandis que vos relais appartiennent à Tailscale. Pour la plupart des utilisateurs, c’est un compromis acceptable. Si ce n’est pas votre cas, déployez votre propre relais.
Pour déployer 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 supprime les relais Tailscale de la map. Si vous faites cela sans relais intégré fonctionnel, les paires de nœuds qui ne peuvent pas se connecter directement ne pourront plus communiquer du tout.
Depuis un client, tailscale netcheck affiche la latence vers chaque région de relais qu’il connaît, et tailscale status indique pour chaque peer s’il est direct avec une adresse ou relay avec un code de région. Un peer bloqué sur relay indique un problème de NAT, pas un problème lié à headscale. Un peer qui est direct mais reste lent relève encore d’un autre problème : la cause habituelle est alors le MTU, plutôt que 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 qui contient 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 passez à Caddy pour écarter le proxy comme cause.
server_url a changé après l’enregistrement des nœuds. Les nœuds continuent d’utiliser 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, exécutez 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 s’y connecter journalise 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, puis 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 pare-feu 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 de nœud constituent la partie à longue durée de validité. 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 manuelle. Les nœuds marqués n’expirent jamais. Définissez expiry: 180d si vous voulez que les enregistrements expirent après un certain délai, mais comprenez les conséquences : chaque nœud non marqué devra alors utiliser sudo tailscale up --login-server https://headscale.example.com --force-reauth selon cette fréquence, et un serveur headless 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 fournit 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 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 ces 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. La section 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, sudo apt install ./headscale.deb, puis redémarrez le service et relancez les vérifications is-active et /health. Depuis la version 0.29, le chemin de mise à niveau est strict. Il est impossible de sauter une version mineure, tout comme de revenir à une version mineure antérieure. Procédez une version mineure à la fois, effectuez une sauvegarde 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
Pourquoi headscale ne démarre-t-il pas juste après l’installation du fichier .deb ?
Le paquet installe l’unité, mais laisse le service arrêté. La valeur par défaut de /etc/headscale/config.yaml est un modèle, pas une configuration fonctionnelle. Modifiez d’abord server_url, listen_addr et base_domain, puis exécutez sudo systemctl enable --now headscale et vérifiez le résultat avec sudo systemctl is-active headscale. Si le service échoue encore, 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 toujours 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, que vous pointez vers votre serveur avec sudo tailscale up --login-server https://headscale.example.com. Ce flag existe dans le client standard. Aucun patch ni rebuild n’est 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 data path passe directement entre vos nœuds via WireGuard. Le trafic ne fait un détour que lorsque deux nœuds ne peuvent pas communiquer directement et basculent sur un relay DERP. Avec la configuration fournie, ces relays sont ceux de Tailscale, accessibles publiquement. Exécutez tailscale status sur un nœud pour voir si un peer est direct ou utilise 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 utilise une requête HTTP upgrade envoyée avec la méthode POST et l’en-tête Upgrade: tailscale-control-protocol. nginx la bloque si vous n’ajoutez pas le bloc map $http_upgrade $connection_upgrade ainsi que 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, pas 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 le renouvellement est automatique. Si le serveur de contrôle fonctionne en HTTP simple, chaque échange entre les clients et celui-ci traverse Internet en clair.