Post

Headscale réseau maillé

Headscale réseau maillé

Headscale vise à mettre en œuvre une alternative open source auto-hébergée au serveur de contrôle Tailscale. L’objectif de Headscale est de fournir aux auto-hébergeurs et aux amateurs un serveur open source qu’ils peuvent utiliser pour leurs projets et leurs laboratoires. Il met en œuvre une portée étroite, un réseau unique Tailscale (tailnet), adapté à un usage personnel, ou une petite organisation open source.

Headscale + DERP intégré

Procédure complète pour installer Headscale + DERP intégré sur un VPS Debian, derrière Caddy, en utilisant le nom headscale.xoyize.xyz

VPS

  • IP: 164.132.198.38
  • IPv6: 2001:41d0:404:200::7ec4
  • Domaine xoyize.xyz, zone dns configurée headscale.xoyize.xyz

Le DERP intégré doit être déclaré avec ses deux adresses publiques pour améliorer la stabilité des connexions IPv4/IPv6.[headscale]

Architecture

On utilisera une seule URL publique :

1
https://headscale.xoyize.xyz

Répartition des ports :

Service Écoute locale/public Rôle
Caddy TCP 80, TCP 443 HTTPS, certificat Let’s Encrypt, proxy Headscale et DERP
Headscale 127.0.0.1:8080 API / serveur de coordination
DERP intégré HTTPS via Caddy + UDP 3478 Relais DERP et STUN
SSH Ton port SSH non standard Administration VPS

Le trafic NFS ne doit jamais être ouvert sur ce VPS de coordination. Le VPS est seulement le point de contrôle/relay mesh, NFS restera accessible uniquement via l’interface mesh de ton serveur Debian.

Préparation VPS Debian

Se connecter au VPS puis mise à jour du système avec des utilitaitres :

1
2
3
4
5
6
7
8
9
sudo apt update
sudo apt full-upgrade -y
sudo apt install -y \
  ca-certificates \
  curl \
  gnupg \
  ufw \
  jq \
  dnsutils

Vérifier le nom DNS depuis le VPS :

1
2
dig +short A headscale.xoyize.xyz
dig +short AAAA headscale.xoyize.xyz

Résultat attendu :

1
2
164.132.198.38
2001:41d0:404:200::7ec4

Vérifier que le serveur possède bien ces IP :

1
2
3
ip -4 addr
ip -6 addr
ip -6 route

Résultat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    altname enp0s3
    altname enxfa163e8b59df
    inet 164.132.198.38/32 metric 100 scope global dynamic ens3
       valid_lft 81962sec preferred_lft 81962sec
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 state UNKNOWN qlen 1000
    inet6 ::1/128 scope host noprefixroute 
       valid_lft forever preferred_lft forever
2: ens3: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 state UP qlen 1000
    inet6 2001:41d0:404:200::7ec4/128 scope global 
       valid_lft forever preferred_lft forever
    inet6 fe80::f816:3eff:fe8b:59df/64 scope link proto kernel_ll 
       valid_lft forever preferred_lft forever
2001:41d0:404:200::7ec4 dev ens3 proto kernel metric 256 pref medium
2001:41d0:404:200::/64 dev ens3 proto static metric 1024 pref medium
fe80::/64 dev ens3 proto kernel metric 256 pref medium
default via 2001:41d0:404:200::1 dev ens3 proto static metric 1024 pref medium

Pare-feu UFW

Avant d’activer UFW, adapter le port SSH à la configuration réelle. Ne pas copier 22 si le VPS utilise un autre port.

Pour une sécurité de base, laisser le pare-feu en refus entrant par défaut

1
2
3
4
5
6
7
8
9
10
sudo ufw default deny incoming
sudo ufw default allow outgoing

sudo ufw allow 55038/tcp comment 'SSH administration'
sudo ufw allow 80/tcp comment 'Caddy ACME HTTP'
sudo ufw allow 443/tcp comment 'Headscale and DERP HTTPS'
sudo ufw allow 3478/udp comment 'Headscale DERP STUN'

sudo ufw enable
sudo ufw status verbose

Il faut impérativement d’avoir une session SSH fonctionnelle ouverte avant sudo ufw enable.

Il faut ouvrir les règles en IPv4 et IPv6 à vérifier sur /etc/default/ufw

1
grep 'IPV6' /etc/default/ufw  # renvoie si ok: IPV6=yes

Le DERP intégré Headscale requiert STUN ; la documentation Headscale liste STUN parmi les exigences lorsque le DERP intégré est activé.[headscale]

Installer Caddy

Installer Caddy depuis le dépôt officiel :

1
2
3
4
5
6
7
8
9
10
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' \
  | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg

curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' \
  | sudo tee /etc/apt/sources.list.d/caddy-stable.list

sudo apt update
sudo apt install -y caddy

Vérification

1
2
systemctl status caddy --no-pager
caddy version  # v2.11.4 h1:XKxkMTgNSizEvKG6QHue6cAsFOteU2qA61w2tKkCWi0=  au 26/09/2026

