~/blog/fr/headplane-headscale-web-ui
Headplane : une interface web pour Headscale

Headscale est le serveur de contrôle open-source d’un réseau auto-hébergé de type Tailscale, et c’est une CLI. C’est très bien jusqu’à ce que vous vouliez voir quels nodes sont en ligne, enregistrer un appareil, ou transmettre une pre-auth key à quelqu’un sans coller une longue sortie de commande dans un chat.
Il y avait une interface web — headscale-ui — et elle fonctionnait bien, mais elle n’a pas eu de release depuis mars 2026 ni de commit depuis. Headplane est l’alternative maintenue, et elle fait plus : nodes, routes, DNS, ACLs, pre-auth keys, plus un terminal SSH dans les nodes et un agent Go pour le côté distant.
Elle a aussi deux modes de défaillance qui m’ont coûté une heure chacun, tous les deux étant une seule ligne dans un fichier de configuration. Ceci est donc un guide de compilation et d’installation où la partie intéressante est les deux choses qui tournent mal.
Ce que vous obtenez
Trois artefacts à partir d’une seule compilation :
- l’application — un serveur React Router qui parle à l’API Headscale
- l’agent (
hp_agent) — un petit binaire Go placé sur les nodes pour fournir SSH et les informations système - le wasm — un terminal qui tourne dans le navigateur
L’agent et le wasm sont optionnels. L’application à elle seule vous donne déjà une interface utilisable.
Vérifiez les prérequis dans le dépôt, pas dans la documentation
La documentation d’installation publiée était en retard sur le code quand je l’ai compilé. Les prérequis qui comptaient vraiment étaient dans le dépôt :
| Composant | Nécessite |
|---|---|
| node | ≥ 24.2 |
| pnpm | 10.4 |
| go | 1.26.6 |
L’exigence sur node est celle qui piège les gens : un node de distribution (ou le paquet Debian) est typiquement en 20 ou 22, et la compilation échoue d’une manière qui ressemble à une erreur de source plutôt qu’à une erreur de version. Vérifiez package.json et le workflow CI dans le dépôt plutôt que de faire confiance à un tableau de versions dans un README — le dépôt fait autorité, la documentation n’est qu’un instantané.
Concrètement : j’ai compilé sur un conteneur qui avait node 20 et pnpm 9, et plutôt que de changer l’interpréteur système — dont d’autres services dépendent — j’ai installé les versions requises dans un répertoire de toolchain isolé et compilé depuis là. La version de node de la machine de compilation ne devrait pas être une décision qui affecte quoi que ce soit d’autre.
Compilation
git clone https://github.com/tale/headplane && cd headplane
pnpm install
./build.sh --wasm --app --agent
Cela prend quelques minutes ; l’essentiel est la compilation Go de l’agent et des bibliothèques Tailscale derrière lui. La compilation Go est la raison de donner un peu de RAM à la machine de compilation — faire cela sur un conteneur de 1 Go revient à regarder l’OOM killer travailler.
Où il doit tourner, et en tant que qui
Headplane doit atteindre l’API Headscale, et il doit pouvoir faire recharger sa configuration à Headscale. Copiez la sortie de compilation vers /opt/headplane et notez où l’interpréteur node a atterri si vous en avez installé un isolé.
Maintenant le premier piège, et c’est toute la raison d’être de cet article.
Faites tourner le service sous l’utilisateur headscale. L’approche évidente est de le faire tourner sous son propre utilisateur et de visser une règle sudoers ou une capability systemd pour qu’il puisse redémarrer Headscale. Headplane évite tout cela : son integration.proc trouve le processus Headscale dans /proc et lui envoie SIGHUP. Il n’appelle jamais systemctl. Donc si le service tourne sous le même utilisateur que celui qui possède le processus Headscale, l’envoi de signal est simplement autorisé — pas de sudo, pas de capabilities, aucune élévation de privilèges nulle part dans la conception.
C’est une décision vraiment bonne de la part de l’auteur, et cela vaut la peine de le savoir avant de vouloir « corriger » le tout en ajoutant des permissions dont il n’a pas besoin.
Ce qui mène directement au second piège : les permissions du répertoire de configuration. Tourner sous headscale signifie que chaque chemin de la configuration du service doit être traversable par cet utilisateur. Le mien était root:root 0750, et l’unité échouait en boucle — start, exit, restart — avec un message sur le fichier de configuration plutôt que sur le répertoire dans lequel il ne pouvait pas entrer. Un chmod et il est monté du premier coup et n’a pas redémarré depuis.
Généralisez la règle, car cela piège les gens sur chaque service de cette forme : quand une unité tourne sous un utilisateur non-root, vérifiez les permissions des répertoires le long du chemin, pas seulement celles du fichier. Un fichier peut être lisible par tous et rester inatteignable derrière un répertoire qui ne l’est pas.
La clé d’API
Headplane s’authentifie auprès de Headscale avec une clé d’API, créée sur le serveur de contrôle :
headscale apikeys create --expiration 90d
Placez-la dans la configuration de Headplane, pas dans le navigateur. C’est un bon moment pour décrire comment l’interface précédente a échoué : son erreur « API test did not succeed » était une clé expirée stockée dans le localStorage du navigateur. Le serveur était sain tout du long — /health renvoyait 200, /api/v1/* renvoyait 401 — et le correctif était d’émettre une nouvelle clé, pas de toucher au serveur de contrôle. Si une interface vous dit que l’API est en panne, faites un curl de l’API avant de la croire.
Donnez une expiration à la clé et notez-la. Une clé qui expire discrètement emporte l’interface avec elle.
Le placer derrière nginx
L’interface est une application web, mais les endpoints de contrôle du tailnet sont un service différent sur un port différent, et vous voulez qu’ils continuent de fonctionner quand l’interface est en panne. Gardez-les séparés :
location /admin {
proxy_pass http://127.0.0.1:3000;
}
# les clients Tailscale parlent directement à ceux-ci
location /key { proxy_pass http://127.0.0.1:8080; }
location /machine/ { proxy_pass http://127.0.0.1:8080; }
location /api/ { proxy_pass http://127.0.0.1:8080; }
location /health { proxy_pass http://127.0.0.1:8080; }
Servir l’interface sous un chemin plutôt que sous un sous-domaine signifie un seul certificat et aucun enregistrement DNS supplémentaire. Vérifiez les tableaux de bord sur lesquels vous comptez : /health qui renvoie 200 est la vérification dont les clients se soucient, et elle ne devrait pas dépendre du processus de l’interface.
Édition de la configuration : laissez-la désactivée
Headplane peut éditer la configuration de Headscale depuis l’interface, et c’est désactivé par défaut pour une bonne raison : le config.yaml de Headscale est en 0640 root:headscale, donc y écrire signifie changer ses permissions en quelque chose dans quoi Headplane peut écrire. Activer cela déplace la configuration de votre serveur de contrôle dans un formulaire web, protégé par ce que soit l’authentification de votre interface.
La mienne reste désactivée. Éditer le fichier à la main est plus lent et nettement plus ennuyeux, ce qui, dans ce cas, est la fonctionnalité.
Pièges
- Lisez les prérequis dans le dépôt. Node ≥ 24.2, pnpm 10.4, go 1.26.6 au moment où je l’ai compilé. Un node trop vieux échoue par une erreur de compilation cryptique.
- Tournez sous l’utilisateur
headscale. L’intégrationSIGHUPn’a besoin d’aucun privilège si vous le faites. - Vérifiez les permissions des répertoires, pas seulement celles des fichiers. Un répertoire de configuration en
0750appartenant à root a produit une boucle de redémarrage sans fin. - Compilez l’interface ailleurs si l’hôte est petit. La compilation Go veut de la mémoire.
- Une clé d’API expirée ressemble à s’y méprendre à un serveur en panne. Émettez une nouvelle clé, puis faites un curl de
/healthpour confirmer. - Gardez
/healthet les endpoints de contrôle indépendants de l’interface. Vos nodes existants ne devraient pas se soucier du redémarrage de l’interface.