Headscale et Tailscale
Début 2024, j’ai découvert Headscale, une implémentation open source et auto-hébergée du serveur de contrôle Tailscale. Depuis, je l’utilise cette solution avec succès. Au fil du temps, j’ai documenté dans Obsidian mon apprentissage et aussi créé plusieurs PDF pour aider mes amis à se connecter sur mon réseau. Finalement, je me suis dit que je pouvais présenter cela sous forme d’une série de billets sur mon blog, qui a repris vie après 10 ans de pause. Cela pourrait même aider à faire connaître cette solution à davantage de personnes francophones. Le contrôle d’accès via l’ACL a été la partie la plus compliquée de mon apprentissage, car il y a très peu de documentation simple disponible sur Internet. J’ai donc particulièrement développé des points, car je pense que cela pourra être utile à de nombreuses personnes.. Ce billet est aussi disponible en anglais, just in case :-)
Serveur VPN
Mon voyage dans le monde du Homelab a commencé en 2017 avec l’achat d’un NAS Synology à 4 baies. J’ai rapidement compris l’intérêt de sécuriser mon serveur. J’ai donc appris à changer les ports de mes services, à exposer le minimum de choses sur Internet, à limiter les pays qui peuvent accéder à mon serveur, à ajouter un reverse proxy et finalement à ajouter un serveur VPN de Synology pour pouvoir y accéder de l’extérieur. Le but étant de pouvoir administrer mon homelab et aussi d’utiliser mes deux PiHoles pour filtrer mes DNS depuis l’extérieur de mon réseau local.
Ce serveur VPN me permettait aussi de connecter mon second NAS hébergé chez un ami pour des sauvegardes hors ligne. Malheureusement, j’avais régulièrement des déconnexions de ce NAS distant. Il ne savait pas se reconnecter automatiquement. De plus, le VPN Synology ne sait pas assigner une IP à une machine, résultat : si je me connectais avec mon ordinateur personnel, mon serveur distant risquait de changer d’IP s’il se reconnectait… Bref, cette solution ne me convenait pas pour de nombreuses raisons.
Durant ce cheminement pour sécuriser mes connexions distantes, j’ai fait quelques tentatives avec WireGuard. J’aimais pouvoir assigner une IP fixe. Mais je n’ai finalement jamais complété cette solution pour remplacer le VPN Synology.
De plus, je voulais trouver une solution pour sécuriser l’accès de mes amis à certains services auto-hébergés comme Vaultwarden et Mealie. J’ai utilisé le filtre IP de Nginx Proxy Manager pour limiter l’accès. Mais cela n’est pas pratique, vu que la majorité de mes amis ont des IP dynamiques et cela ne fonctionnait pas s’ils étaient sur leur téléphone en déplacement. De plus, cela rendait ces services visibles de l’extérieur, une situation qui ne me convenait pas.
Tailscale
Depuis quelque temps, j’avais entendu de bonnes choses sur Tailscale, un système de VPN avec WireGuard qui permet d’avoir des connexions directes avec l’ensemble des node du réseau. Il gère toutes les adresses IP et configurations automatiquement. Cela semble idéal, mais j’avais du mal à aller de l’avant dans cette voie car je ne voulais pas déléguer à un site extérieur les accès à mon homelab.
Headscale
Via le suivi de certains hashtags sur Mastodon, j’ai commencé à entendre parler de headscale, une implémentation d’auto-hébergement open source du contrôleur Tailscale. Il permet d’utiliser les nombreux clients officiels de Tailscale et supporte la majorité des fonctionnalités de Tailscale comme les permissions via un fichier d’ACL. Cela semblait le meilleur des deux mondes.
Son installation est relativement facile pour une personne avec un peu d’expérience dans l’auto-hébergement. Le plus simple est de l’installer via Docker sur un serveur dans votre homelab ou sur un VPS. J’ai choisi cette première méthode car je préfère limiter mes dépendances extérieures. J’ai fait le choix de faire fonctionner Headscale sur une VM séparée dans mon Proxmox pour davantage de sécurité (isolation et sauvegarde).
Voici mon Docker Compose que j’utilise avec les stacks de Portainer :
version: '3.9'
services:
headscale:
container_name: headscale
image: headscale/headscale:0.23.0
restart: unless-stopped
ports:
- 8080:8080
- 9090:9090
volumes:
- /data/docker/headscale/config:/etc/headscale
- /data/docker/headscale/data:/var/lib/headscale
environment:
- TZ=America/Montreal
command: serve
Attention, n’utilisez pas la tag “latest” ; après chaque mise à jour majeure, Headscale a de nombreux changements de rupture, et la configuration doit être revue. C’est le seul de mes conteneurs qui n’a pas été mis à jour automatiquement avec Watchtower.
Avant de commencer le Docker Compose, vous devez créer deux répertoires sur votre hôte Headscale:
- /data/docker/headscale/config
- /data/docker/headscale/data
Vous aurez également besoin d’un fichier de configuration dans le dossier “config”. Téléchargez le fichier par default pour la version 0.23.0, faites les changements ci-dessous suivant sur votre configuration personnelle.
# The url clients will connect to
server_url: https://hs.xxxx.xx
# Address to listen to / bind to on the server
listen_addr: 0.0.0.0:8080
# Address to listen to /metrics
metrics_listen_addr: 0.0.0.0:9090
database:
sqlite:
write_ahead_log: true
policy:
# The mode can be "file" or "database" that defines
# where the ACL policies are stored and read from.
# Permet de gérer les ACL dans l'interface de Headplane
mode: database
# Si vous voulez gere les regles ACL via un fichier et non Headplane.
path: "/etc/headscale/acl.hujson"
dns:
magic_dns: true
base_domain: ts.xxxx.xx
# List of DNS servers to expose to clients.
nameservers:
global:
- 192.168.1.222 # Mon PiHole
- 192.168.1.223 # Mon PiHole secondaire
Interface via Headplane
Il existe plusieurs interfaces disponibles pour interagir plus facilement avec Headscale. Pour le moment, j’ai choisi Headplane qui semble la plus maintenue et permet la gestion du fichier ACL via l’interface graphique.
Il faut créer une clé API dans un terminal de la machine où tourne Headscale:
docker exec headscale headscale apikeys create
Voici à quoi ressemble mon Docker Compose pour lancer Headscale et Headplane.
version: '3.9'
services:
headscale:
container_name: headscale
image: headscale/headscale:v0.23
restart: unless-stopped
ports:
- 8080:8080
- 9090:9090 # pour avoir les metrics
volumes:
- /data/docker/headscale/config:/etc/headscale
- /data/docker/headscale/data:/var/lib/headscale
environment:
- TZ=America/Montreal
command: serve
headplane:
container_name: headplane
image: ghcr.io/tale/headplane:latest
restart: unless-stopped
volumes:
- '/data/docker/headscale/config:/etc/headscale'
- '/data/docker/headscale/data:/var/lib/headscale'
- '/var/run/docker.sock:/var/run/docker.sock:ro'
ports:
- '3000:3000'
environment:
# This is always required for Headplane to work
COOKIE_SECRET: 'xxxxxxx' # Générer avec "openssl rand -hex 32"
HEADSCALE_URL: 'https://hs.xxxx.xxx'
CONFIG_FILE: '/etc/headscale/config.yaml'
#ACL: '/etc/headscale/acl.hujson'
HEADSCALE_INTEGRATION: 'docker'
HEADSCALE_CONTAINER: 'headscale'
DISABLE_API_KEY_LOGIN: 'true'
HOST: '0.0.0.0'
PORT: '3000'
TZ: 'America/Montreal'
ROOT_API_KEY: 'xxxxxxxxxxx' # API key précédemment généré
Je garde les choses ouvertes si une autre interface s’avère plus complète dans le futur. Entre autres, une visualisation des règles d’accès ACL (voir plus bas) sera fantastique :-).
Integration avec Nginx Proxy Manager
De mon côté, j’utilise le reverse proxy NPM (Nginx Proxy Manager). Voici la configuration qui fonctionne dans mon homelab. Il faut évidemment avoir un nom de domaine (via Cloudflare pour moi), un certificat SSL (généré via Let’s Encrypt) et avoir un système pour mettre à jour votre adresse IP locale publique sur votre register (via le docker cloudflare-ddns pour ma part). Personnellement, j’utilise mon domaine xxxx.xxx avec des wildcards pour tous les très nombreux sous-domaines.
Si vous souhaitez utiliser Headplane (ou les métriques) sur le même sous-domaine, une “Custom Nginx Configuration” doit être ajoutée dans la section “advanced”. J’ai également ajouté un fichier robots.txt pour éviter l’indexation et d’autres robots (au cas où :-).
location /{
proxy_pass http://192.168.1.240:8080;
proxy_redirect http:// https://;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location ^~ /metrics/ {
proxy_pass http://192.168.1.240:9090/metrics;
proxy_redirect http:// https://;
allow 192.168.1.0/24; # limite l'acces a seulement le reseau local
allow 100.64.1.0/24; # limite l'acces a seulement le reseau tailnet
deny all;
}
location ^~ /admin/ {
proxy_pass http://192.168.1.240:3000/admin/;
proxy_redirect http:// https://;
allow 192.168.1.0/24; # limite l'acces a seulement le reseau local
allow 100.64.1.0/24; # limite l'acces a seulement le reseau tailnet
deny all;
}
location = /robots.txt {
add_header Content-Type text/plain;
return 200 "User-agent: *\nDisallow: /\nUser-agent: GPTBot\nDisallow: /\nUser-agent: nibbler\nDisallow: /\n";
}
Par défaut, lorsque vous accédez à l’URL https://hs.xxxx.xxx/admin, le register de votre domaine va vous donner votre IP publique locale. Résultat, le filtrage ci-dessus ne fonctionnera pas car NPM verra votre IP publique et non votre IP locale. Pour résoudre simplement ce problème, vous pouvez ajouter un enregistrement DNS local [A/AAAA] dans votre Pi-Hole(s).
hs.xxxx.xx = LOCAL.IP.OF.NPM
Vue que j’ai créé près de 40 sous-domaines dans NPM, il y a une solution plus élégante, les DNS wildcards. Malheureusement, cela n’est pas encore possible de le faire via l’interface utilisateur, il faut utiliser un terminal sur votre Pi-hole.
nano /etc/dnsmasq.d/02-my-wildcard-dns.conf
Ajouter la ligne
address=/xxxx.xxx/LOCAL.IP.OF.NPM
Puis redémarrez votre Pi-hole et tout devrait marcher. Vous pouvez double- vérifier s’il fonctionne correctement en utilisant cette commande:
dig @IP.OF.YOUR.PIHOLE hs.xxxx.xxx
Le résultat devrait être
;; ANSWER SECTION:
hs.xxxx.xxx. 0 IN A LOCAL.IP.OF.NPM
Attention, il est très important de faire cette procédure pour l’ensemble de vos Pi-holes. Je me suis battu des jours pour isoler un problème majeur dans mon homelab à cause d’un Pi-hole mal configuré. Si vous utilisez Orbital Sync pour synchroniser vos Pi-holes, les wildcards ne sont pas synchronisés.
Créer les utilisateurs
Avec le terminal de la machine de headscale:
docker exec headscale headscale users create NOM-UTILISATEUR
Pour le faire dans l’interface de Headplane, cliquez sur “add a new user” dans la page user
Ajout de nodes dans Headscale
Voici les différentes procédures pour ajouter des clients Tailscale dans le réseau Headscale.
- Installation sur iOS
- Installation sur MacOS
- Installation sur Linux Debian
- Installation sur Proxmox LXC
- Installation sur NAS Synology
- Installation sur Hassio (Home Assistant OS)
Désolé, je n’ai pour le moment pas de machine Windows ni de téléphone Android à la maison pour faire un tutoriel.
Exit Node et acces aux sous-réseaux
En plus de la VM de Headscale, j’ai créé une autre VM Debian pour installer Tailscale. Elle remplit plusieurs fonctionnalités.
La plus importante est de pouvoir accéder de l’extérieur à mes machines/VM/LXC qui ne sont pas dans Tailscale. Je l’utilise régulièrement de manière transparente, au point que j’ai l’impression que mon homelab me suit toujours avec moi ! C’est tout simplement magique.
L’exit node me permet de chiffrer mon trafic internet entre mes appareils et mon homelab quand je suis en déplacement. Il est désactivé par défaut ; idéalement, je souhaiterais que Tailscale permette de l’activer automatiquement quand je suis connecté à des Wi-Fi publics. Théoriquement, si un de mes utilisateurs de Tailscale vivant en France activait un exit node, cela pourrait me permettre d’accéder au contenu géolocalisé, comme certains programmes d’Arte. Je vous laisse imaginer d’autres utilisations :-)
Pour ajouter les routes ou le exit node, cela se passe via un terminal de notre nœud Tailscale.
tailscale up --advertise-routes=192.168.1.0/24,192.168.3.0/24,192.168.4.0/24 --accept-routes --advertise-exit-node --login-server=https://hs.xxxx.xxx
Les routes et le exit node doivent être approuver dans Headscale en ligne de commande
docker exec headscale headscale route list
ID | Node | Prefix | Advertised | Enabled | Primary
2 | tailscale | 0.0.0.0/0 | false | false | -
3 | tailscale | ::/0 | false | false | -
4 | tailscale | 192.168.1.0/24 | false | false | false
5 | tailscale | 192.168.3.0/24 | false | false | false
docker exec headscale headscale route enable -r 2 3 4 5
ID | Node | Prefix | Advertised | Enabled | Primary
2 | tailscale | 0.0.0.0/0 | true | true | -
3 | tailscale | ::/0 | true | true | -
4 | tailscale | 192.168.1.0/24 | true | true | true
5 | tailscale | 192.168.3.0/24 | true | true | true
Une autre méthode consiste à le faire via l’interface de Headplane. Sur le nœud, cliquer sur les 3 points à droite et choisir “Edit route settings of Tailscale”.
Contrôle d’accès via ACL
Par défaut, un serveur VPN classique offre un accès total au réseau local. Cela n’est pas envisageable pour moi en cas de compromission par un ransomware/virus d’un de mes amis ou de moi-même. En revanche, la solution Headscale/Tailscale permet d’avoir un contrôle très précis des accès pour chacun des nœuds grâce à un simple fichier de configuration ACL.
Tout d’abord, séparer tous les noeuds du groupe (moi, utilisateur, serveur, etc.). Ensuite, créez un diagramme pour décrire les connexions que vous voulez permettre entre les diffrents groupes. Ce diagramme illustrera les connexions entre mes nœuds Tailscale. Vous pouvez utiliser un simple morceau de papier pour cette tâche. :-)
Il faut ajouter les tags au nœud. Pour ma configuration, j’en ai créé cinq tags différents :
- tag:me | Pour mes appareils personnels, ordinateur, téléphone et tablette
- tag:private | Pour mes services, seulement accessibles par moi.
- tag:semi-private | Pour les services accessibles par mes utilisateurs
- tag:user | Mes utilisateurs
- tag:remote | Mon NAS distant pour mes sauvegardes hors site.
Vous pouvez ajouter des tag via cette ligne de commande :
docker exec headscale headscale nodes tag -i <ID> -t tag:<TAGNAME>
Ou plus simplement, Headspace permet de le faire à travers une interface. Cliquez sur les 3 points à droite du nœud, puis cliquez sur “edit ACL tags”.
Voici mes règles ACL qui correspondent au graphique ci-dessus.
{
"acls": [
{
"action": "accept",
"src": ["tag:me"],
"dst": [
"tag:private:*",
"tag:remote:*",
"tag:semiprivate:*",
"tag:me:*",
"autogroup:internet:*", # accés à internet
"192.168.1.0/24:*", # accés à mon vlan principal
"192.168.3.0/24:*", # accés à mon vlan IoT
"192.168.4.0/24:*" # accés à mon vlan NoT
]
},
{
"action": "accept",
"src": ["tag:private"],
"dst": [
"tag:private:*",
"tag:remote:*",
"tag:semiprivate:*"
]
},
{
"action": "accept",
"src": ["tag:semiprivate"],
"dst": [
"tag:semiprivate:*"
]
},
{
"action": "accept",
"src": ["tag:remote"],
"dst": [
"tag:private:*"
]
},
{
"action": "accept",
"src": ["tag:user"],
"dst": [
"100.64.0.15:9000", # Mealie
"100.64.0.14:8000", # VaulWarden
"100.64.0.13:3001" # Uptime Kuma
]
},
]
}
Ajouter vos règles ACL via le fichier “/etc/headscale/acl.hujson” ou dans l’interface Headplane.
Il me reste encore beaucoup de choses à explorer dans le monde ACL, mais dans l’état actuel, cette configuration répond à mes attentes actuelles.
Pi-Holes
Mes deux Pi-Holes font partie de mon réseau Tailnet. Comme mes appareils personnels sont toujours connectés à Headscale, mes serveurs DNS filtrent une grande partie du suivi, de la publicité et d’autres désagréments d’Internet. J’ai choisi de ne pas inclure ces Pi-Holes pour mes utilisateurs, car je ne veux pas gérer leur trafic DNS, qui révèle beaucoup sur leur vie privée. Malheureusement, il ne semble pas possible d’avoir des serveurs DNS différents par tag dans Headscale/Tailscale.
Ainsi, dans l’état actuel, mes utilisateurs n’utilisent pas le DNS de Tailscale et doivent se connecter à mes services en utilisant l’IP et le port. J’espère qu’une prochaine implémentation sera plus flexible à ce sujet. Mais, si vous avez une solution à cette problématique, je suis preneur !
La Suite
Bien que mon système soit complet et stable, il reste encore quelques améliorations à envisager pour 2025.
La version bêta 0.24.0 de Headscale offre une meilleure gestion d’OpenID grâce à l’authentification OIDC. Je suis en train d’apprendre Authentik, une fonctionnalité que je souhaite ajouter prochainement. Je vais la tester dans une nouvelle VM pour ne pas perturber mon système actuel, qui est essentiel.
Pour le moment, je n’utilise ni les MagicDNS ni le split DNS de Tailscale. Je crée des enregistrements DNS locaux dans mes Pi-Holes pour lier un nom à une adresse IP (par exemple, uptime = 192.168.1.231). Tous les services sont associés à un sous-domaine dans Nginx Proxy Manager (comme nodered.xxxx.xx -> http://192.168.1.231:3001). Cette solution me permet de gérer l’ensemble des services et systèmes, qu’ils fassent ou non partie de Tailscale. Cependant, il est possible que je passe à côté d’une fonctionnalité qui simplifierait mon homelab. Vos commentaires seront les bienvenus.
Par ailleurs, j’aimerais explorer l’option d’avoir mon propre serveur DERP afin d’éviter de dépendre de ceux de Tailscale. Vous pouvez trouver davantage d’informations à ce sujet sur le site de Tailscale.
Conclusion
Même si, à première vue, la solution Headscale/Tailscale peut sembler très technique, elle est en réalité plus simple à mettre en œuvre qu’un serveur VPN, si l’on tient compte de la gestion de ses nombreux inconvénients. Cette solution est vraiment très robuste, à ce jour je n’ai pas eu de problème. Pour être exact, j’ai eu des soucis une fois à cause de mon serveur DNS mal configuré qui n’avait rien à voir avec Tailscale. Comme d’habitude, le problème est très souvent entre la chaise et le clavier :-).
Bief, je vous recommande de tester la solution solution Headscale/Tailscele, jene pense pas que vous serez decu ! Si vous êtes, comme moi, séduit par cette solution, je vous invite à contribuer au projet via GitHub, sur Discord, ou à le financer via Ko-fi. Pour ce qui est de Headplane, cela se passe via GitHub Sponsors.
N’hésitez pas à laisser un commentaire si vous avez besoin de clarifications ou d’aide, ou si j’ai raté un point important.
Ps: Je regrette seulement de ne pas avoir découvert cette solution plus tôt !
Leave a Reply