Caddy gère HTTPS automatiquement lorsqu’un nom de domaine est déclaré dans le Caddyfile, et le conserve renouvelé.[caddyserver][caddyserver]

Caddy doit s’exécuter sous l’utilisateur système caddy, avec /var/lib/caddy accessible en écriture par cet utilisateur. Le mécanisme de certificats automatiques dépend de ce répertoire.[caddyserver][caddyserver]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
sudo systemctl stop caddy

sudo install -d \
  -o caddy \
  -g caddy \
  -m 0750 \
  /var/lib/caddy

sudo install -d \
  -o caddy \
  -g caddy \
  -m 0750 \
  /var/lib/caddy/.config/caddy

sudo chown -R caddy:caddy /var/lib/caddy

sudo systemctl start caddy
sudo systemctl status caddy --no-pager -l

Vérifier ensuite droits et propriétaire :

1
2
3
sudo ls -ld /var/lib/caddy
sudo ls -ld /var/lib/caddy/.config
sudo ls -ld /var/lib/caddy/.config/caddy

On doit voir caddy caddy comme propriétaire et groupe

1
2
3
drwxr-x--- 4 caddy caddy 4096 26 sept. 12:12 /var/lib/caddy
drwxr-xr-x 3 caddy caddy 4096 26 sept. 12:12 /var/lib/caddy/.config
drwxr-x--- 2 caddy caddy 4096 26 sept. 12:12 /var/lib/caddy/.config/caddy

Installer Headscale

Au moment de l’installation, utiliser la dernière version stable publiée dans les releases Headscale. La documentation propose des paquets .deb pour Debian/Ubuntu.headscale

Définis d’abord la version que tu souhaites installer en consultant les releases officielles, puis remplace X.Y.Z ci-dessous :

1
2
3
4
5
6
7
HEADSCALE_VERSION="0.29.4"

curl -fsSLO \
  "https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_amd64.deb"

sudo apt install -y \
  "./headscale_${HEADSCALE_VERSION}_linux_amd64.deb"

Vérifier les fichiers installés :

1
2
3
headscale version
systemctl status headscale --no-pager
sudo ls -la /etc/headscale

À ce stade, arrêter Headscale pour modifier proprement sa configuration :

1
sudo systemctl stop headscale

Configurer Headscale et DERP

Sauvegarder la configuration générée par le paquet :

1
2
sudo cp -a /etc/headscale/config.yaml \
  "/etc/headscale/config.yaml.bak.$(date +%F-%H%M%S)"

Édition:

1
sudo nano /etc/headscale/config.yaml

Vérifier ou remplacer les blocs pertinents. Les noms exacts de quelques paramètres peuvent varier selon la version : pars du fichier d’exemple correspondant exactement à la version installée, plutôt que d’écraser la configuration complète avec un exemple d’une autre release.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
server_url: https://headscale.xoyize.xyz

listen_addr: 127.0.0.1:8080

metrics_listen_addr: 127.0.0.1:9090

prefixes:
  v4: 100.64.0.0/10
  v6: fd7a:115c:a1e0::/48

derp:
  server:
    enabled: true
    region_id: 999
    region_code: xoyize
    region_name: Xoyize DERP
    stun_listen_addr: "0.0.0.0:3478"
    private_key_path: /var/lib/headscale/derp_server_private.key
    automatically_add_embedded_derp_region: true
    ipv4: 164.132.198.38
    ipv6: 2001:41d0:404:200::7ec4

database:
  type: sqlite
  sqlite:
    path: /var/lib/headscale/db.sqlite

  urls: []
  paths: []

  auto_update_enabled: false

Les éléments essentiels sont :

  • server_url doit être exactement l’URL HTTPS publique.
  • listen_addr: 127.0.0.1:8080 évite d’exposer directement l’API Headscale.
  • derp.server.enabled: true active le DERP intégré.
  • ipv4 et ipv6 déclarent les IP publiques réellement annoncées aux clients.
  • stun_listen_addr rend STUN disponible sur UDP 3478.
  • Les listes urls et paths vides, avec auto_update_enabled: false, évitent de charger les relais publics par défaut : tes nœuds utiliseront ton DERP intégré. La documentation Headscale indique que le DERP intégré est désactivé par défaut, qu’il doit être activé explicitement, et montre la déclaration des adresses publiques IPv4/IPv6.[headscale]

Créer le répertoire de données et corriger les permissions si nécessaire :

1
2
sudo install -d -o headscale -g headscale -m 0750 /var/lib/headscale
sudo chown -R headscale:headscale /var/lib/headscale

Puis démarrer et vérifier :

1
2
3
sudo systemctl enable --now headscale
sudo systemctl status headscale --no-pager -l
sudo journalctl -u headscale -n 100 --no-pager -l

