Le besoin
Accéder à son homelab depuis l’extérieur est un besoin fondamental. Les options classiques :
- Port forwarding — expose directement les services, chaque port ouvert est une surface d’attaque. Impose de maintenir des règles NAT sur la box
- VPN traditionnel (OpenVPN/WireGuard point-to-point) — architecture client-serveur, le serveur VPN est un bottleneck et un SPOF. La configuration est manuelle et fragile
- VPN mesh — chaque nœud se connecte directement aux autres en peer-to-peer, pas de point de passage central pour les données
Le VPN mesh est la solution moderne. Tailscale l’a popularisé en simplifiant WireGuard, mais il dépend d’un serveur de coordination hébergé chez eux, avec des limites sur le nombre de nœuds en version gratuite.
Pour un homelab où l’objectif est l’indépendance, on préfère tout garder chez soi.
Headscale : le serveur de coordination
Headscale est une implémentation open-source du serveur de coordination Tailscale. Il permet d’utiliser les clients Tailscale officiels (iOS, Android, Windows, Linux, macOS) avec son propre serveur.
Ce que fait le serveur de coordination
Il faut comprendre un point fondamental : le serveur de coordination (Headscale ou Tailscale) ne voit jamais le trafic. Son rôle est purement de :
- Distribuer les clés publiques WireGuard entre les nœuds
- Coordonner les connexions (aider les nœuds à se trouver malgré le NAT)
- Gérer les ACLs (qui peut communiquer avec qui)
- Distribuer les configurations DNS internes au mesh
Une fois que deux nœuds ont échangé leurs clés et établi un tunnel WireGuard, le serveur de coordination n’intervient plus. Les connexions sont peer-to-peer, chiffrées de bout en bout. Même si Headscale est compromis, l’attaquant ne peut pas déchiffrer le trafic.
Headscale vs Tailscale : ce qu’on gagne et ce qu’on perd
| Aspect | Tailscale (cloud) | Headscale (self-hosted) |
|---|---|---|
| Setup | 5 minutes | 30 minutes |
| Maintenance | Zéro | À notre charge |
| Nœuds gratuits | 100 (personal) | Illimité |
| Auth | SSO intégré (Google, etc.) | OIDC configurable |
| MagicDNS | Oui | Support basique |
| Taildrop (partage fichiers) | Oui | Non |
| Contrôle total | Non | Oui |
| Dépendance externe | Oui | Non |
Ce qu’on perd est principalement du confort (MagicDNS avancé, Taildrop). Ce qu’on gagne est la souveraineté : les clés, les logs, les ACLs, tout est chez soi.
Installation
Headscale tourne dans un LXC dédié sur Proxmox (pas dans Docker — c’est un service critique qui ne doit pas dépendre de la VM Docker).
Pourquoi un LXC dédié ?
Si Headscale tourne dans Docker sur la VM docker-infra, et que cette VM a un problème, on perd à la fois les services ET le VPN pour y accéder à distance. En LXC séparé, le VPN reste up même si Docker est down.
Installation Headscale
# Télécharger le .deb
wget https://github.com/juanfont/headscale/releases/download/v0.27.1/headscale_0.27.1_linux_amd64.deb
dpkg -i headscale_0.27.1_linux_amd64.deb
# Le service est automatiquement créé par le package
systemctl enable --now headscale
Configuration clé
# /etc/headscale/config.yaml
server_url: https://vpn.mondomaine.ovh
listen_addr: 0.0.0.0:8080
metrics_listen_addr: 0.0.0.0:9090
# Préfixes IP pour les nœuds du mesh
prefixes:
v4: 100.64.0.0/10
# Base de données
database:
type: sqlite3
sqlite:
path: /var/lib/headscale/db.sqlite
# DNS interne au mesh
dns:
base_domain: vpn.mondomaine.ovh
magic_dns: true
nameservers:
global:
- 192.168.1.54 # AdGuard Home du LAN
# DERP — serveur relais personnalisé
derp:
paths:
- /etc/headscale/derp.yaml
auto_update_enabled: false
update_frequency: 24h
Points importants :
prefixes: 100.64.0.0/10: le même range que Tailscale utilise. Les IPs VPN ne conflictent pas avec les réseaux privés classiques (10.x, 172.x, 192.168.x)nameservers: 192.168.1.54: les clients VPN utilisent AdGuard Home comme DNS, ce qui leur donne accès au split-horizon (résolution locale des domaines*.mondomaine.ovh)metrics_listen_addr: exposé pour que Prometheus scrape les métriques Headscale
Topologie du réseau VPN
Mon mesh VPN connecte des nœuds très différents :
| Nœud | IP VPN | Rôle | Localisation |
|---|---|---|---|
| docker-infra | 100.64.0.5 | Subnet router (accès LAN complet) | LAN |
| kopia | 100.64.0.3 | Backup server | LAN |
| oracle-vps | 100.64.0.6 | VPS cloud + DERP server | Paris (Oracle Cloud) |
| dns-filter | 100.64.0.7 | DNS AdGuard | LAN |
| postgres | 100.64.0.8 | Base de données | LAN |
| redis | 100.64.0.9 | Cache Redis | LAN |
| google-vps | 100.64.0.10 | Exit-node US | Iowa (Google Cloud) |
Plus les clients mobiles/desktop qui se connectent à la demande.
Subnet routing : accéder au LAN complet
Un nœud peut être configuré comme subnet router, ce qui permet aux autres nœuds du mesh d’accéder à tout le réseau local derrière lui, sans installer Tailscale sur chaque machine.
Côté client (la VM qui fait office de routeur)
tailscale up --login-server https://vpn.mondomaine.ovh \
--advertise-routes=192.168.1.0/24 \
--snat-subnet-routes=true
Le flag --snat-subnet-routes=true est crucial : il fait du SNAT (masquerading) sur le trafic VPN → LAN. Sans ça, les machines du LAN voient des paquets venant d’une IP 100.64.x.x qu’elles ne connaissent pas, et ne savent pas où envoyer les réponses.
Côté Headscale (approuver la route)
headscale routes list
# Identifier l'ID de la route advertised
headscale routes enable --route <route-id>
Côté client distant (qui veut accéder au LAN)
tailscale up --login-server https://vpn.mondomaine.ovh \
--accept-routes
Le flag --accept-routes accepte les routes annoncées par les subnet routers. Résultat : depuis un VPS cloud à Paris, on peut accéder à 192.168.1.55 (PostgreSQL sur le LAN) comme si on était à côté du serveur. Le trafic est chiffré de bout en bout via WireGuard.
Cas d’usage concret : PostgreSQL et Redis accessibles depuis le VPS cloud
Mon VPS Oracle Cloud héberge des agents et des services qui ont besoin d’accéder aux bases de données du homelab. Grâce au subnet routing :
# Depuis oracle-vps (Paris, cloud)
psql -h 192.168.1.55 -U mon_user -d ma_base
redis-cli -h 192.168.1.77
Ces connexions traversent le tunnel WireGuard via le subnet router docker-infra. La latence est celle d’Internet (~15-20 ms Paris↔domicile) mais la sécurité est celle de WireGuard.
Serveur DERP privé
Quand deux nœuds ne peuvent pas établir de connexion directe (NAT restrictif des deux côtés, par exemple un téléphone en 4G et un VPS derrière un pare-feu cloud), le trafic passe par un relais DERP (Designated Encrypted Relay for Packets).
Par défaut, Tailscale utilise ses propres serveurs DERP répartis dans le monde. Avec Headscale, on peut (et on devrait) héberger le sien.
Pourquoi un DERP privé ?
- Confidentialité : même si le trafic est chiffré E2E (WireGuard), les métadonnées (qui communique avec qui, quand, combien de données) sont visibles par le relais
- Performance : un relais géographiquement proche réduit la latence
- Résilience : pas de dépendance aux serveurs Tailscale
Déploiement
Mon serveur DERP tourne sur le VPS Oracle Cloud à Paris, dans un conteneur Docker :
# docker-compose.yml sur le VPS
services:
derp:
image: ghcr.io/tailscale/derper:latest
command:
- /app/derper
- --hostname=derp-paris.mondomaine.ovh
- --certmode=letsencrypt
- --certdir=/data/certs
- --verify-clients
- --stun
ports:
- "80:80" # Let's Encrypt
- "443:443" # DERP (HTTPS)
- "3478:3478/udp" # STUN
Le flag --verify-clients est important : il vérifie que seuls les clients de notre serveur Headscale peuvent utiliser ce relais. Sans ça, n’importe qui pourrait utiliser notre DERP comme proxy.
Configuration dans Headscale
# /etc/headscale/derp.yaml
regions:
900:
regionid: 900
regioncode: par
regionname: "Paris (Custom)"
nodes:
- name: derp-paris
regionid: 900
hostname: derp-paris.mondomaine.ovh
ipv4: 158.x.x.x
stunport: 3478
derpport: 443
La regionid: 900 est arbitraire (les IDs < 100 sont réservés par Tailscale). Le regionname apparaît dans la sortie de tailscale netcheck.
Vérification
# Sur n'importe quel client Tailscale
tailscale netcheck
# Doit afficher :
# Report:
# ...
# - 900: Paris (Custom) : 12.3ms
# ...
DNS pour le DERP
Le DERP est un service externe au LAN — il est hébergé sur un VPS cloud. Il ne doit pas être résolu vers l’IP LAN via le wildcard AdGuard. Une réécriture spécifique dans AdGuard pointe directement vers l’IP publique du VPS :
derp-paris.mondomaine.ovh → 158.x.x.x
Exit-node : IP américaine à la demande
Un nœud du mesh peut servir d’exit-node — tout le trafic Internet du client passe par ce nœud, qui apparaît comme l’IP source.
Mon VPS Google Cloud (e2-micro gratuit) basé dans l’Iowa sert d’exit-node US :
# Sur le VPS Google Cloud
tailscale up --login-server https://vpn.mondomaine.ovh \
--advertise-exit-node
# Côté Headscale, approuver l'exit-node
headscale routes enable --route <route-id>
Utilisation
# Sur mon laptop ou téléphone — activer l'exit-node US
tailscale set --exit-node=google-vps
# Vérifier l'IP publique
curl ifconfig.me
# → 35.x.x.x (IP Google Cloud US)
# Désactiver l'exit-node
tailscale set --exit-node=
Cas d’usage : accéder à du contenu géo-restreint US, tester un service avec une IP américaine, ou simplement avoir un VPN “classique” vers les US quand on est en déplacement.
Interface d’administration
Headscale n’a pas d’interface web native. J’utilise Headscale UI, une interface web tiers qui communique avec l’API Headscale :
services:
headscale-ui:
image: ghcr.io/gurucomputing/headscale-ui:latest
networks:
proxy:
ipv4_address: 172.18.0.21
labels:
- traefik.enable=true
- traefik.http.routers.headscale-ui.rule=Host(`headscale-ui.mondomaine.ovh`)
- traefik.http.routers.headscale-ui.middlewares=authentik@file
L’interface est protégée par Authentik (SSO) — seuls les admins du homelab y ont accès.
Elle permet de visualiser les nœuds connectés, les routes, les ACLs, et de gérer les pré-auth keys sans passer par la CLI.
Monitoring
Headscale expose des métriques Prometheus sur le port 9090 :
# prometheus.yml
- job_name: headscale
static_configs:
- targets: ['192.168.1.139:9090']
Métriques surveillées : nombre de nœuds connectés, sessions actives, taille de la base, dernier contact de chaque nœud.
Enregistrer de nouveaux nœuds
Créer un utilisateur
headscale users create mon-user
Générer une clé d’authentification
# Clé réutilisable, expire dans 24h
headscale preauthkeys create --user mon-user --reusable --expiration 24h
Connecter un client
# Linux
tailscale up --login-server https://vpn.mondomaine.ovh --authkey=la-cle-generee
# Android/iOS
# Modifier le serveur de contrôle dans les paramètres de l'app Tailscale
# puis utiliser la clé d'auth
# macOS/Windows
# Similaire à Linux, via la CLI ou les paramètres de l'app
Cas d’usage concrets
| Situation | Solution VPN |
|---|---|
| SSH au homelab depuis un café | Client Tailscale sur le laptop → accès direct à toutes les IPs LAN |
| Consulter Grafana depuis le téléphone | Client Tailscale iOS → grafana.mondomaine.ovh résolu en LAN |
| Backup hors-site (VPS → NAS) | VPS connecté au mesh → accès NFS/SMB via subnet route |
| PostgreSQL/Redis depuis le cloud | VPS → 192.168.1.55:5432 via subnet route |
| DNS filtré partout | AdGuard Home accessible via le mesh (nameserver VPN) |
| IP américaine pour du contenu US | Exit-node Google Cloud US |
| Debugging depuis le téléphone | Accès aux logs Loki/Grafana en mobilité |
Performance
| Métrique | Valeur |
|---|---|
| Latence peer-to-peer local | 1-3 ms |
| Latence via Internet (Paris ↔ domicile) | 15-20 ms |
| Latence via DERP (quand P2P échoue) | 25-40 ms |
| Débit WireGuard local | ~800 Mbps |
| Débit via Internet | Limité par l’upload (~300 Mbps fibre) |
| Temps d’établissement connexion | 2-5 secondes |
| Fiabilité | Zéro déconnexion non planifiée en 6+ mois |
Le protocole WireGuard est remarquablement efficient. La surcharge CPU est négligeable, même sur un LXC avec 2 cœurs. Le handshake est quasi instantané — on passe du “pas connecté” à “tunnel actif” en moins de 5 secondes.
Sécurité
Quelques bonnes pratiques appliquées :
- Clés pré-auth éphémères : les clés expirent en 24h. Pas de clé qui traîne indéfiniment
- DERP verify-clients : le relais DERP n’accepte que les clients de notre mesh
- ACLs : restreindre qui peut accéder à quoi (par défaut, tous les nœuds peuvent communiquer — à restreindre selon les besoins)
- Pas de port forwarding : aucun port n’est ouvert sur la box pour le VPN. Tout passe par DERP ou NAT traversal automatique
- Logs : Headscale log toutes les connexions, intégré au monitoring Prometheus
Conclusion
Headscale transforme l’accès distant d’un casse-tête de port forwarding en une expérience transparente. On installe le client, on se connecte, et tout le réseau local est accessible comme si on était à côté du serveur.
C’est probablement la brique de mon homelab qui a le meilleur ratio effort/bénéfice : une heure d’installation, zéro maintenance depuis, et un accès sécurisé depuis n’importe où dans le monde — du téléphone au VPS cloud en passant par le laptop au café.