Headscale: come usare Tailscale sul tuo VPS
Configura un server di controllo Tailscale sul tuo VPS: installa headscale dal pacchetto .deb ufficiale, imposta server_url e collega il primo nodo.
Che cos'è headscale
Headscale è un'implementazione self-hosted del server di controllo Tailscale. La macchina che coordina la rete privata è quindi un VPS di tua proprietà. È un progetto della community e non è gestito da Tailscale Inc. Ogni macchina esegue comunque il client ufficiale tailscale, configurato per usare il tuo server con un solo flag: --login-server.
Il server di controllo gestisce i dispositivi autorizzati a usare la rete. Assegna a ogni nodo un indirizzo nell'intervallo 100.64.0.0/10, distribuisce le chiavi pubbliche e comunica ai nodi come trovarsi tra loro. I tunnel continuano a usare WireGuard e vengono stabiliti direttamente tra i nodi. Il traffico tra due macchine non passa dal server headscale, a meno che non sia possibile stabilire un percorso diretto e i nodi debbano usare un relay.
Ogni istanza di headscale gestisce una sola tailnet, cioè una rete Tailscale. Il progetto la considera adatta all'uso personale o a una piccola organizzazione. Con tre o quattro macchine, una VPN WireGuard semplice su un VPS di tua proprietà richiede meno software da gestire e offre meno punti di errore. Headscale è utile quando non vuoi più scrivere manualmente un blocco [Peer] per ogni nuovo laptop. Per un confronto più ampio tra i due modelli, consulta le differenze tra WireGuard e Tailscale.
Cosa serve prima dell'installazione
- Un VPS con Ubuntu 24.04, un indirizzo IPv4 pubblico e accesso sudo. Se il server è nuovo, seguire prima i primi dieci minuti su un nuovo VPS.
- Un record DNS A che punti a quell'indirizzo. Questa guida usa
headscale.example.com. - Un secondo dominio o sottodominio per MagicDNS. Questa guida usa
tailnet.example.net. Non deve essere lo stesso dominio indicato inserver_url. - Un computer client da aggiungere, con Linux, macOS, Windows, Android o iOS.
Installare headscale dal pacchetto .deb ufficiale
Il progetto pubblica pacchetti .deb nella pagina delle release su GitHub. A luglio 2026 la release corrente è 0.29.3. Verifica prima l'architettura, perché il nome del file la indica.
sudo apt update
sudo apt install -y wget
dpkg --print-architectureQuesto comando restituisce amd64 su una normale VPS x86 e arm64 su un piano di tipo Ampere o Graviton. Inserisci il risultato nella variabile seguente.
HEADSCALE_VERSION="0.29.3"
HEADSCALE_ARCH="amd64"
wget --output-document=headscale.deb \\
"https://github.com/juanfont/headscale/releases/download/v${HEADSCALE_VERSION}/headscale_${HEADSCALE_VERSION}_linux_${HEADSCALE_ARCH}.deb"
sudo apt install -y ./headscale.deb
headscale versionIl prefisso ./ davanti al nome del file è obbligatorio. Senza di esso, apt cerca nei repository un pacchetto denominato headscale.deb e restituisce un errore.
Il pacchetto crea un utente di sistema headscale, scrive un file /etc/headscale/config.yaml predefinito e installa un'unità systemd. Non avvia il servizio, e questo è l'ordine corretto. La configurazione fornita indirizza server_url a http://127.0.0.1:8080, che non è un indirizzo raggiungibile da nessun client, quindi avviare ora il servizio sarebbe errato anche se si avviasse correttamente. Eseguendo sudo systemctl is-active headscale in questa fase viene visualizzato inactive. È un comportamento previsto, non un errore.
Configura server_url prima di avviare il servizio
Modifica /etc/headscale/config.yaml con sudo nano /etc/headscale/config.yaml oppure applica le stesse tre modifiche con sed. Conserva una copia dell'originale, perché il file è lungo e ampiamente commentato ed è il riferimento migliore per le altre impostazioni.
sudo cp /etc/headscale/config.yaml /etc/headscale/config.yaml.orig
sudo sed -i 's|^server_url:.*|server_url: https://headscale.example.com|' /etc/headscale/config.yaml
sudo sed -i 's|^listen_addr:.*|listen_addr: 127.0.0.1:8080|' /etc/headscale/config.yaml
sudo sed -i 's|^ base_domain:.*| base_domain: tailnet.example.net|' /etc/headscale/config.yaml
sudo grep -E '^(server_url|listen_addr):|^ base_domain:' /etc/headscale/config.yamlserver_url è l'indirizzo che headscale scrive in ogni registrazione client. Da quel momento in poi, i client si connettono esattamente a quella stringa. Deve quindi contenere il nome pubblico preceduto da https://, mai 127.0.0.1.
listen_addr indica dove il processo si mette in ascolto. Lascialo configurato sull'interfaccia di loopback. Un reverse proxy sullo stesso server termina TLS (Transport Layer Security) e inoltra le richieste al processo, quindi nessun sistema esterno deve poter raggiungere la porta 8080.
base_domain è il suffisso MagicDNS, cioè il dominio utilizzato per assegnare i nomi ai nodi. Deve essere un nome di dominio completo senza punto finale e deve essere diverso dal dominio indicato in server_url, altrimenti i due spazi dei nomi entrerebbero in conflitto.
Lascia invariata la sezione del database. Il valore predefinito è SQLite in /var/lib/headscale/db.sqlite, all'interno di una directory creata e gestita dal pacchetto, e SQLite è sufficiente per un tailnet di queste dimensioni.
Avviare headscale e verificare che sia in esecuzione
sudo systemctl enable --now headscale
sudo systemctl is-active headscale
curl -sS -o /dev/null -w '%{http_code}\\n' http://127.0.0.1:8080/healthis-active stampa active e curl stampa 200. enable --now esegue entrambe le operazioni: avvia il servizio e lo configura per l'avvio dopo un riavvio.
Se is-active stampa failed, leggere il journal con sudo journalctl -u headscale -n 50 --no-pager. In questa fase, il problema è quasi sempre il file di configurazione, perché headscale analizza l'intero file prima di aprire un socket. Un'indentazione errata o una chiave sconosciuta arresta quindi il processo prima che inizi ad ascoltare. Correggere il file, quindi eseguire sudo systemctl restart headscale. Ogni modifica successiva alla configurazione richiede lo stesso riavvio. In seguito, i client si riconnettono automaticamente. Se le unità systemd sono una novità, eseguire i propri servizi e timer con systemd illustra i comandi usati qui.
Verificare i file di stato mentre si è nella shell:
stat -c '%U %n' /var/lib/headscale/db.sqlite /var/lib/headscale/noise_private.keyEntrambe le righe iniziano con headscale, l'utente senza privilegi creato dal pacchetto. noise_private.key è l'identità del server per i client. Conservarlo. Se lo si elimina, headscale ne genera uno nuovo e ogni nodo deve registrarsi di nuovo.
Mettere TLS davanti a headscale
I client devono raggiungere server_url tramite HTTPS. Caddy è la soluzione più rapida, perché richiede e rinnova autonomamente il certificato.
sudo apt install -y caddySostituire /etc/caddy/Caddyfile con il blocco della documentazione di headscale:
headscale.example.com {
reverse_proxy 127.0.0.1:8080 {
header_up True-Client-IP {remote_host}
header_up X-Real-IP {remote_host}
}
}sudo caddy validate --adapter caddyfile --config /etc/caddy/Caddyfile
sudo systemctl restart caddy
sudo systemctl is-active caddyvalidate visualizza adapted config to JSON quando il file viene analizzato correttamente. Un avviso che indica che il file non è formattato è solo estetico. Dal laptop, anche curl -sS -o /dev/null -w '%{http_code}\\n' https://headscale.example.com/health dovrebbe visualizzare 200. Questo singolo controllo dimostra che DNS, firewall, certificato e proxy funzionano correttamente insieme.
Ecco il dettaglio del proxy che può richiedere molto tempo per essere diagnosticato. La connessione di controllo di Tailscale è un aggiornamento HTTP, viene avviata con POST anziché GET e il valore dell'intestazione Upgrade è tailscale-control-protocol. Caddy lo inoltra senza configurazione aggiuntiva. nginx non lo fa, quindi un front-end nginx richiede la seguente mappa di aggiornamento:
map $http_upgrade $connection_upgrade {
default keep-alive;
'' close;
}
server {
listen 443 ssl;
server_name headscale.example.com;
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_buffering off;
proxy_pass http://127.0.0.1:8080;
}
}Se si omettono queste righe, le richieste normali continuano ad avere esito positivo. Per questo /health restituisce 200 e tutto sembra funzionare, mentre la connessione di controllo a lunga durata non viene mai stabilita e i nodi si registrano per poi rimanere offline. Se si sceglie nginx, Certbot su Ubuntu 24.04 con nginx descrive la configurazione della parte relativa al certificato.
Quali porte aprire in UFW
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status verboseLa porta 443 trasporta tutte le comunicazioni dei client. La porta 80 serve solo per la sfida HTTP di ACME (ambiente per la gestione automatica dei certificati) e per il reindirizzamento a HTTPS. Caddy ne ha bisogno per ottenere un certificato.
La porta 8080 rimane chiusa. listen_addr è 127.0.0.1:8080, quindi il proxy raggiunge headscale tramite l'interfaccia di loopback e non è necessaria alcuna regola del firewall. L'apertura della porta 8080 su Internet espone ai client un canale di controllo in chiaro e non offre alcun vantaggio. Tenere presente che la maggior parte dei provider utilizza un secondo firewall nel pannello di controllo, separato da UFW. Di conseguenza, una porta può essere aperta sul server e rimanere chiusa sul perimetro della rete. Nozioni di base sul firewall UFW su un VPS illustra in maggiore dettaglio la sintassi delle regole.
Creare un utente e una chiave preauth
sudo headscale users create alice
sudo headscale users listIl comando headscale è un client. Comunica con il demone in esecuzione tramite il socket Unix in /var/run/headscale/headscale.sock, che ha modalità 0770 ed è di proprietà del gruppo headscale. Ne seguono due conseguenze. Il comando non funziona se il servizio è arrestato; questo è un altro motivo per cui l'ordine di questa guida è importante. Inoltre, richiede sudo, a meno che non aggiungiate il vostro account al gruppo headscale.
users list stampa un ID accanto a ogni nome. Vi serve quel numero, perché il comando per creare la chiave accetta un ID utente numerico, non un nome.
sudo headscale preauthkeys create --user 1 --expiration 24hLa chiave viene stampata una sola volta. Copiatela subito. Una chiave preauth è monouso e rimane valida per un'ora, salvo diversa indicazione; per questo conviene impostare --expiration 24h durante i test. Aggiungete --reusable per una chiave che registra più macchine e trattatela come una password, perché chiunque ne sia in possesso può unirsi alla vostra rete.
Connettere il primo client con --login-server
Sulla macchina che si vuole aggiungere:
curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up --login-server https://headscale.example.com --auth-key 'hskey-auth-PASTE-YOUR-KEY-HERE'
tailscale status
tailscale ip -4tailscale ip -4 stampa l'indirizzo assegnato da headscale, ad esempio 100.64.0.1. Tornando al server, sudo headscale nodes list mostra il nodo con il relativo ID, l'utente e lo stato online.
Il valore di --login-server deve corrispondere esattamente a server_url, incluso lo schema e senza slash finale. I valori vengono confrontati come stringhe. Una mancata corrispondenza fa registrare il client a un indirizzo e gli comunica poi di contattarne un altro.
Una macchina che aveva già eseguito l'accesso al servizio ospitato di Tailscale conserva tale accesso. Eseguire prima sudo tailscale logout, quindi eseguire tailscale up con --login-server.
Se si omette --auth-key, il client stampa invece un URL. Aprirlo per visualizzare l'identificatore del tentativo di registrazione, che deve essere approvato sul server:
sudo headscale auth register --user alice --auth-id PASTE-THE-ID-FROM-THE-PAGEQuesto modulo è più pratico per il proprio laptop. Le chiavi preautenticate sono preferibili per le operazioni automatizzate, perché non richiedono la presenza di una persona.
DERP e quale componente inoltra il traffico quando il percorso diretto non è disponibile
DERP (designated encrypted relay for packets) è il percorso di fallback. Quando due nodi non riescono ad aprire una connessione WireGuard diretta, in genere perché entrambi si trovano dietro NAT (network address translation) rigido, inviano invece i pacchetti tramite un relay. Il relay non possiede alcuna chiave, quindi non può leggere il traffico. Tuttavia, vede quali nodi comunicano e la quantità di dati trasferita.
È importante capire cosa fa la configurazione predefinita. Headscale viene distribuito con il riferimento a https://controlplane.tailscale.com/derpmap/default, auto_update_enabled: true e update_frequency: 3h, quindi il control plane è sotto il tuo controllo, mentre i relay appartengono a Tailscale. Per la maggior parte degli utenti è un compromesso ragionevole. In caso contrario, esegui un relay autonomo.
Per eseguire un relay autonomo, imposta enabled: true sotto derp.server in config.yaml, riavvia headscale e apri la porta STUN (session traversal utilities for NAT) con sudo ufw allow 3478/udp. Il file di configurazione indica chiaramente il requisito: server_url deve usare https, perché DERP richiede TLS. Svuotando l'elenco derp.urls rimuovi i relay di Tailscale dalla mappa. Se lo fai senza un relay incorporato funzionante, qualsiasi coppia di nodi che non riesce a connettersi direttamente non potrà connettersi affatto.
Da un client, tailscale netcheck stampa la latenza verso ogni area relay conosciuta e tailscale status contrassegna ogni peer come direct con un indirizzo oppure relay con un codice di area. Un peer che resta su relay indica un problema di NAT, non un problema di headscale.
Perché un nodo risulta offline?
Il proxy elimina l'upgrade. Questo è il caso più comune. Il segnale caratteristico è che tutto il resto funziona: /health restituisce 200, headscale nodes list mostra il nodo, ma il nodo non entra mai online. La connessione di controllo è una richiesta POST che trasporta Upgrade: tailscale-control-protocol. Un proxy che non la inoltra interrompe l'unico canale che comunica lo stato del nodo. Confronta la configurazione nginx con il blocco map precedente oppure passa a Caddy per escludere il proxy come causa.
server_url è cambiato dopo la registrazione dei nodi. I nodi continuano a connettersi al valore ricevuto durante la registrazione. Se lo hai modificato, esegui sudo tailscale up --login-server https://headscale.example.com --force-reauth su ogni nodo.
Il client non è in esecuzione. Sul nodo, esegui sudo systemctl is-active tailscaled e sudo journalctl -u tailscaled -n 50 --no-pager. Un client che non riesce a risolvere o raggiungere il tuo dominio registra lì i tentativi di riconnessione.
La chiave è scaduta. Questo caso è trattato nella sezione successiva.
Per monitorare il lato server durante il test, esegui sudo journalctl -u headscale -f sul VPS e riavvia tailscaled sul client. Un nodo che raggiunge headscale produce immediatamente righe nel log. Se non viene registrato nulla, la richiesta non sta arrivando. Controlla quindi DNS, firewall e proxy prima di controllare headscale.
Scadenza delle chiavi e nodo che smette di funzionare dopo alcune settimane
Esistono due scadenze separate. Confonderle fa perdere tempo.
Le chiavi di preautenticazione scadono rapidamente per impostazione predefinita. Il valore predefinito è un'ora e un solo utilizzo. Se tailscale up rifiuta la chiave, generane una nuova sul server invece di modificare qualcosa sul client.
Le chiavi dei nodi hanno una durata maggiore. La sezione node di config.yaml imposta expiry: 0, mentre 0 indica che non esiste alcuna scadenza predefinita: un nodo registrato rimane valido finché non lo fai scadere. I nodi con tag non scadono mai. Imposta expiry: 180d se vuoi che le registrazioni scadano automaticamente e considera le conseguenze: ogni nodo senza tag dovrà quindi eseguire sudo tailscale up --login-server https://headscale.example.com --force-reauth secondo quella pianificazione, mentre un server headless al quale nessuno effettua nuovamente l'autenticazione verrà rimosso autonomamente dalla rete.
Esegui questa operazione manualmente quando qualcuno perde un laptop. sudo headscale nodes list restituisce l'ID, sudo headscale nodes expire -i 3 disconnette quel nodo e sudo headscale nodes delete -i 3 lo rimuove completamente dalla rete.
Backup e aggiornamenti
/var/lib/headscale e /etc/headscale costituiscono l'intero server. Arresta il servizio prima di copiarli, perché SQLite potrebbe avere operazioni di scrittura in corso e un database copiato sotto carico potrebbe risultare incoerente.
sudo systemctl stop headscale
sudo tar czf /root/headscale-state.tgz -C /var/lib headscale
sudo tar czf /root/headscale-config.tgz -C /etc headscale
sudo systemctl start headscale
sudo chmod 600 /root/headscale-*.tgzSposta entrambi i file fuori dal server. Contengono le chiavi private e tutte le registrazioni, quindi richiedono la stessa cura del server stesso. backup restic da un VPS spiega come eseguire questa operazione secondo una pianificazione e con la crittografia.
Gli aggiornamenti ripetono la procedura di installazione: scarica il nuovo .deb e sudo apt install ./headscale.deb, quindi riavvia il servizio e ripeti i controlli is-active e /health. Dalla versione 0.29, il percorso di aggiornamento è vincolante. Il salto di una versione minor è bloccato, così come il downgrade a una versione minor precedente. Passa a una versione minor alla volta, esegui un backup prima di ogni passaggio e leggi prima le note di rilascio di quella versione, perché lo stesso rilascio ha modificato il comportamento dei criteri ACL e spostato diverse chiavi di configurazione.
FAQ
Perché headscale non riesce ad avviarsi subito dopo l'installazione del file .deb?
Il pacchetto installa l'unità, ma lascia il servizio arrestato. Inoltre, il valore predefinito di /etc/headscale/config.yaml è un modello e non una configurazione funzionante. Modifica prima server_url, listen_addr e base_domain, quindi esegui sudo systemctl enable --now headscale e verifica con sudo systemctl is-active headscale. Se il problema persiste, sudo journalctl -u headscale -n 50 --no-pager identifica l'errore. In questa fase, la causa è quasi sempre un errore YAML, perché headscale analizza l'intero file prima di mettersi in ascolto su una porta.
Devo comunque installare il normale client Tailscale sui miei computer?
Sì. Headscale sostituisce solo il server di controllo. Ogni nodo esegue il client ufficiale di Tailscale e lo indirizzi al tuo server con sudo tailscale up --login-server https://headscale.example.com. Questo flag è disponibile nel client standard, quindi non è necessario applicare patch o ricompilare il software.
Il mio traffico passa attraverso il server headscale?
Di solito no. Headscale coordina la rete e distribuisce chiavi e indirizzi, mentre il percorso dei dati usa WireGuard direttamente tra i nodi. Il traffico passa da un relay DERP solo quando due nodi non riescono a comunicare direttamente. Con la configurazione fornita, questi relay sono quelli pubblici di Tailscale. Esegui tailscale status su un nodo per verificare se un determinato peer è direct o si trova su un relay.
Perché il mio nodo rimane offline dopo la registrazione?
Un nodo che compare in headscale nodes list, ma non passa mai online, ha solitamente perso la connessione di controllo sul reverse proxy. Questa connessione è un aggiornamento HTTP inviato con una richiesta POST e l'header Upgrade: tailscale-control-protocol. nginx la interrompe se non aggiungi il blocco map $http_upgrade $connection_upgrade e le righe proxy_set_header corrispondenti. Caddy la inoltra senza configurazione aggiuntiva. Per questo è un modo rapido per verificare se la causa è il proxy.
Ho bisogno di un nome di dominio e di TLS per headscale?
In pratica, sì. I client si connettono alla stringa specificata in server_url. I certificati vengono emessi per i nomi e non per gli indirizzi IP senza nome di dominio. Inoltre, il file di configurazione specifica che DERP richiede TLS. Un dominio abbinato a Caddy richiede circa cinque minuti e fornisce un endpoint HTTPS che rinnova automaticamente il certificato. Se esegui il server di controllo tramite HTTP semplice, ogni comunicazione tra i client e il server attraversa Internet senza cifratura.