Résultat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
● headscale.service - headscale coordination server for Tailscale
     Loaded: loaded (/usr/lib/systemd/system/headscale.service; enabled; preset: enabled)
     Active: active (running) since Sat 2026-09-26 11:55:32 CEST; 80ms ago
 Invocation: eeb5eacfe7a049cbbdb2b17506ebc794
   Main PID: 5841 (headscale)
      Tasks: 4 (limit: 4561)
     Memory: 5.4M (peak: 5.4M)
        CPU: 51ms
     CGroup: /system.slice/headscale.service
             └─5841 /usr/bin/headscale serve


sept. 26 11:41:22 vps-56d1c3c5 systemd[1]: Stopped headscale.service - headscale coordination server for Tailscale.
sept. 26 11:55:32 vps-56d1c3c5 systemd[1]: Started headscale.service - headscale coordination server for Tailscale.

Si le service ne démarre pas, lancer directement une validation de configuration avec la commande supportée par la release ou consulter immédiatement :

1
sudo journalctl -xeu headscale --no-pager

Configurer Caddy

Créer le Caddyfile suivant :

1
2
3
4
5
6
7
sudo tee /etc/caddy/Caddyfile >/dev/null <<'EOF'
headscale.xoyize.xyz {
    encode zstd gzip

    reverse_proxy 127.0.0.1:8080
}
EOF

Dans cette architecture, Caddy termine TLS sur TCP 443 et transmet les requêtes HTTP vers Headscale en local. La directive reverse_proxy est la directive Caddy prévue à cet effet.[caddyserver][caddyserver]

Valider

1
2
3
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
# si message WARN	Caddyfile input is not formatted; run 'caddy fmt --overwrite' to fix...
sudo caddy fmt --overwrite --config /etc/caddy/Caddyfile

et recharger :

1
2
3
sudo systemctl reload caddy
sudo systemctl status caddy --no-pager -l
sudo journalctl -u caddy -n 100 --no-pager -l

Résultat

1
2
3
sept. 26 12:02:40 vps-56d1c3c5 systemd[1]: Reloaded caddy.service - Caddy.
...
sept. 26 12:02:40 vps-56d1c3c5 systemd[1]: Reloaded caddy.service - Caddy.

Tester le certificat et le proxy :

1
2
curl -4 -I https://headscale.xoyize.xyz
curl -6 -I https://headscale.xoyize.xyz

Un code HTTP 404, 401 ou une réponse d’API est acceptable : ce qui compte est d’obtenir une connexion HTTPS valide et une réponse de Headscale. Une erreur de certificat, un timeout ou une connexion refusée doit être corrigé avant d’enrôler des machines.

Vérifications DERP/STUN

Depuis le VPS :

1
sudo ss -lntup | grep -E ':(80|443|3478|8080)\b'

Résultat

1
sudo ss -lntup | grep -E ':(80|443|3478|8080)\b'

Attendu :

  • Caddy sur TCP 80 et 443.
  • Headscale uniquement sur 127.0.0.1:8080.
  • DERP/STUN sur UDP 3478, IPv4 et IPv6 si le système et la configuration sont corrects.

Vérifie l’écoute UDP IPv6 explicitement :

1
2
3
4
5
udp   UNCONN 0      0                        *:443              *:*    users:(("caddy",pid=5996,fd=7))          
udp   UNCONN 0      0                        *:3478             *:*    users:(("headscale",pid=5841,fd=11))     
tcp   LISTEN 0      128              127.0.0.1:8080       0.0.0.0:*    users:(("headscale",pid=5841,fd=13))     
tcp   LISTEN 0      4096                     *:80               *:*    users:(("caddy",pid=5996,fd=8))          
tcp   LISTEN 0      4096                     *:443              *:*    users:(("caddy",pid=5996,fd=6)) 

On doit voir idéalement à la fois *:3478 *:*

Clients Tailscale

Créer utilisateur et clé inscription

Headscale a fait évoluer sa CLI ; vérifier d’abord les sous-commandes exactes delta version :

1
2
headscale users --help
headscale preauthkeys --help

Dans les versions récentes, créer un utilisateur, ici yann et yick :

1
2
3
4
5
6
7
8
9
10
11
sudo headscale users create yann
sudo headscale users create yick
sudo headscale users create yano
sudo headscale users create skim

sudo headscale users create leno
sudo headscale users create debjek
sudo headscale users create yannig
sudo headscale users create ugra
sudo headscale users create yai
sudo headscale users create yiak

Liste utilisateur:

1
sudo headscale users list
1
2
3
4
5
6
7
8
9
10
11
ID | Name | Username | Email | Created            
1  |      | yann     |       | 2026-09-26 10:27:03
2  |      | yick     |       | 2026-09-26 12:01:00
3  |      | yano     |       | 2026-09-26 12:35:05
4  |      | skim     |       | 2026-09-26 13:06:49
5  |      | leno     |       | 2026-09-26 15:55:20
6  |      | debjek   |       | 2026-09-26 15:55:20
7  |      | yannig   |       | 2026-09-26 15:55:21
8  |      | ugra     |       | 2026-09-26 15:55:21
9  |      | yai      |       | 2026-09-26 15:55:22
10 |      | yiak     |       | 2026-09-26 15:55:22

