Configurare un server VPN WireGuard sul proprio server

Pubblicato il 16 min di lettura

Dal server vuoto al tunnel WireGuard funzionante: coppie di chiavi, NAT con nftables, wg-quick come servizio, QR code per il telefono e i tre problemi su cui ci si blocca davvero.

Che cosa è uguale su tutte e quattro le distribuzioni, e che cosa no

WireGuard fa stabilmente parte del kernel Linux dalla versione 5.6. Su Debian 13, Debian 12, Ubuntu 24.04 e Ubuntu 22.04 non devi quindi più compilare un modulo DKMS, non devi aggiungere un repository di backport e non devi importare alcuna chiave esterna. Tutte e quattro forniscono inoltre lo stesso livello degli strumenti userland (versione upstream 1.0.20210914), il che significa: i comandi wg e wg-quick si comportano ovunque allo stesso modo.

Le differenze stanno tutte nel contorno, ed è esattamente lì che la maggior parte delle guide fallisce:

  • Filtro dei pacchetti: wireguard-tools raccomanda nftables oppure iptables. Poiché apt prende la prima alternativa disponibile, su un'installazione Debian minimale finisce nftables, non iptables. Le righe iptables -t nat -A POSTROUTING copiate un po' ovunque non producono alcun effetto finché non installi il pacchetto.
  • Passaggio del DNS: per la riga DNS = wg-quick richiama ostinatamente il programma resolvconf. Su Ubuntu il pacchetto systemd-resolved porta con sé uno strato di compatibilità, mentre su un'installazione Debian minimale spesso resolvconf non esiste affatto. Per installarlo, il pacchetto giusto su Debian e su Ubuntu 22.04 si chiama openresolv, mentre su Ubuntu 24.04 questo pacchetto non esiste più. Riguarda soltanto i client Linux, non i telefoni.
  • Frontend del firewall: le immagini Ubuntu portano spesso un ufw già attivo, Debian di norma no. ufw blocca l'inoltro per impostazione predefinita, anche quando net.ipv4.ip_forward è impostato a 1.

Controlla per prima cosa che cosa hai davanti:

apt-get update
apt-get install -y wireguard wireguard-tools nftables qrencode
wg --version
apt-cache policy wireguard-tools

Se wg --version restituisce un numero di versione, lo userland c'è. Se il kernel collabora lo scopri invece solo al primo wg-quick up. Se lì compare il messaggio RTNETLINK answers: Operation not supported oppure Unable to access interface: Protocol not supported, stai usando un kernel senza supporto WireGuard, tipicamente uno molto vecchio o un kernel di container fortemente ridotto.

Generare le coppie di chiavi senza crearsi problemi

WireGuard non conosce nomi utente né certificati. Per ogni partecipante esiste esattamente una coppia di chiavi, più eventualmente una preshared key condivisa come ulteriore livello simmetrico. Genera entrambe con la umask impostata, altrimenti le chiavi private restano leggibili da chiunque:

umask 077
wg genkey | tee /etc/wireguard/server.key | wg pubkey > /etc/wireguard/server.pub
wg genkey | tee /etc/wireguard/telefono.key | wg pubkey > /etc/wireguard/telefono.pub
wg genpsk > /etc/wireguard/telefono.psk
ls -l /etc/wireguard

La directory /etc/wireguard non devi crearla tu, il pacchetto wireguard-tools la porta già con i permessi 0700. Più importante è una particolarità di umask: il valore vale solo nella sessione di shell in cui lo imposti. Se generi le chiavi in due riprese, per esempio perché nel frattempo la connessione è caduta e hai eseguito un nuovo accesso, da quel momento lavori di nuovo con la maschera predefinita, e server.key e telefono.psk finiscono sul disco con 0644. Imposta quindi i permessi ancora una volta, in modo esplicito, alla fine:

