Headscale: il tuo server Tailscale su un VPS
Installa headscale dal pacchetto ufficiale .deb, imposta server_url prima dell'avvio e collega il primo nodo a una tailnet gestita da te.
Cos'è headscale
Headscale è un'implementazione self-hosted del server di controllo Tailscale. Il server 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, indirizzato al tuo server tramite un'unica opzione, --login-server.
Il server di controllo determina quali dispositivi appartengono alla rete. Assegna a ogni nodo un indirizzo nell'intervallo 100.64.0.0/10, distribuisce le chiavi pubbliche e indica ai nodi come trovarsi tra loro. I tunnel restano tunnel WireGuard, creati direttamente tra i nodi. Il traffico tra due macchine di tua proprietà non passa attraverso il server headscale, a meno che non sia possibile creare un percorso diretto e i nodi debbano usare un relay. Gestire direttamente questo ruolo di coordinamento cambia chi lo controlla, non le sue funzionalità. Prima di considerare la migrazione un miglioramento della sicurezza, conviene quindi capire a quali risorse può e non può accedere un server di controllo in questo modello.
Ogni istanza di headscale gestisce una sola tailnet, cioè una sola 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 componenti che possono causare problemi. Headscale diventa utile quando non vuoi più scrivere manualmente un blocco [Peer] per ogni nuovo laptop. Spesso il costo spinge a valutare questa soluzione, quindi conviene leggere cosa include realmente il piano gratuito ospitato prima di gestire un server: pochi dispositivi personali rientrano normalmente nei suoi limiti. Se hai già superato tale limite, confronta il costo con il prezzo dei piani a pagamento, calcolato per utente e non per dispositivo, perché un nucleo familiare che usa un solo account può mantenere costi contenuti anche quando il numero di dispositivi aumenta. Se vuoi un piano di controllo self-hosted, ma preferisci usare un client proprietario e un'interfaccia web per gestire i peer invece di un sostituto diretto di Tailscale, NetBird su un singolo VPS è l'alternativa da valutare. Per un confronto più ampio tra i due modelli, consulta le differenze tra WireGuard e Tailscale.
Prerequisiti per l'installazione
- Un VPS con Ubuntu 24.04, un indirizzo IPv4 pubblico e accesso sudo. Se il server è nuovo, completa 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 coincidere con il dominio indicato inserver_url. - Un computer client da aggiungere alla rete, 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-architectureSu una VPS x86 standard, il comando stampa amd64; su un piano di tipo Ampere o Graviton stampa arm64. 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 chiamato headscale.deb e termina con un errore.
Il pacchetto crea un utente di sistema headscale, scrive una configurazione predefinita /etc/headscale/config.yaml 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 il processo partisse. 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 contiene molti commenti, quindi resta 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 inserisce nella registrazione di ogni client. Da quel momento i client si connettono sempre esattamente a quella stringa, quindi deve essere il nome pubblico con https:// davanti, mai 127.0.0.1.
listen_addr indica l'indirizzo su cui il processo si mette in ascolto. Lascialo sull'interfaccia di loopback. Un reverse proxy sullo stesso server termina TLS (transport layer security) e inoltra le richieste al servizio, quindi dall'esterno del server non è necessario raggiungere la porta 8080.
base_domain è il suffisso MagicDNS, cioè il dominio sotto cui i nodi ricevono i nomi. Deve essere un fully qualified domain name senza punto finale e deve essere diverso dal dominio indicato in server_url, perché altrimenti i due spazi dei nomi entrerebbero in conflitto.
Non modificare la sezione del database. Il valore predefinito è SQLite in /var/lib/headscale/db.sqlite, in una directory creata e gestita dal pacchetto, e SQLite è sufficiente per una 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, consultare il journal con sudo journalctl -u headscale -n 50 --no-pager. In questa fase, il problema è quasi sempre nel 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 accettare connessioni. Correggere il file, quindi eseguire sudo systemctl restart headscale. Ogni modifica successiva alla configurazione richiede lo stesso riavvio. I client si riconnettono automaticamente. Se le unità systemd sono una novità, gestire servizi e timer propri con systemd descrive i comandi usati in questa procedura.
Controllare 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. Non eliminarlo. Se lo si elimina, headscale ne genera uno nuovo e ogni nodo deve eseguire nuovamente la registrazione.
Configurare TLS davanti a headscale
I client devono raggiungere server_url tramite HTTPS. Caddy è la soluzione più semplice, perché richiede e rinnova autonomamente il certificato.
sudo apt install -y caddySostituisci /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 segnala la mancata formattazione del file è 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 spesso richiede ore di troubleshooting. La connessione di controllo di Tailscale usa un aggiornamento HTTP, viene avviata con una richiesta POST anziché GET e il valore dell'header Upgrade è tailscale-control-protocol. Caddy inoltra questa richiesta senza configurazioni aggiuntive. nginx non lo fa, quindi un front end nginx richiede la seguente mappa per l'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 ometti queste righe, le richieste ordinarie continuano ad avere esito positivo. Per questo /health restituisce 200 e sembra che tutto funzioni, mentre la connessione di controllo di lunga durata non viene mai stabilita e i nodi si registrano, quindi restano offline. Se scegli nginx, Certbot su Ubuntu 24.04 con nginx descrive la 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 soltanto per la challenge HTTP di ACME (ambiente per la gestione automatica dei certificati) e per il redirect a HTTPS; inoltre, 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. Aprire la porta 8080 a Internet espone ai client un canale di controllo in chiaro e non offre alcun vantaggio. Considerare che la maggior parte dei provider gestisce un secondo firewall nel pannello di controllo, separato da UFW; una porta può quindi essere aperta sul server e rimanere chiusa sul perimetro. Nozioni di base sul firewall UFW su un VPS illustra in maggiore dettaglio la sintassi delle regole.
Crea un utente e una chiave di preautenticazione
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 conseguono due aspetti. Il comando non funziona quando il servizio è arrestato; questo è l'altro motivo per cui l'ordine indicato in 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 questo 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 di preautenticazione è monouso e resta 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 la possieda può unirsi alla vostra rete.
Connettere il primo client con --login-server
Sulla macchina che si desidera 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 sul 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 differenza fa sì che il client si registri a un indirizzo e venga poi istruito a comunicare con un altro.
Una macchina che ha eseguito in precedenza l'accesso al servizio hosted di Tailscale conserva quella sessione. Eseguire prima sudo tailscale logout, quindi eseguire tailscale up con --login-server.
Se si omette --auth-key, il client stampa un URL. Aprirlo per visualizzare nella pagina l'identificatore del tentativo di registrazione, quindi approvarlo sul server:
sudo headscale auth register --user alice --auth-id PASTE-THE-ID-FROM-THE-PAGEQuesto metodo è più pratico sul proprio laptop. Le chiavi di preautenticazione sono preferibili per le procedure automatizzate, perché non richiedono la presenza di un operatore. Dopo che il VPS è diventato un nodo, può anche trasportare il traffico Internet delle altre macchine, tramite la configurazione del nodo di uscita. L'unica differenza è che la route pubblicizzata viene approvata sul server con il comando headscale, anziché in una console di amministrazione hosted. Se si desidera raggiungere una rete privata che si trova dietro il VPS, invece di utilizzare il VPS come accesso a Internet, lo stesso passaggio di approvazione consente di pubblicizzare quella subnet al resto del tailnet. Pubblicare una singola applicazione da un nodo, invece di instradare intere reti attraverso di esso, è un'attività diversa. serve e funnel sono i due metodi disponibili, ma entrambi utilizzano i certificati e il meccanismo di ingresso propri di Tailscale. Vanno quindi considerati funzionalità del tailnet hosted, non strumenti forniti da headscale.
DERP e il sistema che inoltra il traffico quando il percorso diretto non è disponibile
DERP (designated encrypted relay for packets) è il percorso di fallback. Quando due nodi non riescono a stabilire una connessione WireGuard diretta, in genere perché entrambi si trovano dietro NAT (network address translation) restrittivi, inviano i pacchetti tramite un relay. Il relay non dispone di chiavi, quindi non può leggere il traffico. Può però vedere quali nodi comunicano e la quantità di dati trasferita.
È importante capire cosa fa la configurazione predefinita. Headscale viene distribuito con 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 in 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 embedded funzionante, ogni coppia di nodi che non riesce a connettersi direttamente non potrà connettersi affatto.
Da un client, tailscale netcheck mostra la latenza verso ogni regione relay che conosce, mentre tailscale status contrassegna ogni peer come direct con un indirizzo oppure relay con un codice di regione. Un peer bloccato su relay indica un problema NAT, non un problema di headscale. Un peer che è direct ma rimane lento è un caso diverso; in genere la causa è l’MTU, non il tunnel in sé.
Perché un nodo risulta offline?
Il proxy elimina l’upgrade. È il caso più comune. Il segnale caratteristico è che tutto il resto sembra funzionare: /health restituisce 200, headscale nodes list mostra il nodo, ma il nodo non entra mai online. La connessione di controllo è una richiesta POST che contiene Upgrade: tailscale-control-protocol. Un proxy che non inoltra questa richiesta interrompe l’unico canale che comunica lo stato del nodo. Confronta la configurazione di 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. Se il client non riesce a risolvere o raggiungere il tuo dominio, registra lì i tentativi di riconnessione.
La chiave è scaduta. L’argomento è 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 genera immediatamente righe nei log. Se non compare nulla, la richiesta non sta arrivando. Controlla DNS, firewall e proxy prima di controllare headscale.
Scadenza delle chiavi e nodo che smette di funzionare dopo settimane
Esistono due scadenze separate. Confonderle fa perdere tempo.
Le chiavi di preautenticazione scadono rapidamente per impostazione predefinita. La durata predefinita è di un'ora e una sola utilizzazione. Se tailscale up rifiuta la chiave, generane una nuova sul server invece di modificare qualcosa sul client.
Le chiavi dei nodi sono l'elemento a lunga durata. La sezione node di config.yaml definisce expiry: 0, mentre 0 significa che non esiste alcuna scadenza predefinita: un nodo registrato resta valido finché non lo fai scadere. I nodi con tag non scadono mai, indipendentemente da questa impostazione. Imposta expiry: 180d se vuoi che le registrazioni scadano dopo un certo periodo 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 senza interfaccia grafica a cui nessuno esegue nuovamente l'autenticazione verrà rimosso dalla rete autonomamente.
Esegui questa operazione manualmente quando qualcuno perde un laptop. sudo headscale nodes list mostra l'ID, quindi 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 può 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-*.tgzTrasferisci entrambi i file fuori dal server. Contengono le chiavi private e tutte le registrazioni, quindi richiedono la stessa attenzione 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 ed esegui nuovamente i controlli is-active e /health. A partire dalla versione 0.29, il percorso di aggiornamento è rigoroso. Non è possibile saltare una versione minor e non è possibile effettuare il downgrade a una versione minor precedente. Aggiorna una versione minor alla volta, esegui un backup prima di ogni passaggio e leggi prima le note di rilascio della versione, perché quel rilascio ha modificato il comportamento dei criteri ACL e spostato diverse chiavi di configurazione.
FAQ
Perché headscale non si avvia subito dopo l'installazione del file .deb?
Il pacchetto installa l'unità, ma lascia il servizio arrestato, e il valore predefinito /etc/headscale/config.yaml è un modello, 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 indica la causa. A questo punto si tratta quasi sempre di 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 soltanto 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. Quel 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 devia soltanto quando due nodi non riescono a raggiungersi direttamente e usano un relay DERP. 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 resta offline dopo la registrazione?
Un nodo che compare in headscale nodes list ma non diventa mai online ha in genere perso la connessione di controllo tramite il reverse proxy. Questa connessione è un upgrade 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, quindi consente di verificare rapidamente se la causa è il proxy.
Mi servono un nome di dominio e 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 privi di nome, inoltre il file di configurazione specifica che DERP richiede TLS. Un dominio insieme a Caddy richiede circa cinque minuti di configurazione e fornisce un endpoint HTTPS con rinnovo automatico. Eseguire il server di controllo su HTTP non cifrato significa che ogni comunicazione tra i client e il server attraversa Internet in chiaro.