Créer une clé d’inscription limitée dans le temps, par exemple 24 heures :

1
2
3
4
5
6
# syntaxe
sudo headscale preauthkeys create \
  --user ID \
  --expiration 24h
# ou
sudo headscale preauthkeys create -u 1 -e 24h  

Copier la clé générée dans un gestionnaire de mots de passe, elle permettra à un appareil de rejoindre le mesh.

Pour des serveurs, créer plutôt une clé distincte à usage unique et avec un tag prédéfini — par exemple tag:server — lorsque la politique ACL sera en place. Ne pas réutiliser une clé longue durée sur plusieurs machines.

Une fois la machine connectée, la clé d’inscription (`--authkey`) n’est normalement plus nécessaire pour que le client fonctionne.

Lors de la connexion, Tailscale enregistre la machine dans le tailnet et lui attribue sa propre identité, notamment une clé de nœud. Le service peut ensuite redémarrer et se reconnecter sans réutiliser la clé d’inscription.

Vous pouvez donc :

  • supprimer la clé du terminal, d’un script ou d’un gestionnaire de secrets ;
  • révoquer la clé dans la console d’administration Tailscale si elle ne doit plus servir ;
  • conserver une clé uniquement si elle est utilisée pour inscrire automatiquement d’autres machines.

Attention : cela ne signifie pas que la machine est définitivement indépendante de l’authentification. Selon la configuration du tailnet, elle peut devoir se réauthentifier si sa clé de nœud expire, si la machine est supprimée du tailnet ou si vous exécutez :

1
sudo tailscale logout

Dans ce dernier cas, il faudra généralement exécuter à nouveau :

1
sudo tailscale up

Pour une machine serveur, vérifiez simplement que l’état reste connecté après un redémarrage :

1
2
sudo systemctl restart tailscaled
tailscale status

Par prudence, si la clé d’inscription a été exposée dans un historique ou un script, révoquez-la même si elle est à usage unique ou déjà utilisée.

Installer application tailscale

Debian
La méthode officielle installe le dépôt Tailscale puis le paquet adapté à votre version de Debian :

1
2
3
curl -fsSL https://tailscale.com/install.sh | sh
# Activer et démarrer le service
sudo systemctl enable --now tailscaled

Arch Linux
Installer le paquet depuis les dépôts officiels :

1
2
3
sudo pacman -S tailscale
# Activer et démarrer le service
sudo systemctl enable --now tailscaled

Le client Tailscale est ensuite identique, quel que soit le serveur Headscale.

Activer le client

Sur le client, installer Tailscale, puis utiliser l’URL Headscale et la clé créée :

1
2
3
sudo tailscale up \
  --login-server=https://headscale.xoyize.xyz \
  --authkey=COLLER_ICI_LA_CLE

Sur le VPS Headscale, vérifier l’enregistrement :

1
sudo headscale nodes list

list

1
2
3
4
5
ID | Hostname | Name  | MachineKey | NodeKey | User | Tags | IP addresses      | Ephemeral | Last seen           | Expiration | Connected | Expired
2  | alder    | alder | [3Ccdq]    | [qQ/Is] | yick |      | 100.64.0.2        | false     | 2026-09-26 12:11:30 | N/A        | online    | no     
   |          |       |            |         |      |      | fd7a:115c:a1e0::2 |           |                     |            |           |        
3  | pc1      | pc1   | [zvVf2]    | [2OpsD] | yann |      | 100.64.0.3        | false     | 2026-09-26 12:32:08 | N/A        | online    | no     
   |          |       |            |         |      |      | fd7a:115c:a1e0::3 |           |                     |            |           |        

Puis, côté client :

1
2
tailscale status
tailscale netcheck
1
2
3
4
5
6
7
8
9
10
11
12
100.64.0.1  ouestline  yann  linux  -  

Report:
	* Time: 2026-09-26 13:43:53.886284382+02:00
	* UDP: true
	* IPv4: yes, 83.228.219.134:40800
	* IPv6: yes, [2001:1600:18:102::c9]:44340
	* MappingVariesByDestIP: 
	* PortMapping: 
	* Nearest DERP: Xoyize DERP
	* DERP latency:
		- xoyize: 9ms     (Xoyize DERP)

Ajouter ensuite le serveur Debian NFS et un seul client Arch, avant tout le reste.

  • Vérifier d’abord SSH via le nom mesh ou l’adresse overlay
  • puis tester le montage NFS.

Très bien. Maintenant que le contrôle Headscale, le DERP et le HTTPS fonctionnent, la bonne suite consiste à définir le modèle de confiance avant d’inscrire le reste du parc, puis à verrouiller NFS à trois niveaux : politique mesh, pare-feu local, et export NFS.