chmod 600 /etc/wireguard/*.key /etc/wireguard/*.psk

Qui si ripetono sempre tre errori:

  1. Chiave pubblica e chiave privata scambiate. Entrambe sono stringhe Base64 lunghe 44 caratteri e hanno lo stesso aspetto. Se in [Interface] PrivateKey finisce per errore una chiave pubblica, il tunnel parte comunque, ma non si arriva mai a un handshake. La verifica è possibile in qualsiasi momento: wg pubkey < /etc/wireguard/server.key deve restituire esattamente il contenuto di server.pub.
  2. Ritorno a capo copiato insieme alla chiave. Le chiavi copiate dal terminale con il mouse si portano dietro volentieri degli spazi. wg lo segnala con Key is not the correct length or format.
  3. Permessi dei file. Se dimentichi umask 077, all'avvio wg-quick avvisa con Warning: `/etc/wireguard/wg0.conf' is world accessible. Non è un dettaglio estetico: in quel file la chiave privata sta in chiaro.

La configurazione del server

Individua per prima cosa il nome della tua interfaccia verso internet. Sulle macchine virtuali si chiama eth0, ens3 oppure enp1s0, a seconda dell'immagine:

ip route show default

Crea poi /etc/wireguard/wg0.conf. La rete del tunnel dovrebbe essere una che in giro non ti capita di incontrare nel Wi-Fi di un hotel, quindi meglio evitare 192.168.0.0/24 o 192.168.1.0/24:

[Interface]
Address = 10.8.0.1/24
ListenPort = 51820
PrivateKey = CONTENUTO_DI_SERVER_KEY

PostUp = nft add table ip wgnat
PostUp = nft add chain ip wgnat postrouting '{ type nat hook postrouting priority srcnat; policy accept; }'
PostUp = nft add rule ip wgnat postrouting ip saddr 10.8.0.0/24 oifname "eth0" masquerade
PostDown = nft delete table ip wgnat

[Peer]
# telefono
PublicKey = CONTENUTO_DI_TELEFONO_PUB
PresharedKey = CONTENUTO_DI_TELEFONO_PSK
AllowedIPs = 10.8.0.2/32

Sostituisci eth0 con la tua interfaccia reale. Se preferisci restare su iptables, installa iptables e usa invece queste righe:

PostUp = iptables -t nat -A POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE
PostDown = iptables -t nat -D POSTROUTING -s 10.8.0.0/24 -o eth0 -j MASQUERADE

Due dettagli che vengono citati raramente. Primo: ogni riga PostUp viene eseguita tramite una shell, e se una di queste si interrompe con un errore, wg-quick annulla l'intero avvio. Un errore di battitura nella regola nft non porta quindi a un tunnel che funziona a metà, ma a nessun tunnel. Secondo: AllowedIPs sul lato server ha un significato completamente diverso da quello che ha sul lato client. Qui è una tabella di assegnazione che stabilisce quale indirizzo mittente appartiene a quale peer. Se inserisci lo stesso indirizzo per due peer, vince quello caricato per ultimo e l'altro resta muto. Ogni client riceve esattamente una /32.

Imposta i permessi e verifica la sintassi prima di avviare:

chmod 600 /etc/wireguard/wg0.conf
wg-quick strip wg0

wg-quick strip stampa la configurazione senza le righe proprie di wg-quick. Se il comando arriva in fondo, il file è formalmente a posto. Se invece segnala wg-quick: Line unrecognized, di solito hai scritto un'opzione nella sezione sbagliata, per esempio DNS sotto [Peer].

Attivare l'inoltro IP in modo permanente

Senza inoltro ogni pacchetto si ferma sul server. Lo attivi subito e in modo permanente:

echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/99-wireguard.conf
sysctl --system
sysctl net.ipv4.ip_forward

L'ultimo comando deve restituire net.ipv4.ip_forward = 1. Se vuoi far passare anche IPv6 nel tunnel, nello stesso file va aggiunta la riga net.ipv6.conf.all.forwarding=1.

Se ufw è attivo, tutto questo non basta ancora. ufw imposta una propria policy di inoltro che vale indipendentemente dall'interruttore del kernel. Apri la porta e consenti il passaggio in modo esplicito:

ufw allow 51820/udp
ufw route allow in on wg0 out on eth0

Il quadro tipico quando ci si dimentica dell'inoltro è particolarmente insidioso: l'handshake riesce, il ping verso 10.8.0.1 riesce, ma niente di quello che sta dietro è raggiungibile. Chi guarda solo l'handshake finisce per cercare per ore nel posto sbagliato.

Configurare wg-quick come servizio

Il pacchetto porta con sé una unit template: il nome dopo la @ è il nome del file di configurazione senza estensione:

systemctl enable wg-quick@wg0
systemctl start wg-quick@wg0
systemctl status wg-quick@wg0
journalctl -u wg-quick@wg0 -n 50 --no-pager

Una trappola che molti notano solo dopo settimane: SaveConfig = true. Questa opzione, quando il servizio viene fermato, riscrive nel file lo stato corrente. Nel farlo vanno persi tutti i commenti, tutte le righe PostUp nel loro ordine originale e ogni struttura inserita a mano. Per un server la cui configurazione curi a mano, lascia perdere questa opzione.

Dopo l'avvio non valutare lo stato dal codice di uscita, ma dall'interfaccia:

wg show
ip -brief address show wg0
ss -ulpn

ss -ulpn deve mostrare un processo in ascolto sulla porta UDP 51820. wg show elenca i peer, a questo punto ancora senza handshake.

Configurazione del client e QR code per il telefono

Conviene creare il file del client direttamente sul server, visto che lì si trovano già tutte le chiavi. Attenzione: sul lato client AllowedIPs significa un'altra cosa, cioè quali destinazioni devono passare per il tunnel. 0.0.0.0/0, ::/0 vuol dire: tutto.

mkdir -p /etc/wireguard/clients

Contenuto di /etc/wireguard/clients/telefono.conf:

[Interface]
PrivateKey = CONTENUTO_DI_TELEFONO_KEY
Address = 10.8.0.2/32
DNS = 9.9.9.9, 149.112.112.112

[Peer]
PublicKey = CONTENUTO_DI_SERVER_PUB
PresharedKey = CONTENUTO_DI_TELEFONO_PSK
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = indirizzo.del.tuo.server:51820
PersistentKeepalive = 25

PersistentKeepalive = 25 con i telefoni non è un lusso. Il NAT delle reti mobili dimentica spesso le associazioni UDP dopo 30 o 60 secondi, e senza keepalive il server non riesce più a contattare di propria iniziativa una connessione già esistente.

Il QR code lo generi direttamente nel terminale:

qrencode -t ansiutf8 < /etc/wireguard/clients/telefono.conf

Nell'app WireGuard tocca il più, scegli l'importazione tramite QR code e punta la fotocamera sul terminale. Due indicazioni pratiche: riduci la dimensione del carattere nel terminale prima di generare il codice, altrimenti non entra nell'inquadratura. E non cancellare subito il file dopo l'importazione, ti servirà di nuovo quando cambi dispositivo. Chi lo cancella comunque deve generare una nuova coppia di chiavi, perché la chiave privata non si può ricavare a ritroso da quella pubblica.

Da che cosa capisci che funziona davvero

Il fatto che systemctl start ritorni senza output significa soltanto che l'interfaccia esiste. La verifica vera e propria è fatta di tre livelli, e conviene percorrerli in quest'ordine:

wg show wg0 latest-handshakes
wg show wg0 transfer

Livello uno, l'handshake. latest-handshakes restituisce un timestamp Unix per ogni peer. Se lì c'è 0, non si è mai stabilita nessuna connessione. Nella vista estesa di wg show leggi invece latest handshake: 42 seconds ago.

Livello due, il flusso di dati. Sotto transfer trovi i byte ricevuti e quelli inviati. Solo byte inviati e nessuno ricevuto significa: i tuoi pacchetti escono, ma non torna indietro niente. È quasi sempre un firewall o un endpoint sbagliato, mai un problema di chiavi.

Livello tre, il percorso reale. Sul client verifica se il traffico viene davvero instradato nel tunnel:

ip route get 1.1.1.1

Se lì compare dev wg0, il routing è corretto. Solo a questo punto ha senso dare un'occhiata a un sito che mostra il tuo indirizzo IP pubblico. Se mostra l'indirizzo del tuo server, hai finito.

Diagnosi: nessun handshake

Il quadro di errore più frequente in assoluto. Attiva per prima cosa il logging del modulo del kernel, di solito dà la risposta in dieci secondi:

echo 'module wireguard +p' > /sys/kernel/debug/dynamic_debug/control
dmesg -w

Ora avvia dal client un tentativo di connessione e leggi in diretta. I tre messaggi che contano:

  • wireguard: wg0: Handshake for peer 1 (...) did not complete after 5 seconds, retrying (try 2) e nient'altro. Al server non arriva nemmeno un pacchetto. Fai la controprova con tcpdump -n -i eth0 udp port 51820. Se lì non vedi niente, la causa è il firewall davanti al server, la porta, oppure il fatto che il client si trova in una rete che filtra l'UDP in uscita. Un test dalla rete mobile invece che dal Wi-Fi aziendale separa questi casi in modo pulito.
  • wireguard: wg0: Invalid handshake initiation from .... I pacchetti arrivano, ma non corrispondono. Quasi sempre la chiave pubblica del server nel file del client è sbagliata, oppure la preshared key è inserita solo da un lato. Una preshared key deve essere identica su entrambi i lati oppure mancare su entrambi.
  • Nessun messaggio, anche se tcpdump mostra dei pacchetti. In quel caso WireGuard è in ascolto su un'altra porta o su un altro indirizzo. Controllo con ss -ulpn.

Un caso particolare raramente documentato riguarda l'orologio. WireGuard inserisce un timestamp nel primo messaggio di handshake e il server memorizza per ogni peer il valore più alto mai visto. I timestamp più vecchi vengono scartati: è la protezione contro il replay. Se un dispositivo ha l'orologio molto avanti nel futuro e si è collegato una volta, dopo la correzione dell'ora non passa più. Il server tiene questo stato in memoria, quindi la soluzione è: sistemare l'orologio e poi eseguire sul server wg-quick down wg0 e wg-quick up wg0. Dopo la ricostruzione il blocco sparisce.

Disattiva poi di nuovo il logging, perché è piuttosto chiacchierone:

echo 'module wireguard -p' > /sys/kernel/debug/dynamic_debug/control

Diagnosi: il DNS non funziona

Sintomo: il tunnel è su, ping 1.1.1.1 funziona, ma nessun nome viene risolto. Le cause sono esattamente tre.

Primo: il resolver non è raggiungibile. Se inserisci DNS = 10.8.0.1, sul server deve esserci davvero un name server in ascolto su quell'indirizzo. Un Debian o un Ubuntu appena installati non ne hanno nessuno. O inserisci un resolver pubblico che viene raggiunto attraverso il tunnel, oppure ne configuri uno tu. Per la seconda strada basta dnsmasq con un piccolo file sotto /etc/dnsmasq.d/wireguard.conf:

interface=wg0
bind-dynamic
no-resolv
server=9.9.9.9
server=1.1.1.1
cache-size=1000

bind-dynamic è importante perché all'avvio di dnsmasq wg0 potrebbe non esistere ancora. no-resolv su Ubuntu è obbligatorio: senza questa riga dnsmasq legge /etc/resolv.conf, vi trova l'indirizzo stub 127.0.0.53 di systemd-resolved e costruisce un anello. E il punto che su Ubuntu morde più spesso: dnsmasq occupa la porta 53. Su un sistema con systemd-resolved attivo quella porta è già assegnata, i due servizi vanno in conflitto e dnsmasq non parte. Le righe interface=wg0 e bind-dynamic non sono quindi un dettaglio estetico, ma impediscono che dnsmasq si leghi a tutti gli indirizzi. Verifica poi con ss -ulpn che dnsmasq e systemd-resolved non si contendano la porta 53.

Secondo: il resolver sta fuori dagli AllowedIPs. Chi invece di 0.0.0.0/0 fa passare nel tunnel solo singole reti e come server DNS inserisce un indirizzo che non compare in quell'elenco, manda le richieste fuori dal tunnel, nella rete locale.

Terzo, solo sui client Linux: manca resolvconf. La riga DNS = viene passata da wg-quick a un programma chiamato resolvconf. Se manca, l'avvio si interrompe con /usr/bin/wg-quick: line 32: resolvconf: command not found. Controlla prima di tutto se il programma è presente:

command -v resolvconf

Al momento di installarlo le distribuzioni si dividono, ed è proprio qui che le guide copiate falliscono su Ubuntu 24.04. Lì il pacchetto openresolv non esiste né in main né in universe, e il comando termina con E: Package 'openresolv' has no installation candidate:

SistemaPacchetto corretto
Debian 11, Debian 12, Debian 13apt-get install -y openresolv
Ubuntu 22.04apt-get install -y openresolv
Ubuntu 24.04apt-get install -y resolvconf

Su Ubuntu 24.04 resolvconf è un pacchetto virtuale che si risolve in modo univoco in systemd-resolved e che nel farlo crea /usr/sbin/resolvconf, cioè esattamente il binario che wg-quick richiama per la riga DNS. Lì puoi altrettanto bene installare direttamente systemd-resolved. Se vuoi una riga sola che funzioni su tutti i sistemi citati, usa questa:

apt-get install -y openresolv || apt-get install -y resolvconf

Su Ubuntu 24.04 esiste una variante di questo problema che compare dopo un aggiornamento dalla 22.04: Failed to resolve interface "tun.wg0": No such device. La causa è un vecchio pacchetto resolvconf rimasto dalla 22.04 insieme a /etc/resolvconf/interface-order, quindi non il pacchetto virtuale con lo stesso nome della 24.04. Sulla base di quel file wg-quick antepone al nome dell'interfaccia un tun., e lo strato di compatibilità di systemd-resolved non sa che farsene. La soluzione è rimuovere il vecchio pacchetto, in modo che resti solo lo strato di compatibilità di systemd-resolved.

Diagnosi: problemi di MTU

Il quadro di errore più sgradevole, perché sembra funzionare tutto. L'handshake regge, il ping va, SSH va, ma le pagine web si caricano a metà e restano bloccate, i download grandi si interrompono e proprio HTTPS è quello colpito. Il motivo: i pacchetti piccoli passano, quelli grandi no.

WireGuard aggiunge attorno a ogni pacchetto 60 byte quando il tunnel passa su IPv4 (20 byte IP, 8 byte UDP, 32 byte WireGuard) e 80 byte su IPv6. Per questo wg-quick sottrae forfettariamente 80 byte dalla path MTU rilevata e su un percorso normale da 1500 arriva a 1420. È una scelta volutamente conservativa e nella maggior parte dei casi è corretta.

Non è corretta quando il percorso è più stretto di 1500, per esempio con DSL e PPPoE (1492), dietro a un ulteriore tunnel oppure su alcune reti mobili. Misura la path MTU reale dal client verso l'indirizzo pubblico del server, con il bit Don't Fragment impostato e senza tunnel:

ping -M do -s 1472 -c 3 INDIRIZZO_DI_DESTINAZIONE

1472 più 28 byte di intestazione fanno 1500. Se torna ping: local error: message too long, mtu=... oppure Frag needed and DF set, abbassa il valore un passo alla volta finché non passa: 1464, 1444, 1414, 1372. Al valore trovato aggiungi 28 e sottrai 80. Con 1464 si arriva quindi a 1492 di path MTU e 1412 di MTU del tunnel.

Il valore si inserisce nella sezione [Interface], sul lato che ha il problema:

MTU = 1412

La controprova rapida, prima di metterti a fare conti: imposta per prova MTU = 1280. È la MTU più piccola garantita da IPv6 e funziona praticamente ovunque. Se con quel valore le pagine si caricano correttamente, il problema era la MTU e puoi avvicinarti con calma al valore ottimale. Se il problema resta, la causa è altrove.

Sul server stesso una MTU sbagliata è più rara, ma possibile: se lì è impostato un valore più alto di quanto il percorso consenta, l'effetto si vede solo verso determinate destinazioni. Un'occhiata a ip -brief address show wg0 e ip link show wg0 mostra il valore attualmente impostato.

Modificare i peer durante l'esercizio senza buttare fuori tutti

Il riflesso di digitare systemctl restart wg-quick@wg0 dopo ogni modifica butta giù tutte le connessioni esistenti e ricostruisce le regole NAT. Su un server con più utenti è inutilmente brutale. WireGuard sa allineare la configurazione mentre è in funzione:

wg syncconf wg0 <(wg-quick strip wg0)

Il comando confronta il file con lo stato corrente e modifica solo le differenze. I peer esistenti mantengono la loro sessione. Tieni presente che la sostituzione di processo con <(...) richiede bash o zsh, mentre in una sh pura non funziona. Un singolo peer lo puoi anche aggiungere direttamente:

wg set wg0 peer CHIAVE_PUBBLICA allowed-ips 10.8.0.3/32

Questa modifica vive solo in memoria. Scrivila anche nel file di configurazione, altrimenti dopo il prossimo riavvio il peer è sparito. È tra l'altro la causa più frequente della frase "ieri funzionava ancora".

Se vuoi gestire un endpoint WireGuard in modo permanente e con un indirizzo stabile, un server tuo è la base più ovvia. Da KernelHost i server root KVM e i server dedicati girano nel datacenter maincubes di Francoforte sul Meno (TÜV TIER3+) sulla nostra rete, in PrePaid e senza durata minima. Come approfondimento sono utili i nostri articoli sulla messa in sicurezza di SSH e sulla configurazione di ufw.

Domande frequenti

Quale versione di WireGuard c'è in Debian 13, Debian 12, Ubuntu 24.04 e Ubuntu 22.04?
Tutte e quattro forniscono la stessa versione upstream degli strumenti, la 1.0.20210914, ciascuna con la propria revisione di distribuzione. Il modulo WireGuard vero e proprio arriva dal kernel fin dalla versione 5.6 e non va compilato a parte. I comandi wg e wg-quick si comportano allo stesso modo su tutti e quattro i sistemi, le differenze stanno nel filtro dei pacchetti (nftables oppure iptables), in resolvconf e in ufw. Quando devi installare resolvconf, su Debian e su Ubuntu 22.04 il pacchetto si chiama openresolv, mentre su Ubuntu 24.04 openresolv non esiste più e installi resolvconf.
Perché le righe iptables di molte guide non funzionano su Debian?
Il pacchetto wireguard-tools raccomanda nftables oppure iptables come alternativa. apt installa la prima alternativa disponibile, quindi nftables. Su un'installazione Debian minimale iptables non è quindi presente affatto e la riga PostUp fallisce, cosa che interrompe l'intero avvio di wg-quick. O usi la variante nft, oppure installi iptables in modo esplicito.
L'handshake riesce, ma non arrivo su internet. Da che cosa dipende?
Controlla in quest'ordine: net.ipv4.ip_forward deve valere 1, la regola NAT deve indicare l'interfaccia di uscita corretta (ip route show default te la mostra) e con ufw attivo serve inoltre ufw route allow in on wg0 out on eth0. Con ufw il solo interruttore del kernel non basta, perché ufw imposta una propria policy di inoltro.
Come riconosco un problema di MTU?
Il caso tipico è questo: il tunnel è su, ping e SSH funzionano, ma le pagine web si caricano solo a metà e i download grandi si interrompono. Imposta per prova MTU = 1280 nella sezione [Interface] del client. Se il problema sparisce, era la MTU. Il valore ottimale lo ricavi con ping -M do verso l'indirizzo del server, abbassando il payload finché non passa, poi aggiungendo 28 e sottraendo 80.
Devo gestire un server DNS mio perché il DNS funzioni nel tunnel?
No. Nella configurazione del client puoi semplicemente inserire un resolver pubblico, che viene poi raggiunto attraverso il tunnel. Un resolver tuo sul server (per esempio dnsmasq su wg0) conviene se vuoi risolvere nomi interni o mettere in cache le richieste. L'unica cosa importante è: se inserisci DNS = 10.8.0.1, su quell'indirizzo deve esserci davvero un name server in ascolto.
Perché un dispositivo non passa più dopo aver sistemato l'orologio?
WireGuard si protegge dal replay con un timestamp nel primo messaggio di handshake. Il server memorizza per ogni peer il valore più alto che ha visto e scarta quelli più vecchi. Se un dispositivo si è collegato una volta con un orologio spostato nel futuro, dopo la correzione viene rifiutato. Lo stato sta in memoria, quindi basta un wg-quick down wg0 e un wg-quick up wg0 sul server.

WireGuard VPN Debian Ubuntu nftables Rete Tutorial