Contexte et besoins
Dans le cadre de mon alternance, l’équipe veut évaluer une alternative aux VPN d’accès distant classiques pour relier les machines du lab et les postes des admins en télétravail. Le candidat à étudier est Headscale : un serveur de coordination open source compatible avec les clients Tailscale officiels, qui permet de monter un réseau mesh WireGuard auto-hébergé.
Cette intervention est un POC : je déploie un seul nœud Headscale sur le lab, j’enregistre un client Tailscale depuis mon laptop d’admin, et je vérifie qu’il rejoint bien le réseau. Pas d’ACL fines, pas de reverse proxy HTTPS, pas de relais DERP perso : on reste sur l’essentiel pour valider que la mécanique fonctionne et qu’elle vaut la peine d’être creusée.
Rappel théorique
VPN « hub-and-spoke » vs réseau mesh
Un VPN classique (le WireGuard servi par une passerelle UniFi, par exemple) fait passer tout le trafic par un point central :
C’est simple, mais le serveur central est un goulot d’étranglement et un point de panne unique.
Un réseau mesh (Tailscale, Headscale, ZeroTier) ne fait que coordonner les pairs. Le trafic réel passe directement de A vers B sans repasser par le serveur :
Le serveur Headscale distribue les clés publiques et les endpoints (IP/port public de chaque pair). Une fois que A et B connaissent la clé publique l’un de l’autre, ils ouvrent un tunnel WireGuard direct.
Plage d’IP du mesh
Headscale distribue par défaut des IP dans 100.64.0.0/10, plage CGNAT réservée par la RFC 6598. Choix volontaire pour ne pas entrer en conflit avec un LAN interne classique en 10.0.0.0/8 ou 192.168.0.0/16.
Preauth-key
Chaque client doit s’authentifier auprès de Headscale au premier enregistrement. La méthode utilisée pour ce POC est la preauth-key : une chaîne générée côté serveur, qu’on copie sur le client lors de la commande tailscale up. Plus simple qu’OAuth ou OIDC, suffisant pour un POC.
Topologie
| Équipement | Rôle | Adresse réelle | Adresse mesh |
|---|---|---|---|
VM headscale-srv (Debian 12) | Serveur de coordination Headscale | 192.168.10.230/24 | — |
| Laptop admin (Linux) | Client Tailscale | DHCP / 4G | 100.64.0.1 |
Prérequis
- VM Debian 12 dédiée (1 vCPU, 512 Mo RAM, 10 Go disque), IP statique sur le LAN du lab.
- Une entrée DNS interne :
headscale.lab.local→192.168.10.230. - Compte admin sudo sur la VM pour l’installation.
Note
Pour ce POC je reste en HTTP sur le LAN du lab. En vue d’un usage réel par des postes en télétravail, il faudra publier Headscale derrière un reverse proxy HTTPS avec un certificat valide.
Installation de Headscale
Récupération du binaire officiel
HEADSCALE_VERSION="0.23.0"
# Téléchargement du binaire officiel pour amd64
curl -L -o /usr/local/bin/headscale \
https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_amd64
chmod +x /usr/local/bin/headscale
headscale versionExplications ligne par ligne :
HEADSCALE_VERSION: la version est figée explicitement, plutôt que de tirer unlatest. Plus reproductible, et on choisit quand on monte de version.curl -L: suit les redirections (GitHub Releases redirige vers un CDN).chmod +x: rend le binaire exécutable.headscale version: sanity check pour vérifier que le binaire fonctionne.
Préparation des dossiers et de l’utilisateur
# Utilisateur système dédié, sans shell
useradd -r -s /usr/sbin/nologin -d /var/lib/headscale headscale
# Dossiers de configuration et de données
install -d -o headscale -g headscale /etc/headscale
install -d -o headscale -g headscale /var/lib/headscale
# Récupération de la config par défaut
curl -L -o /etc/headscale/config.yaml \
https://github.com/juanfont/headscale/raw/v${HEADSCALE_VERSION}/config-example.yaml
chown headscale:headscale /etc/headscale/config.yamlL’utilisateur système headscale et les dossiers /etc/headscale (config) + /var/lib/headscale (base et clés) suivent le pattern Linux classique. Le service tournera sous cet utilisateur, jamais en root.
Configuration minimale
J’édite /etc/headscale/config.yaml. Pour un POC, seules quelques clés méritent d’être ajustées :
# URL annoncée aux clients
server_url: http://headscale.lab.local:8080
# Adresse d'écoute du serveur
listen_addr: 0.0.0.0:8080
# Plage d'IP du mesh
prefixes:
v4: 100.64.0.0/10
# Base de données SQLite (largement suffisant pour un POC)
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
# Clés cryptographiques (générées au premier démarrage)
private_key_path: /var/lib/headscale/private.key
noise:
private_key_path: /var/lib/headscale/noise_private.key
# Logs
log:
level: infoExplications :
server_url: URL que les clients utiliseront pour parler à Headscale. Elle doit pointer vers la VM via le DNS interne.listen_addr 0.0.0.0:8080: Headscale écoute en HTTP sur le port 8080. Pour un usage réel hors LAN, on mettra un reverse proxy devant pour terminer TLS.prefixes v4: range IPv4 distribué aux nœuds du mesh.database type: sqlite: SQLite, suffisant pour quelques dizaines de nœuds. PostgreSQL est l’option pour aller plus loin.private_key_pathetnoise.private_key_path: clés cryptographiques générées automatiquement au premier démarrage, à sauvegarder.
Toutes les autres clés (DERP, MagicDNS, IPv6, OIDC) restent par défaut pour ce POC.
Service systemd
# /etc/systemd/system/headscale.service
[Unit]
Description=headscale controller
After=network.target
[Service]
Type=simple
User=headscale
Group=headscale
ExecStart=/usr/local/bin/headscale serve
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.targetExplications :
User/Group: headscale: exécution sous l’utilisateur dédié, pas en root.Restart=always: relance automatique en cas de crash.- Pas de
[Service]durci complexe ici : c’est un POC, on garde la conf simple ; on durcira au passage en production (sandboxing systemd, ProtectSystem, etc.).
# Charger le service et le démarrer
systemctl daemon-reload
systemctl enable --now headscale
# Vérifier l'état
systemctl status headscale
journalctl -u headscale -fAu premier démarrage, Headscale crée la base SQLite et les clés cryptographiques dans /var/lib/headscale.
Création d’un utilisateur et d’une preauth-key
Headscale isole les nœuds par utilisateur. Je crée un utilisateur lab-admin qui regroupera les devices des admins.
# Création de l'utilisateur
sudo -u headscale headscale users create lab-admin
# Vérification
sudo -u headscale headscale users list
# Génération d'une clé pré-authentifiée valable 1 heure
sudo -u headscale headscale preauthkeys create \
--user lab-admin \
--expiration 1hLa sortie ressemble à :
ID Key Reusable Expiration
1 abcd1234ef5678...0123456789abcdef0123456789abcd false 2026-01-26 19:00:00Je copie la clé. Elle servira à enregistrer le laptop client.
Enregistrement d’un client Tailscale
Sur mon laptop d’admin :
# Installation du client Tailscale officiel
curl -fsSL https://tailscale.com/install.sh | sh
# Connexion à Headscale au lieu du SaaS Tailscale
sudo tailscale up \
--login-server=http://headscale.lab.local:8080 \
--authkey=abcd1234ef5678...0123456789abcdef0123456789abcd
# Vérification
sudo tailscale status
sudo tailscale ip -4Explications ligne par ligne :
- Le client Tailscale officiel est compatible avec Headscale parce qu’il sait parler à un autre control plane via
--login-server. --authkey: la preauth-key générée à l’étape précédente. Elle permet au client de s’authentifier sans interaction navigateur.tailscale status: montre les nœuds connus du mesh.tailscale ip -4: affiche l’IP100.64.x.xattribuée au client.
Vérifications
Côté Headscale
sudo -u headscale headscale nodes listSortie attendue :
ID Hostname IP User Online
1 laptop-admin 100.64.0.1 lab-admin ✓Le laptop est bien enregistré et marqué en ligne.
Côté client
sudo tailscale statusSortie attendue :
100.64.0.1 laptop-admin lab-admin@ active; directLe statut direct indique que le client a établi un tunnel WireGuard. Sur un mesh à un seul nœud, le direct est avec Headscale lui-même ; on prendra plus de sens dès qu’un deuxième pair sera enregistré.
Test de coupure / reprise
# Coupure du tunnel
sudo tailscale down
# tailscale status → message « Logged out »
# Réouverture
sudo tailscale upSans recoller la preauth-key : le client est déjà connu de Headscale, il se reconnecte tout seul.
Problèmes rencontrés et solutions
| Symptôme | Cause | Correction |
|---|---|---|
tailscale up répond failed to connect to control server | URL mal saisie ou Headscale pas démarré | Vérifier systemctl status headscale et curl http://headscale.lab.local:8080/health |
tailscale up répond invalid pre-auth key | Clé expirée ou déjà utilisée | Régénérer une preauth-key avec headscale preauthkeys create |
headscale nodes list ne montre pas le nœud | Le tailscale up côté client n’a pas réussi à parler au serveur | Vérifier la connectivité IP entre le laptop et la VM Headscale |
Service headscale qui ne démarre pas | YAML invalide dans config.yaml | Lire la sortie de journalctl -u headscale, corriger l’indentation |
| Base SQLite verrouillée | Plusieurs processus Headscale en parallèle | systemctl status headscale, vérifier qu’un seul processus tourne |
Compétences du bloc 1 mobilisées
| Compétence officielle | Mobilisation concrète |
|---|---|
| Installer et configurer des éléments d’infrastructure | Déploiement de Headscale (binaire, dossiers, utilisateur système, service systemd, configuration YAML). |
| Mettre en place et vérifier les niveaux d’habilitation associés à un service | Service exécuté sous un utilisateur dédié, accès au mesh contrôlé par preauth-keys par utilisateur. |
| Réaliser les tests d’intégration et d’acceptation d’un service | Test bout en bout : démarrage Headscale, création utilisateur, génération preauth-key, enregistrement client, vérification côté serveur et côté client. |
| Étudier une solution technique | Comparaison documentée du modèle mesh (Tailscale/Headscale) vs hub-and-spoke (VPN classique) pour orienter le choix de l’équipe. |
Bilan
Le POC est concluant : Headscale tourne sur la VM headscale-srv, un client Tailscale s’enregistre en une commande avec une preauth-key, l’IP 100.64.0.1 est attribuée et la communication avec le serveur passe. La mécanique de mesh WireGuard est validée pour un nœud, ce qui permet à l’équipe de décider de poursuivre vers une mise en production ou non.
Trois enseignements :
- la simplicité d’enregistrement côté client (
tailscale up --login-server=... --authkey=...) est l’argument le plus tangible vs un VPN classique : pas de fichier.conf, pas de QR code, pas de génération de clés à la main ; - le modèle mesh ne prend tout son sens qu’avec plusieurs pairs : à un seul client, on ne mesure pas encore la différence avec un VPN hub-and-spoke. La suite du POC consistera à ajouter d’autres clients pour valider le P2P ;
- la réutilisation des clients Tailscale officiels est un atout : pas besoin de maintenir un client custom, on profite du polish de l’app officielle (Linux, Windows, macOS, mobile).
Pour la suite, et si l’équipe valide le POC : passer Headscale derrière un reverse proxy HTTPS avec un certificat valide, écrire des ACL pour cloisonner les utilisateurs, et brancher l’authentification sur l’OIDC de l’annuaire interne au lieu des preauth-keys.
Sources
- Headscale — Documentation , Juan Font et contributeurs.
- Headscale — GitHub Releases , Juan Font.
- Tailscale — How Tailscale works , Tailscale Inc.
- WireGuard — White Paper , Jason A. Donenfeld, 2017.
- RFC 6598 — IANA-Reserved IPv4 Prefix for Shared Address Space , IETF, 2012.
- ANSSI — Recommandations relatives à l’administration sécurisée des systèmes d’information , ANSSI.