Je te propose de séparer les rôles ainsi :

Rôle Tag Machines typiques Accès NFS
Administration tag:admin Desktop/laptop Arch de confiance SSH vers les serveurs ; éventuellement NFS si également client
Serveur NFS tag:nfs-server Le Debian qui héberge le dossier Reçoit TCP 2049 seulement depuis les clients NFS
Client NFS tag:nfs-client Desktop/laptop Arch et VM Debian qui montent le partage TCP 2049 uniquement vers le serveur NFS
Mobile tag:mobile Android Aucun accès NFS
Serveur standard tag:server VPS/VM Debian sans NFS SSH administré depuis tag:admin

Les ACL Headscale se configurent dans un fichier de politique HuJSON dont le chemin doit être défini via policy.path dans config.yaml. Les tags sont appliqués à l’enregistrement ou administrés avec headscale nodes tag.[headscale][headscale]

Politique Headscale

Inventaire et rôles

Alias Système / rôle Adresse actuelle Tag(s) Headscale Accès NFS
cwwk Serveur Debian NFS 192.168.0.205 tag:server, tag:nfs-server Héberge l’export
e6230w Laptop CachyOS 192.168.10.90 tag:admin, tag:nfs-client Monte l’export
pc1 Desktop EndeavourOS LAN, non listé dans CSV tag:admin, tag:nfs-client Monte l’export
proxmox Hôte Proxmox 192.168.0.215 tag:server Non
vm105 VM Debian 192.168.0.229 tag:server Non initialement
skrime VPS IPv6-only 2a14:7c0:1002:19a8:: tag:server Non
yannig VPS Debian 51.38.37.240 tag:server Non
xoyaz VPS Debian 51.254.133.45 tag:server Non
yannir VPS Debian 144.91.89.149 tag:server Non
yiak VPS Debian 83.228.219.134 tag:server Non
sxb VPS Headscale / DERP 164.132.198.38, 2001:41d0:404:200::7ec4 Aucun, ou tag:control-plane Non
Android Mobile Variable tag:mobile Jamais

skrime est le test prioritaire de ton IPv6-only : ajoute-le assez tôt, après e6230w et cwwk, afin de confirmer le chemin vers Headscale et DERP sans dépendre d’IPv4.

Vérifier le nom d’utilisateur

Avant de déposer cette politique, vérifier le nom exact de l’utilisateur Headscale :

1
sudo headscale users list

Si la sortie est, par exemple :

1
2
ID | Name
1  | yann

garder :

1
"group:admin": ["yann"]

S’il est affiché autrement, utiliser ce nom exact. Ne pas mettre yann@ sans raison : Headscale attend l’identité configurée dans sa propre base, pas nécessairement une identité de fournisseur externe.

Déclarer le chemin dans Headscale

Sur le VPS Headscale, localiser la configuration et vérifier le chemin actuellement défini :

1
2
sudo grep -nE '^(policy:| *path:)' /etc/headscale/config.yaml
sudo grep -n -A5 -B2 '^policy:' /etc/headscale/config.yaml

On a un bloc de ce type :

1
2
3
4
5
6
7
8
9
10
11
220:    path: /var/lib/headscale/db.sqlite
295:policy:
300:  path: ""
293-# ACLs: https://tailscale.com/docs/features/access-control/acls
294-# Grants: https://tailscale.com/docs/features/access-control/grants
295:policy:
296-  # The mode can be "file" or "database" that defines
297-  # where the policies are stored and read from.
298-  mode: file
299-  # If the mode is set to "file", the path to a HuJSON file containing policies.
300-  path: ""

On ajoute

1
2
policy:
  path: "/etc/headscale/policy.hujson"

Créer /etc/headscale/policy.hujson sur sxb par cette politique. Elle est restrictive par défaut :

  • Les postes d’administration peuvent joindre chaque serveur uniquement sur son port SSH actuel.
  • Les clients NFS peuvent joindre cwwk seulement en TCP/2049.
  • Android n’obtient aucune règle vers NFS.
  • Aucun accès implicite « tous ports, toutes machines » n’existe.

Créer le fichier de politique avec des permissions restrictives :

1
2
3
4
sudo install -o root -g headscale -m 0640 \
  /dev/null /etc/headscale/policy.hujson

sudo nano /etc/headscale/policy.hujson

