📍 Article principal de la série : Headscale 2026 : guide pratique.
Sans ACL, votre Headscale est un réseau plat où chaque appareil peut joindre chaque autre sur tous les ports. Pour une équipe de 30 personnes répartie entre Dakar, Abidjan et Casablanca, c’est un anti-pattern de sécurité majeur. Ce tutoriel détaille la mise en place de politiques HuJSON robustes : utilisateurs, groupes, tags, autorisations granulaires, exemples copier-coller.
Prérequis
- Headscale en production (voir tutoriel d’installation).
- Au moins 5 appareils enregistrés.
- Notion de RBAC (Role-Based Access Control).
- Niveau attendu : intermédiaire/avancé.
- Temps estimé : 2 à 3 heures.
Étape 1 — Comprendre la structure HuJSON
Une politique Headscale comporte cinq sections principales :
groups: groupes d’utilisateurs (équivalents groupes RBAC).tagOwners: qui peut assigner quels tags.autoApprovers: routes et exit nodes pré-approuvés.acls: règles d’autorisation effectives.ssh: règles spécifiques SSH (Tailscale SSH).
Étape 2 — Définir les utilisateurs et groupes
Dans /etc/headscale/policy.hujson :
{
"groups": {
"group:admins": ["amadou@votre-entreprise.com"],
"group:devs": ["amadou@...", "fatou@...", "ousmane@..."],
"group:marketing": ["aicha@...", "moussa@..."],
"group:finance": ["khadija@..."],
"group:freelances": ["consultant1@external.com"]
},
"tagOwners": {
"tag:server-prod": ["group:admins"],
"tag:server-staging": ["group:devs"],
"tag:database-prod": ["group:admins"],
"tag:office-dakar": ["group:admins"]
}
}
Étape 3 — Définir les ACL principales
"acls": [
// Admins ont accès total
{
"action": "accept",
"src": ["group:admins"],
"dst": ["*:*"]
},
// Développeurs : SSH sur staging et prod, accès apps internes
{
"action": "accept",
"src": ["group:devs"],
"dst": ["tag:server-staging:22,80,443,5432,6379"]
},
{
"action": "accept",
"src": ["group:devs"],
"dst": ["tag:server-prod:443"]
},
// Marketing : accès à Vaultwarden, Outline, Plausible uniquement
{
"action": "accept",
"src": ["group:marketing"],
"dst": ["100.64.0.10:443", "100.64.0.11:443", "100.64.0.12:443"]
},
// Finance : accès ERP uniquement
{
"action": "accept",
"src": ["group:finance"],
"dst": ["100.64.0.20:443"]
},
// Freelances : staging seulement, expire automatiquement
{
"action": "accept",
"src": ["group:freelances"],
"dst": ["tag:server-staging:80,443"]
},
// Subnet routers : autorisation pour LAN bureau Dakar
{
"action": "accept",
"src": ["group:devs", "group:admins"],
"dst": ["192.168.10.0/24:*"]
}
]
Étape 4 — SSH avec Tailscale SSH
Tailscale SSH (et Headscale SSH) permet de SSH sans clé publique, l’authentification étant gérée par le mesh :
"ssh": [
// Admins peuvent SSH partout en root
{
"action": "accept",
"src": ["group:admins"],
"dst": ["tag:server-prod", "tag:server-staging"],
"users": ["root", "ubuntu", "debian"]
},
// Devs peuvent SSH sur staging avec leur propre user
{
"action": "accept",
"src": ["group:devs"],
"dst": ["tag:server-staging"],
"users": ["autogroup:nonroot"]
},
// Note : "action":"check" + "checkPeriod" est une feature Tailscale Cloud Enterprise.
// Headscale stable (0.28+) ne le supporte pas — tomber sur "accept" simple
// ou ajouter une couche 2FA externe (Authelia, Authentik) devant les services prod.
{
"action": "accept",
"src": ["group:devs"],
"dst": ["tag:server-prod"],
"users": ["autogroup:nonroot"]
}
]
Étape 5 — Tester la politique avant application
headscale policy check --file /etc/headscale/policy.hujson
# Retourne : Policy is valid
headscale policy set --file /etc/headscale/policy.hujson
# Retourne : Policy applied successfully
Étape 6 — Assigner les tags aux serveurs
Sur le serveur Postgres production :
tailscale up --login-server=https://headscale.votre-entreprise.com \
--auth-key=PREAUTH \
--advertise-tags=tag:server-prod,tag:database-prod
Côté Headscale, approuver l’attribution :
headscale nodes tag -i ID_DU_NODE -t tag:server-prod,tag:database-prod
Étape 7 — Audit et conformité
Toutes les actions Headscale sont loggées. Pour un audit mensuel :
journalctl -u headscale --since "1 month ago" | grep -i "policy\|tag\|register"
Pour une PME soumise à RGPD ou ARTCI, exporter ces logs vers Loki + Grafana avec rétention 1 an minimum.
Cas spécial : équipe multi-sites
Pour une équipe répartie sur 3 villes (Dakar, Abidjan, Casablanca), définir des tags géographiques permet d’écrire des règles type « les devs Dakar peuvent SSH les serveurs Dakar, idem Abidjan » :
"groups": {
"group:devs-dakar": [...],
"group:devs-abidjan": [...],
"group:devs-casa": [...]
},
"tagOwners": {
"tag:server-dakar": ["group:devs-dakar"],
"tag:server-abidjan": ["group:devs-abidjan"],
"tag:server-casa": ["group:devs-casa"]
},
"acls": [
{"action": "accept", "src": ["group:devs-dakar"], "dst": ["tag:server-dakar:*"]},
{"action": "accept", "src": ["group:devs-abidjan"], "dst": ["tag:server-abidjan:*"]},
// Sauf admins, qui ont tout
{"action": "accept", "src": ["group:admins"], "dst": ["*:*"]}
]
Erreurs fréquentes
| Erreur | Cause | Solution |
|---|---|---|
| Politique vide bloque tout | Pas de règle accept par défaut | Ajouter règle {"action":"accept","src":["*"],"dst":["*:*"]} au début pour tester |
| Erreur HuJSON syntaxe | Virgule manquante | headscale policy check indique la ligne |
| Tag refusé | tagOwners absent | Ajouter le tag dans tagOwners |
| Freelance toujours connecté | Compte non supprimé | headscale users destroy --name <nom> ou headscale users destroy -i <ID> (le user doit avoir 0 nœud) |
| SSH par tag refusé | tag non assigné côté server | Vérifier tailscale status sur le serveur |
| Updates ne propagent pas | Cache client après changement ACL | Forcer reconnect côté machine : tailscale down && tailscale up (ou tailscale logout puis re-login). --auto-update sert à mettre à jour le binaire tailscale, pas le cache ACL. |
Ce que la théorie européenne oublie en zone CEDEAO
Trois ajustements concrets. Freelances et stagiaires : très fréquents en Afrique, créer un groupe dédié group:temporaires avec ACL strictes (staging only, ports limités, SSH read-only). Suppression auto possible via cron : headscale users destroy --name temporaire-X après 30 jours d’inactivité (Headscale 0.27+ ; nécessite que l’utilisateur n’ait plus de nœuds). Sites distribués : pour un cabinet d’avocats à Casablanca/Tanger/Marrakech, tagger chaque site permet d’isoler les ressources locales. Conformité ARTCI : exiger les ACL en lecture/écriture sur les serveurs production uniquement pour les comptes traçables (pas de compte partagé).
Tutoriels frères
FAQ
Combien de règles ACL Headscale supporte-t-il ? Plusieurs centaines sans impact perceptible. Les tests Tailscale ont validé jusqu’à 10 000 règles.
Comment auditer qui a accédé à quoi ? Logs Headscale + logs SSH côté serveurs (auditd ou rsyslog). Croiser les deux donne une trace complète.
Politique HuJSON Tailscale Cloud compatible ? 95% oui. Quelques différences sur SSH check mode et certaines features Enterprise non supportées.
Que faire en cas d’erreur de politique en prod ? Garder une politique « safe rollback » testée dans Git. headscale policy set --file safe.hujson rétablit l’ancien état en 5 secondes.
Comment imposer un check 2FA pour SSH prod ? Mode "action": "check" avec checkPeriod. À la première session SSH, l’utilisateur doit confirmer via app mobile ou clé matérielle.
Pour explorer plus loin
- 🔝 Retour au guide général : guide pratique Headscale 2026
- Documentation HuJSON Tailscale : tailscale.com/kb/1018/acls
Solution d’hébergement pour ce tutoriel
Hostinger accueille un site WordPress, un VPS Linux ou une boutique en ligne sans configuration complexe.
Lien d affiliation. Si vous achetez via ce lien, le blog reçoit une petite commission sans surcoût pour vous.
Etape 1 : Inventorier les machines et utilisateurs du mesh
Avant d ecrire la moindre regle ACL, listez tout ce qui doit communiquer. Pour une equipe distribuee Dakar/Abidjan/Casablanca, cela ressemble a : 4 laptops dev (2 a Dakar, 1 a Abidjan, 1 a Casablanca), 2 serveurs Hetzner Helsinki (api-prod, db-prod), 1 VPS Scaleway Paris (ci-runner), 1 NAS bureau Dakar (sauvegardes). Donnez a chaque machine un nom stable, et a chaque utilisateur un identifiant Headscale (rachid, awa, jean, fatou).
# Lister les noeuds enregistres dans Headscale
headscale nodes list
Sortie attendue : un tableau avec ID, nom, utilisateur, IP Tailscale (100.64.0.0/10), date de derniere connexion. Verifiez que chaque machine porte un user clair et un hostname normalise (laptop-rachid, srv-api-prod).
Etape 2 : Definir la matrice qui peut joindre quoi
Sur papier, dessinez une matrice. Lignes : sources (rachid, awa, ci-runner). Colonnes : destinations (api-prod, db-prod, nas-dakar). Case : ports autorises ou bloque. Exemple : rachid peut joindre api-prod sur 22 et 443, db-prod uniquement via le bastion, nas-dakar sur 445. ci-runner peut joindre api-prod sur 22 mais pas db-prod.
Cette matrice devient le contrat. Toute regle ACL ecrite plus tard doit pouvoir se justifier par une case de la matrice. Sans ce travail prealable, vous ecrivez des regles ACL au feeling et vous ouvrez des trous.
Etape 3 : Activer le mode ACL dans Headscale
Headscale supporte les ACL au format HuJSON (JSON avec commentaires). Editez le fichier de configuration principal (souvent /etc/headscale/config.yaml) et indiquez le chemin du fichier ACL. Redemarrez Headscale.
# Extrait config.yaml
policy:
mode: file
path: /etc/headscale/acl.hujson
Sortie attendue apres systemctl restart headscale : aucune erreur dans journalctl -u headscale. Tant que le fichier acl.hujson n existe pas ou est invalide, Headscale refuse les nouvelles connexions. Creez-le immediatement.
Etape 4 : Ecrire un fichier ACL minimal mais fonctionnel
Commencez par une politique deny-all puis ouvrez explicitement. Definissez d abord les groupes, puis les regles. Headscale lit le fichier de haut en bas et la premiere regle qui matche gagne.
{
"groups": {
"group:devs": ["rachid","awa","jean","fatou"],
"group:ops": ["rachid"],
"group:ci": ["ci-runner"]
},
"tagOwners": {
"tag:prod": ["group:ops"],
"tag:infra": ["group:ops"]
},
"acls": [
{ "action":"accept","src":["group:devs"],"dst":["tag:prod:443"] },
{ "action":"accept","src":["group:ops"], "dst":["tag:prod:22","tag:infra:*"] },
{ "action":"accept","src":["group:ci"], "dst":["tag:prod:22"] }
]
}
Sortie attendue apres headscale policy check : Policy is valid. Toute typo (virgule manquante, group inexistant) sera signalee avec numero de ligne.
Etape 5 : Tagger les machines de production
Une ACL parle de tags, pas de noms. Chaque serveur doit recevoir le bon tag au moment du tailscale up. Sans tag, les regles tag:prod ne s appliquent pas.
# Sur le serveur api-prod (Hetzner Helsinki)
sudo tailscale up --login-server https://hs.exemple.io \
--advertise-tags=tag:prod --auth-key=tskey-xxx
Sortie attendue : Success. Verifiez ensuite avec headscale nodes list que la colonne Tags affiche bien tag:prod. Refaites pour chaque serveur infra et chaque runner CI.
Etape 6 : Tester chaque regle avec headscale policy test
Headscale 0.28+ valide la politique au moment du policy check et au reload (logs). Pour tester des paires src/dst de manière déclarative, utiliser la section "tests": [...] dans la politique HuJSON. Indispensable avant tout changement en production : une mauvaise virgule peut couper l acces SSH a tous vos serveurs en meme temps.
# Valider la syntaxe et la cohérence de la politique
headscale policy check --file /etc/headscale/acl.hujson
# Sortie : Policy is valid — ou erreur précise avec ligne fautive
# Recharger Headscale (SIGHUP) et observer les logs
sudo systemctl reload headscale
journalctl -u headscale -n 50 --no-pager | grep -i "policy\|error"
Pour des tests fonctionnels, utiliser la section "tests": [...] dans la politique HuJSON elle-même : Tailscale et Headscale acceptent ce bloc qui décrit les paires src/dst attendues et qui fait échouer le reload si elles ne matchent pas. C’est la voie officielle pour tester avant prod.
Etape 7 : Limiter le SSH aux operateurs et activer Tailscale SSH
Tailscale SSH (et son equivalent Headscale) remplace les cles SSH classiques par une authentification basee sur l identite Headscale. Plus de cles a faire tourner, plus de doute sur qui s est connecte. Activez avec –ssh dans la commande tailscale up sur chaque serveur cible.
# Sur api-prod
sudo tailscale up --ssh --advertise-tags=tag:prod --auth-key=tskey-xxx
Cote ACL, ajoutez une section sshAllow pour controler qui peut SSH sur quoi. Resultat attendu : ssh rachid@api-prod.exemple.ts.net fonctionne sans cle, ssh awa@api-prod echoue avec connection refused.
Etape 8 : Versionner le fichier ACL dans Git avec revue
Le fichier acl.hujson est du code. Committez-le dans un repo Git prive (Gitea auto-heberge a Dakar, ou GitHub prive), ouvrez une pull request pour chaque modification, et exigez au moins une revue avant merge. Cette discipline evite 90 % des incidents ACL.
# Hook pre-commit qui valide le fichier
#!/bin/sh
headscale policy check --file acl.hujson || exit 1
Sortie attendue : tout commit avec un fichier ACL invalide est rejete localement. Aucun fichier casse n arrivera jamais sur la branche principale.
Etape 9 : Surveiller les denials avec les logs Headscale
Chaque connexion refusee est journalisee. Activez le niveau debug temporairement, faites passer du trafic legitime, et chassez les faux negatifs (vraies regles oubliees). Une fois stable, repassez en info pour limiter le bruit.
# Suivre les denials en temps reel
journalctl -u headscale -f | grep -i "denied\|rejected"
Sortie attendue en regime nominal : zero denial pour le trafic legitime, et quelques denials sur des ports inattendus (telemetrie, mises a jour) que vous arbitrez au cas par cas.
Etape 10 : Planifier la rotation des cles d auth et la revue trimestrielle
Les pre-auth keys Headscale doivent expirer. Generez-les avec –expiration 24h pour les nouveaux laptops, et 168h pour les renouvellements de serveurs. Tous les trois mois, exportez la liste des noeuds, supprimez les machines disparues (laptop vole, freelance parti), et faites tourner les tags.
# Récupérer d'abord l'ID numérique de l'utilisateur (depuis Headscale 0.26)
headscale users list
# Sortie : tableau avec ID, Name — noter l'ID numérique de "rachid", par exemple 7
# Générer une clé éphémère de 24h pour ce user (--user accepte un ID numérique depuis 0.26 ;
# en 0.28+, --user devient optionnel)
headscale preauthkeys create --user 7 --expiration 24h --reusable=false
Sortie attendue : une chaine tskey-… a copier sur le nouveau laptop. Dans la continuité, lisez notre guide Wazuh SIEM et notre tutoriel Wi-Fi securise.