VPN (Headscale / Tailscale)
The VPN connects all zones and remote machines: Headscale 🡕 is the coordinator (on the HCS), Tailscale 🡕 is the client on each node. Overview in Network.
Enabling the VPN
Section titled “Enabling the VPN”Enabling coordination and declaring the HCS (profile: hcs) is sufficient:
network: coordination: enable: true hostname: "hcs" domain: "headscale"Roles are derived from each host’s profile:
- HCS: mesh coordination (Headscale).
- Gateway: subnet router + exit node: publishes its zone’s subnet.
- Other nodes: Tailscale clients.
Prepare the tailnet (once)
Section titled “Prepare the tailnet (once)”All machines join the mesh via a shared Headscale attach user (nix). It is created once for the entire network; if it already exists, skip this step.
just enter hcssudo headscale users list # already present? → nothing to dosudo headscale users create nix --display-name "Nix Admin" --email "nix@domain.tld"Register a gateway
Section titled “Register a gateway”The gateway profile already makes the gateway a Tailscale client: at
deployment, the tailscaled-autoconnect service registers it automatically on
Headscale and announces its zone’s subnet. Two manual steps remain:
provide an attach key in sops (before deployment) and approve the
routes on the HCS (after).
-
On the HCS: generate an attach key
Fenêtre de terminal just enter hcssudo headscale users list # ID of the nix usersudo headscale preauthkeys create --reusable --expiration 1h --user <id>Copy the displayed key (
tskey-...). It only needs to live long enough for the deployment. -
Store the key in secrets
Fenêtre de terminal just sops # opens usr/secrets/secrets.yamlSet the key under
tailscale/authKey:usr/secrets/secrets.yaml tailscale:authKey: "tskey-..." -
Deploy the gateway
Fenêtre de terminal just apply gwThe gateway registers itself automatically. Internally, the daemon runs:
Fenêtre de terminal tailscale up \--login-server https://headscale.domain.tld \--auth-key file:/run/secrets/tailscale/authKey \--advertise-routes 10.0.0.0/16 --accept-routes --reset -
On the HCS: approve the announced routes
Fenêtre de terminal sudo headscale nodes list # ID of the gatewaysudo headscale nodes approve-routes --identifier <id> --routes 10.0.0.0/16 -
Declare the gateway’s tailnet IP
At registration, Headscale assigns the gateway a tailnet IP (
100.64.x.y), visible inheadscale nodes list. Record it inetc/config.yaml: other nodes reach the zone (internal DNS, replication, monitoring) via this address.etc/config.yaml zones:maison:gateway:vpn:ipv4: "100.64.0.8"Then regenerate and redeploy the affected nodes (the HCS and the zone’s clients) to propagate the address.
Administration
Section titled “Administration”The headscale command (alias h = sudo headscale) on the HCS:
sudo headscale users list # userssudo headscale nodes list # connected clientssudo headscale nodes routes # advertised / approved routesClient self-healing
Section titled “Client self-healing”A tailscaled can silently disconnect from Headscale: the control connection is lost while the service remains “active”, and a gateway’s subnet becomes unreachable until a manual restart. A local watchdog detects and fixes this, on every Tailscale client.
| Aspect | Behavior |
|---|---|
| Probe | every 60 s: tailscale status (backend, online state, health) |
| Debounce | 3 consecutive probe failures (~3 min) before acting: tolerates transient WAN outages |
| Fix | systemctl restart tailscaled (automatic re-registration on Headscale) |
| Anti-loop | only one fix per 10 min window |
| Observability | metrics dnf_tailscale_healthy and dnf_tailscale_selfheal_restarts_total (textfile node_exporter) |
Two alerts accompany the mechanism:
TailscaleUnhealthy: stuck unhealthy despite the fix (underlying issue);TailscaleFlapping: self-heal triggers too often (root cause to address).
Control plane bootstrap
Section titled “Control plane bootstrap”The Headscale server name normally resolves through the tailnet: on a gateway, dnsmasq routes the whole network domain to the tailnet’s DNS server. The client would therefore need the VPN to reach the server that brings up the VPN — and at boot, the local resolver is not listening yet.
Each Tailscale client therefore receives the HCS public IP in its /etc/hosts,
deduced from etc/config.yaml (the HCS itself is excluded):
grep headscale /etc/hosts # 217.182.207.26 headscale.example.orgThe client thus needs no resolver to connect, even when the local DNS is down — precisely when the self-repair mechanism above must be able to act.
Access from outside
Section titled “Access from outside”To join the network from outside a zone, see Connecting from outside.