La politique suivante est plus sûre et ne suppose pas que les utilisateurs des machines sont identiques. Elle donne au seul compte d’administration yann@ le droit de gérer les tags, tout en appliquant les droits réseau par rôle de machine :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
{
  "groups": {
    "group:mesh-admin": ["yann@"]
  },

  "tagOwners": {
    "tag:admin": ["group:mesh-admin"],
    "tag:server": ["group:mesh-admin"],
    "tag:nfs-server": ["group:mesh-admin"],
    "tag:nfs-client": ["group:mesh-admin"],
    "tag:mobile": ["group:mesh-admin"],
    "tag:control-plane": ["group:mesh-admin"]
  },

  "acls": [
    // Tes deux postes d'administration, une fois marqués tag:admin,
    // peuvent administrer les serveurs via leurs ports SSH existants.
    {
      "action": "accept",
      "src": ["tag:admin"],
      "dst": [
        "tag:server:51002", // skrime vm9471
        "tag:server:55045", // xoyaz
        "tag:server:55134", // yiak
        "tag:server:55149", // yannir
        "tag:server:55215", // proxmox
        "tag:server:55229", // vm105
        "tag:server:55240", // yannig
        "tag:server:56230", // e6230
        "tag:nfs-server:55205" // cwwk alder
      ]
    },

    // Partage NFS : exclusivement les nœuds tag:nfs-client,
    // exclusivement le nœud tag:nfs-server, exclusivement TCP/2049.
    {
      "action": "accept",
      "src": ["tag:nfs-client"],
      "dst": ["tag:nfs-server:2049"]
    }
  ]
}

Dans cette version :

  • cwwk recevra : tag:server,tag:nfs-server.
  • e6230w recevra : tag:admin,tag:nfs-client.
  • pc1 recevra : tag:admin,tag:nfs-client.
  • proxmox, vm105, skrime, yannig, xoyaz, yannir, yiak reçoivent : tag:server.
  • Android reçoit : tag:mobile seulement.
  • sxb ne doit pas recevoir de tag:nfs-*; il peut rester non inscrit, ou être classé tag:control-plane si tu installes un client Tailscale pour l’administrer, mais ce n’est pas nécessaire.

Après enregistrement :

1
2
3
sudo systemctl restart headscale
sudo systemctl status headscale --no-pager -l
sudo journalctl -u headscale -n 100 --no-pager

Si Headscale refuse de démarrer, restaurer immédiatement le fichier précédent et lire l’erreur de syntaxe dans le journal.

Préserver le service pendant les tests

Ne redémarrer pas Headscale directement après une modification non validée. La version expose les outils de politique nécessaires :

1
2
3
4
5
6
7
8
9
# Sauvegarde
sudo headscale policy get > /tmp/policy-current.hujson

sudo cp -a /etc/headscale/policy.hujson \
  "/etc/headscale/policy.hujson.bak.$(date +%F-%H%M%S)"
# Modifier et vérifier
sudo headscale policy check \
  --file /etc/headscale/policy.hujson
# si OK --> Policy is valid

Redémarrer

1
sudo systemctl restart headscale

Ordre d’enrôlement

N’ajoute pas les dix machines d’un coup. Cet ordre isole les problèmes réseau, ACL et NFS.

  1. pc1 : premier poste d’administration et premier client NFS.
  2. cwwk : serveur NFS.
  3. e6230w : second client NFS.
  4. skrime : VPS IPv6-only.
  5. proxmox et vm105.
  6. Les VPS publics restants : yannig, xoyaz, yannir, yiak.
  7. Android, taggé seulement tag:mobile.

Avant de créer les clés, consulte la syntaxe exacte de ta version :

1
2
sudo headscale preauthkeys create --help
sudo headscale nodes tag --help

Inscription de PC1

Comme pc1 est un poste humain et non un serveur, il peut être enregistré puis taggé depuis Headscale. Sur sxb :

1
2
3
sudo headscale preauthkeys create \
  --user 1 \         # yann --> ID=1
  --expiration 1h

Sur pc1 :

1
2
3
4
5
6
sudo pacman -Syu tailscale
sudo systemctl enable --now tailscaled

sudo tailscale up \
  --login-server=https://headscale.xoyize.xyz \
  --authkey=COLLE_LA_CLE

Sur sxb, liste les nœuds et relève l’ID de PC1 :

1
sudo headscale nodes list

Puis tague-le :

1
2
3
sudo headscale nodes tag \
  -i ID_PC1 \
  -t tag:admin,tag:nfs-client

Ces tags donnent à PC1 ses droits de SSH d’administration et de montage NFS.

Inscription de cwwk

Crée une clé à usage court, avec les tags serveur et NFS :

1
2
3
4
sudo headscale preauthkeys create \
  --user yann \
  --expiration 1h \
  --tags tag:server,tag:nfs-server

Sur cwwk :

1
2
3
4
5
6
7
sudo apt update
sudo apt install -y tailscale
sudo systemctl enable --now tailscaled

sudo tailscale up \
  --login-server=https://headscale.xoyize.xyz \
  --authkey=COLLE_LA_CLE

Puis, sur sxb :

1
sudo headscale nodes list

Tu dois voir cwwk avec les tags tag:server et tag:nfs-server. Sinon :

1
2
3
sudo headscale nodes tag \
  -i ID_CWWK \
  -t tag:server,tag:nfs-server

Ne lance pas --advertise-tags en plus si la clé porte déjà ces tags : cela évite la confusion.

Inscription de e6230w

Sur sxb :

