Tailscale, c’est bien. L’idée de reposer sur des serveurs tiers pour son réseau mesh, un peu moins. Headscale est l’implémentation open source du serveur de contrôle Tailscale : on garde les clients officiels, on remplace le plan de contrôle par quelque chose qu’on héberge soi-même.
Point de départ : permettre un accès distant sécurisé aux serveurs d’une école. Pas envie de leur ouvrir des ports en direct ou de faire confiance à un service cloud tiers pour la gestion du réseau. Un petit VPS à 3-4€/mois avec Headscale, et les machines de l’école deviennent accessibles depuis n’importe où, sans exposition publique.
- Headscale pour le plan de contrôle
- Headplane pour l’interface d’administration
- Authelia pour l’authentification
- Caddy comme reverse proxy
- Le tout dans Docker Compose sur un VPS
Ce qu’on construit
~/workshop/headscale/
├── headscale/ # config Headscale
├── headplane/ # config Headplane (webui)
├── authelia/ # config Authelia
├── caddy/ # Caddyfile
└── docker-compose.yml
J’ai utilisé un sous-domaine d’un domaine que j’avais déjà. Caddy s’occupe des certificats Let’s Encrypt tout seul.
Headscale
Le conteneur
# docker-compose.yml
name: "headscale-stack"
services:
headscale:
image: headscale/headscale:0.25.1
restart: unless-stopped
container_name: headscale
ports:
- "0.0.0.0:8080:8080"
- "127.0.0.1:9090:9090" # metrics/debug, interne uniquement
environment:
- TZ=Europe/Paris
volumes:
- ./headscale:/etc/headscale:ro
- headscale_data_lib:/var/lib/headscale:rw
- headscale_data_run:/var/run/headscale:rw
command: serve
volumes:
headscale_data_lib:
headscale_data_run:
Fixer la version (bonne pratique dans la plupart des cas) évite les surprises : il y a eu plusieurs incompatibilités entre versions mineures. La dernière version est sur le repo GitHub.
La configuration
On récupère le fichier exemple depuis le repo (en adaptant la version) :
wget -O ./headscale/config.yaml \
https://raw.githubusercontent.com/juanfont/headscale/v0.25.1/config-example.yaml
Ce qui est important à changer :
server_url: https://headscale.mondomaine.fr:443
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 127.0.0.1:9090
prefixes:
v4: 100.64.0.0/10
v6: fd7a:115c:a1e0::/48
allocation: sequential
database:
type: sqlite
sqlite:
path: /var/lib/headscale/db.sqlite
write_ahead_log: true
acme_email: "votre@email.fr"
dns:
magic_dns: true
base_domain: mondomaine.fr # suffixe pour les FQDN des clients, doit être différent du domaine Headscale
nameservers:
global:
- 1.1.1.1
- 9.9.9.9
unix_socket: /var/run/headscale/headscale.sock
unix_socket_permission: "0770"
logtail:
enabled: false # désactive l'envoi de logs vers Tailscale Inc.
Le reste peut rester en valeurs par défaut pour commencer.
Headplane (webui)
Headplane est l’interface d’administration officielle de Headscale. Elle s’appuie sur le socket Unix de Headscale via gRPC.
# docker-compose.yml (suite)
headplane:
image: ghcr.io/tale/headplane:latest
restart: unless-stopped
container_name: headplane
volumes:
- headscale_data_run:/var/run/headscale:ro
- ./headplane/config.yaml:/etc/headplane/config.yaml:ro
depends_on:
- headscale
Configuration minimale ./headplane/config.yaml :
headscale:
url: http://headscale:8080
config_path: /etc/headscale/config.yaml
integration:
headscale_socket: /var/run/headscale/headscale.sock
Headplane expose son interface sur le port 3000 par défaut. Caddy s’en occupe ensuite.
Authelia
Authelia protège l’interface Headplane avec une couche SSO. Headscale peut aussi s’y brancher via OIDC pour l’enregistrement des clients, mais c’est optionnel.
Secrets
mkdir -p authelia/.secrets
openssl rand -base64 32 > authelia/.secrets/jwt_secret
openssl rand -base64 32 > authelia/.secrets/session_secret
openssl rand -base64 32 > authelia/.secrets/storage_encryption_key
Le conteneur
# docker-compose.yml (suite)
authelia:
image: authelia/authelia:latest
restart: unless-stopped
container_name: authelia
volumes:
- ./authelia/config:/config
- ./authelia/.secrets:/secrets:ro
environment:
- AUTHELIA_JWT_SECRET_FILE=/secrets/jwt_secret
- AUTHELIA_SESSION_SECRET_FILE=/secrets/session_secret
- AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE=/secrets/storage_encryption_key
Configuration ./authelia/config/configuration.yaml
server:
host: 0.0.0.0
port: 9091
log:
level: info
authentication_backend:
file:
path: /config/users_database.yml
access_control:
default_policy: deny
rules:
- domain: headplane.mondomaine.fr
policy: two_factor
session:
name: authelia_session
domain: mondomaine.fr
expiration: 1h
inactivity: 5m
storage:
local:
path: /config/db.sqlite3
notifier:
filesystem:
filename: /config/notification.txt # remplacer par smtp en prod
Créer un utilisateur
docker run authelia/authelia:latest \
authelia crypto hash generate argon2 \
--random --random.length 20 --random.charset alphanumeric
Le hash va dans ./authelia/config/users_database.yml :
users:
florent:
displayname: "Florent"
password: "$argon2id$..."
email: votre@email.fr
groups:
- admins
Caddy (reverse proxy)
# docker-compose.yml (suite)
caddy:
image: caddy:latest
restart: unless-stopped
container_name: caddy
ports:
- "80:80"
- "443:443"
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
volumes:
caddy_data:
caddy_config:
./caddy/Caddyfile :
headscale.mondomaine.fr {
reverse_proxy headscale:8080
}
headplane.mondomaine.fr {
forward_auth authelia:9091 {
uri /api/verify?rd=https://auth.mondomaine.fr
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
}
reverse_proxy headplane:3000
}
auth.mondomaine.fr {
reverse_proxy authelia:9091
}
Démarrage
docker network create frontend
docker compose up -d
Créer une clé API pour Headplane :
docker exec headscale headscale apikeys create
Connecter des clients
Linux / macOS
PRE_AUTH_KEY=$(docker exec headscale headscale preauthkeys create --user florent)
tailscale up \
--auth-key=$PRE_AUTH_KEY \
--login-server=https://headscale.mondomaine.fr \
--accept-routes \
--accept-dns=false
Windows (PowerShell admin)
$AuthKey = "votre_preauth_key"
Start-Process powershell -Verb runAs -ArgumentList `
"net stop Tailscale; `
Remove-Item -Force -Recurse $env:LOCALAPPDATA\Tailscale; `
net start Tailscale; `
tailscale logout; `
tailscale up --auth-key=$AuthKey --login-server=https://headscale.mondomaine.fr --accept-routes --unattended; exit"
Exposer un subnet
Pour exposer un réseau local (ex: 192.168.10.0/24) aux autres clients :
# Sur la machine qui fait passerelle
tailscale up --advertise-routes=192.168.10.0/24 \
--login-server=https://headscale.mondomaine.fr \
--auth-key=<key>
# Autoriser la route côté serveur
docker exec headscale headscale routes list
docker exec headscale headscale routes enable -r <id>
Notes
La stack est délibérément simple : SQLite, fichiers locaux, pas de DERP custom. Ca tourne sur un petit VPS sans problème pour un usage personnel ou une petite équipe.
Quelques variantes si ça ne colle pas avec l’existant :
- Remplacer Caddy par Traefik (plus flexible pour du multi-service existant)
- Utiliser Cloudflare Tunnel à la place du VPS exposé
- Passer à PostgreSQL si on a besoin de réplication
- Brancher Headscale sur un OIDC externe plutôt qu’Authelia (Keycloak, Authentik…)