1
2
3
4
sudo headscale preauthkeys create \
  --user yann \
  --expiration 1h \
  --tags tag:admin,tag:nfs-client

Sur le laptop CachyOS :

1
2
3
4
5
6
sudo pacman -Syu tailscale
sudo systemctl enable --now tailscaled

sudo tailscale up \
  --login-server=https://headscale.xoyize.xyz \
  --authkey=COLLE_LA_CLE

Puis valide :

1
2
3
tailscale status
tailscale netcheck
tailscale ping cwwk

Sur le VPS :

1
sudo headscale nodes list

Inscription de skrime IPv6-only

Cette machine est importante parce qu’elle valide l’objectif qui t’avait bloqué avec Nebula.

Sur sxb, crée une clé serveur :

1
2
3
4
sudo headscale preauthkeys create \
  --user yann \
  --expiration 1h \
  --tags tag:server

Sur skrime :

1
2
3
4
5
6
7
8
9
10
11
12
13
getent ahosts headscale.xoyize.xyz
curl -6 -I https://headscale.xoyize.xyz

sudo apt update
sudo apt install -y tailscale
sudo systemctl enable --now tailscaled

sudo tailscale up \
  --login-server=https://headscale.xoyize.xyz \
  --authkey=COLLE_LA_CLE

tailscale netcheck
tailscale status

La commande getent ahosts doit au minimum faire apparaître l’IPv6 2001:41d0:404:200::7ec4, et curl -6 doit réussir avant même l’installation du client. tailscale netcheck indiquera ensuite si UDP et IPv6 sont utilisables depuis ce VPS.

Serveurs restants

Pour chaque serveur Debian, crée une clé courte et taggée tag:server :

1
2
3
4
sudo headscale preauthkeys create \
  --user yann \
  --expiration 1h \
  --tags tag:server

Puis, sur chaque serveur :

1
2
3
4
5
6
7
sudo apt update
sudo apt install -y tailscale
sudo systemctl enable --now tailscaled

sudo tailscale up \
  --login-server=https://headscale.xoyize.xyz \
  --authkey=COLLE_LA_CLE

Répète avec une nouvelle clé pour chaque machine. À la fin de chaque enrôlement :

1
sudo headscale nodes list

Tu pourras distinguer les hôtes par leurs noms Tailscale ; ajuste le hostname Linux avant l’inscription si nécessaire :

1
sudo hostnamectl set-hostname vm105

Fais-le avant tailscale up, ou réinscris/renomme proprement le nœud s’il apparaît sous un nom inutilisable.

Attribution manuelle des tags

Après qu’une machine est enregistrée avec son utilisateur correspondant, relèver son ID :

1
sudo headscale nodes list

Puis appliquer son ou ses tags depuis le VPS.

Pour cwwk :

1
2
3
sudo headscale nodes tag \
  -i ID_CWWK \
  -t tag:server,tag:nfs-server

Pour e6230w :

1
2
3
sudo headscale nodes tag \
  -i ID_E6230W \
  -t tag:admin,tag:nfs-client

Pour pc1 :

1
2
3
sudo headscale nodes tag \
  -i ID_PC1 \
  -t tag:admin,tag:nfs-client

Pour chacun des autres serveurs :

1
2
3
sudo headscale nodes tag \
  -i ID_SERVEUR \
  -t tag:server

Pour Android :

1
2
3
sudo headscale nodes tag \
  -i ID_ANDROID \
  -t tag:mobile

Puis contrôle le résultat :

1
sudo headscale nodes list

Ne jamais donner le tag tag:nfs-client à Android, skrime, aux VPS publics ou au VPS Headscale/DERP.

Verrouiller NFS sur cwwk

Adresses IP sur cwwk

1
2
3
4
5
6
7
8
23: tailscale0: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1280 qdisc fq_codel state UNKNOWN group default qlen 500
    link/none 
    inet 100.64.0.2/32 scope global tailscale0
       valid_lft forever preferred_lft forever
    inet6 fd7a:115c:a1e0::2/128 scope global 
       valid_lft forever preferred_lft forever
    inet6 fe80::f61e:fceb:e27b:233f/64 scope link stable-privacy proto kernel_ll 
       valid_lft forever preferred_lft forever

Lorsque pc1, cwwk et e6230w apparaissent dans headscale nodes list

1
2
3
4
5
6
7
8
ID | Hostname | Name   | MachineKey | NodeKey | User           | Tags           | IP addresses      | Ephemeral | Last seen           | Expiration | Connected | Expired
2  | alder    | alder  | [3Ccdq]    | [qQ/Is] | tagged-devices | tag:nfs-server | 100.64.0.2        | false     | 2026-09-26 14:29:55 | N/A        | online    | no     
   |          |        |            |         |                | tag:server     | fd7a:115c:a1e0::2 |           |                     |            |           |        
3  | pc1      | pc1    | [zvVf2]    | [2OpsD] | tagged-devices | tag:admin      | 100.64.0.3        | false     | 2026-09-26 14:29:54 | N/A        | online    | no     
   |          |        |            |         |                | tag:nfs-client | fd7a:115c:a1e0::3 |           |                     |            |           |        
4  | e6230    | e6230  | [fwlWZ]    | [ZHTpC] | tagged-devices | tag:admin      | 100.64.0.4        | false     | 2026-09-26 14:29:54 | N/A        | online    | no     
   |          |        |            |         |                | tag:nfs-client | fd7a:115c:a1e0::4 |           |                     |            |           |        
5  | vm9471   | vm9471 | [LmbOQ]    | [PonWl] | tagged-devices | tag:server     | 100.64.0.5        | false     | 2026-09-26 14:29:54 | N/A        | online    | no     

relèver les IP mesh de pc1 et e6230w

1
2
3
cwwk    100.64.0.2
pc1     100.64.0.3
e6230w  100.64.0.4

Sur cwwk, dans /etc/exports :

1
2
3
/sharenfs \
  100.64.0.3(rw,sync,no_subtree_check,root_squash,sec=sys) \
  100.64.0.4(rw,sync,no_subtree_check,root_squash,sec=sys)

Appliquer :

1
2
sudo exportfs -rav
sudo exportfs -v

Puis protèger le port NFS localement :

1
2
3
4
5
6
7
sudo ufw allow in on tailscale0 to any port 2049 proto tcp \
  comment 'NFSv4 private mesh only'

sudo ufw deny in 2049/tcp \
  comment 'Block NFS outside mesh'

sudo ufw status numbered

Status

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Status: active

To                         Action      From
--                         ------      ----
443                        ALLOW       Anywhere                  
DNS                        ALLOW       Anywhere                  
55205/tcp                  ALLOW       Anywhere                  
Anywhere                   ALLOW       192.168.0.0/24            
Anywhere                   ALLOW       192.168.10.0/24           
Anywhere                   DENY        132.196.69.70             
2049/tcp on tailscale0     ALLOW       Anywhere                   # NFSv4 private mesh only
2049/tcp                   DENY        Anywhere                   # Block NFS outside mesh
443 (v6)                   ALLOW       Anywhere (v6)             
DNS (v6)                   ALLOW       Anywhere (v6)             
55205/tcp (v6)             ALLOW       Anywhere (v6)             
2049/tcp (v6) on tailscale0 ALLOW       Anywhere (v6)              # NFSv4 private mesh only
2049/tcp (v6)              DENY        Anywhere (v6)              # Block NFS outside mesh

NFS est donc protégé par :

  • L’absence d’exposition Internet volontaire.
  • L’ACL Headscale : seuls les nœuds avec tag:nfs-client atteignent TCP/2049.
  • Le pare-feu cwwk : TCP/2049 accepté seulement par tailscale0.
  • /etc/exports : seules les IP mesh de PC1 et e6230w peuvent monter l’export.
  • root_squash : root sur un client ne devient pas root sur cwwk.

/etc/exports contrôle explicitement quels clients peuvent monter un chemin, et root_squash fait partie des protections NFS usuelles.[man7][man7]

SSH - migration vers les IP mesh

Crée progressivement des entrées parallèles dans ton fichier ~/.ssh_servers_private, sans supprimer les endpoints actuels tant que le mesh est en validation.

Exemple, après avoir relevé les IP mesh :

1
2
3
Alias;User;IPouDomain;Port_SSH;SSH_Key;Local;Distant;Ping;Opt;IP4_6
cwwk-mesh;yick;100.64.0.2;55205;yick-ed25519;;;Y;;4
skrime-mesh;skim;fd7a:115c:a1e0::1234;51002;skrime-ed25519;;;Y;;6

Tu peux aussi utiliser les noms DNS Headscale si MagicDNS fonctionne correctement, mais les IP mesh sont le meilleur point de départ pour isoler les problèmes de DNS, ACL ou SSH.

Vérification finale

Depuis pc1 :

1
2
3
4
# IP alder= 160.664.0.2
tailscale status
tailscale ping alder
nc -vz 160.664.0.2 2049

Depuis skrime, Android ou un VPS tel que xoyaz :

1
nc -vz 160.664.0.2 2049
Le test vers TCP/2049 doit échouer hors clients NFS.

Puis monter dans alder cwwk :

1
2
3
4
5
sudo mkdir -p /sharenfs
sudo mount -t nfs4 \
  -o vers=4.2,proto=tcp,sec=sys \
  160.664.0.2:/sharenfs \
  /sharenfs

Eléments critiques :

1
2
3
4
/etc/headscale/
/var/lib/headscale/
/etc/caddy/
/var/lib/caddy/

Le VPS sxb ne doit pas rejoindre le partage NFS et ne doit jamais exposer, monter ni relayer NFS. Il reste limité aux fonctions Headscale, DERP, STUN, Caddy et administration SSH.

Cet article est sous licence CC BY 4.0 par l'auteur.