
Lunedì mattina, 7:42, secondo caffè ancora caldo. Apro il monitoring di un servizio interno che gira sul mini PC in casa e vedo tutti i grafici a zero da circa quaranta minuti. Niente alert via Discord, niente errori nei log del container. Provo a pingare l’IP pubblico che ho nel record A: risponde, ma è un IP diverso da quello che mi aspettavo. Apro il pannello del provider casalingo e leggo l’avviso fresco di giornata: “abbiamo migrato la tua linea su nuova infrastruttura, l’IP pubblico è stato sostituito da CG-NAT condiviso”. Niente IP pubblico, niente port forwarding, niente accesso al servizio dall’esterno. Quaranta minuti dopo è di nuovo online, ma non con NAT classico: con un Cloudflare Tunnel tirato su in quindici minuti dalla shell. Vedi anche gli altri post di Production.
Questo post è il riassunto onesto di tre strade che ho usato (e a volte mescolato) per esporre servizi dell’home lab al pubblico: NAT classico con DDNS, Cloudflare Tunnel, Tailscale su WireGuard. Quando ognuna è la scelta giusta, dove ognuna ti pianta in faccia, e cosa rifarei dal day uno.
Strada 1: NAT classico con DDNS
Il pattern che tutti abbiamo usato per primo, e che funziona benissimo finché funziona.
Lo schema è elementare. Hai un dominio (<dominio>), un record A che punta all’IP pubblico del router casalingo, e sul router una regola di port forwarding che manda il traffico TCP :80 e :443 verso l’IP privato del server in LAN (<IP-PRIVATO>). Sul server gira un reverse proxy (nginx, Caddy, traefik) che fa SSL termination con Let’s Encrypt e dispatcha verso i vari servizi di backend.
Se l’IP pubblico è statico, fine: setti il record una volta, vivi felice. Se l’IP pubblico è dinamico (la maggioranza delle linee residenziali italiane lo è), ti serve DDNS. Cioè: uno script in cron che ogni minuto legge l’IP pubblico corrente, lo confronta con quello salvato, e se sono diversi chiama l’API Cloudflare per aggiornare il record A. Il template che giro fra i nodi del fleet, scrubbato, è pressappoco questo:
#!/bin/bash
ZONE_ID="<ZONE_ID>"
RECORD_ID="<RECORD_ID>"
API_TOKEN="<TOKEN_CON_SCOPE_DNS_EDIT>"
CURRENT_IP=$(curl -s https://api.ipify.org)
CACHED_IP=$(cat /var/cache/ddns_ip 2>/dev/null || echo "")
if [ "$CURRENT_IP" != "$CACHED_IP" ]; then
curl -s -X PUT \
-H "Authorization: Bearer $API_TOKEN" \
-H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"<dominio>\",\"content\":\"$CURRENT_IP\",\"ttl\":120}" \
"https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID"
echo "$CURRENT_IP" > /var/cache/ddns_ip
fi
Cron ogni minuto, log su syslog, e basta. Il TTL del record a 120 secondi serve a fare in modo che, quando il provider ti cambia l’IP, la propagazione DNS si chiuda in due minuti scarsi invece che in un’ora. Per un cliente che mi consulta su un home server “che gira da tre anni senza problemi”, questo è ancora il setup di default.
Quando NAT classico funziona benissimo:
- Hai una linea con IP pubblico stabile o dinamico ma senza CG-NAT (la cosa la verifichi confrontando l’IP che vedi dal router con quello che vedi da
curl ipify.orgsu un device della LAN: se sono diversi, sei dietro NAT del provider). - Hai accesso amministrativo al router (sei a casa tua, non in coworking, non in B&B con WiFi condiviso).
- Hai un singolo punto di esposizione, o al massimo una manciata di servizi su porte diverse.
- La latenza ti interessa: NAT classico è il path più corto in assoluto fra il client e il tuo server, perché il pacchetto va dal client all’IP pubblico al router, viene NATtato verso il privato, fine. Tipicamente 25-30 ms da una rete italiana FTTH a un server in casa nello stesso paese.
Quando NAT classico ti pianta in faccia:
- CG-NAT del provider. Non hai più un IP pubblico tuo: ne condividi uno con altre 200 utenze residenziali. Il port forwarding semplicemente non esiste più. È esattamente il caso che mi è capitato lunedì mattina.
- Coworking, ufficio condiviso, B&B. Non controlli il router, non puoi creare regole di forwarding. Anche se l’IP pubblico è vero, il forwarding non lo fai.
- IPv4 in via di estinzione. Nei prossimi anni i provider italiani spingeranno sempre più aggressivamente CG-NAT come default. Considera questa strada come strada legacy, non come default per setup nuovi.
- TLS challenge HTTP-01 dietro firewall del provider. Alcuni provider bloccano la porta 80 in entrata anche quando hai IP pubblico. Let’s Encrypt HTTP-01 fallisce. Workaround: DNS-01 con Cloudflare DNS, ne ho parlato in un post dedicato di Production.
Strada 2: Cloudflare Tunnel (Zero Trust)
L’idea concettuale è ribaltata rispetto al NAT classico: invece di aprire una porta in entrata sul tuo router, fai partire una connessione persistente in uscita verso Cloudflare. Quando un client chiama <dominio>, il traffico arriva sull’edge Cloudflare, viaggia dentro al tunnel persistente, e raggiunge il tuo server in LAN. Il tuo router non vede mai connessioni in entrata, non c’è nessuna porta aperta, e tutto questo funziona indipendentemente dal fatto che tu sia dietro CG-NAT, in coworking o sul WiFi del bar.
Setup pratico. Sul server in LAN installi cloudflared (binario ufficiale Cloudflare, pacchetti per Debian/Ubuntu/Arch o tar statico). Una volta:
cloudflared tunnel login
cloudflared tunnel create lab
Poi un file di config in /etc/cloudflared/config.yml:
tunnel: <TUNNEL_ID>
credentials-file: /etc/cloudflared/<TUNNEL_ID>.json
ingress:
- hostname: vault.<dominio>
service: http://<IP-PRIVATO>:8080
- hostname: ha.<dominio>
service: http://<IP-PRIVATO>:8123
- service: http_status:404
Sul lato Cloudflare, un record CNAME automatico (o esplicito) che punta vault.<dominio> e ha.<dominio> al tunnel. Su systemd:
cloudflared service install
systemctl enable --now cloudflared
Il tunnel resta su, fa keepalive in uscita verso Cloudflare ogni 30 secondi, e quando un client chiama uno dei due hostname il pacchetto arriva sul backend privato. Niente port forwarding, niente cert da gestire (la TLS termination è sull’edge Cloudflare), niente DDNS da tenere allineato.
Il quindici-minuti-dopo della mattina di lunedì è stato esattamente questo: ho disinstallato il setup DDNS+forwarding (che comunque era inutile dietro al nuovo CG-NAT), ho lanciato l’install di cloudflared, ho copiato il template del tunnel da un altro nodo, ho cambiato gli hostname nel file di config, ho fatto puntare i CNAME su Cloudflare. Il servizio è tornato online prima del terzo caffè.
Quando Cloudflare Tunnel è la scelta giusta:
- Sei dietro CG-NAT, in coworking, su rete che non controlli. Il tunnel risolve il problema di colpo.
- Vuoi esporre HTTP/HTTPS senza pensieri di certificate management. L’edge Cloudflare termina TLS automaticamente con cert wildcard zone-level.
- Ti va bene avere Cloudflare nel path. Se hai già la zona Cloudflare proxata, stai già passando da loro. Il tunnel rende solo esplicito il fatto.
- Hai bisogno di Access policy. Il free tier di Cloudflare Zero Trust ti permette di mettere autenticazione SSO (Google Workspace, GitHub, OTP) davanti a un servizio interno. Per Vaultwarden self-hosted o un dashboard di monitoring questo è il vero valore aggiunto.
Quando Cloudflare Tunnel ti dà fastidio:
- Latenza aggiuntiva. Il pacchetto passa per il PoP Cloudflare più vicino. Tipicamente 80-90 ms di RTT da Italia, contro 25-30 ms del NAT classico. Per HTTP/HTTPS è quasi invisibile, per WebSocket interattivi (terminale SSH via wetty, gioco peer-to-peer) lo senti.
- Dipendenza dura da Cloudflare. Se Cloudflare ha un incidente globale, il tuo home lab è offline. Capita raramente, ma capita. Considera il tunnel come single point of failure se i servizi sono critici.
- Bandwidth limit non documentato in chiaro. Il free tier ufficialmente è “unlimited” ma di fatto se ci passi 50 TB al mese ti scrivono. Per un home lab personale o di team piccolo non è un problema reale, per chi serve traffico video pesante sì.
- TCP non-HTTP è più scomodo. Il tunnel supporta anche TCP arbitrario (SSH, RDP), ma serve client
cloudflaredanche dal lato chi accede, oppure l’access via browser-based SSH di Cloudflare. Per uso quotidiano fra dev del team, è più friction.
Strada 3: Tailscale (mesh VPN su WireGuard)
Cambiamo paradigma di nuovo. Tailscale non espone il servizio al pubblico: crea una rete privata virtuale fra i tuoi device, su WireGuard, con discovery automatico e nomi DNS interni. Il NAS in casa, il MacBook in trasferta, l’iPhone in spiaggia, il Proxmox del laboratorio: tutti nella stessa rete 100.x.x.x, tutti raggiungibili con un nome interno tipo nas.lab o vault.lab (con la feature MagicDNS attiva), nessuno esposto al pubblico.
Setup. Sul lato Tailscale: account gratuito (login con Google, GitHub o Apple), creazione di una “tailnet”. Sul lato device: install del client (binari ufficiali per macOS/Linux/Windows/iOS/Android), tailscale up, autorizzazione del nodo dal pannello web. Novanta secondi per nodo, in serata aggiungi tutti i device della casa.
Esempio concreto del mio setup. Il NAS in casa con TrueNAS ha Tailscale installato come app. Si chiama nas.lab. Dal mio iPhone, anche fuori casa, in 4G, apro l’app Files o un client SMB e vedo il NAS come nas.lab. Niente FQDN pubblico, niente record DNS esposto al mondo, niente Cloudflare nel path. Il pacchetto va dal mio iPhone a un PoP Tailscale, viene routato direttamente al NAS in casa via WireGuard, e arriva sul filesystem. Tipicamente 30-40 ms di RTT, perché Tailscale fa hole punching aggressivo e quando entrambi gli endpoint sono dietro NAT cooperativi il path diventa diretto LAN-to-LAN senza relay (la modalità “DERP relay” si attiva solo quando il NAT punching fallisce, e raddoppia la latenza).
Stesso pattern per il Proxmox del laboratorio. Lo chiamo pve.lab su Tailscale, e da qualunque rete del mondo apro https://pve.lab:8006/ e vedo l’interfaccia Proxmox. Nessun port forwarding, nessuna esposizione pubblica del management.
Quando Tailscale è la scelta giusta:
- Accesso personale o di team, non pubblico. I servizi non devono essere visibili al mondo, solo a te o al team.
- Decentralizzazione del calcolo nel team. Per Romiltec, ogni dev ha il suo LXC sul cluster Proxmox interno. Tailscale gli permette di lavorare da qualsiasi rete (casa, coworking, B&B in Sardegna ad agosto) come se fosse in laboratorio. È una delle cose che ha più cambiato il modo in cui lavoriamo, ne parlo in un altro post di Production.
- Niente certificate management per uso interno. Il traffico è cifrato da WireGuard end-to-end, quindi il fatto che la GUI Proxmox sia su
https://self-signed non è un problema (i certificati Let’s Encrypt nominativi servirebbero solo all’avvio del browser per non chiedere “accetti questo cert?”; in alternativa Tailscale offre cert HTTPS nominativi per le tailnet ma il free tier ha limiti). - Subnet routing avanzato. Tailscale può anche routare un’intera subnet privata (es. tutta la tua LAN
192.168.50.0/24) attraverso un nodo Tailscale che fa da exit gateway. Utile se hai 30 device in casa e vuoi accedere a tutti senza installare Tailscale su ognuno.
Quando Tailscale non è la scelta giusta:
- Servizio destinato al pubblico. Tailscale è VPN: solo chi è invitato nella tailnet vede i nodi. Per un blog WordPress che deve essere su Google, ovvio che questa non è la strada.
- Account Tailscale come single point of authority. Se l’admin della tailnet perde l’account, tutti i nodi sono orfani. Per uso aziendale serio si usa SSO Tailscale a pagamento, non il free tier con login Google personale.
- Free tier limitato a 100 device. Per un home lab personale, ne avanza. Per un team di 30 dev con device multipli ciascuno, ti incartano.
- Funnel pubblico via Tailscale. Esiste come feature, ma è limitato e meno pulito di Cloudflare Tunnel per quel caso d’uso.
La matrice decisionale che giro
Quando un cliente mi chiede consiglio sul “come espongo questa cosa”, il flowchart mentale è semplice.
| Scenario | Strada |
|---|---|
| Servizio pubblico, IP statico, controllo router | NAT classico + Cloudflare DNS |
| Servizio pubblico, IP dinamico no CG-NAT, controllo router | NAT classico + DDNS |
| Servizio pubblico, CG-NAT o niente controllo router | Cloudflare Tunnel |
| Servizio pubblico con auth davanti (vault, dashboard) | Cloudflare Tunnel + Access policy |
| Accesso personale a NAS / Proxmox / dashboard interna | Tailscale |
| Decentralizzazione di calcolo per team | Tailscale (con SSO se serio) |
| WebSocket interattivo low-latency, pubblico | NAT classico se possibile, sennò Tunnel ma valuti il delta |
| Cliente che vuole isolamento + accesso per pochi | Tailscale, eventualmente con shared node |
I tre cluster sopra non sono mutuamente esclusivi: nel mio setup convivono. Il blog pubblico va via NAT classico (ora Cloudflare Tunnel, dopo il cambio CG-NAT). Vaultwarden self-hosted è dietro Cloudflare Tunnel con Access policy email-only sul team. Il NAS personale e la GUI Proxmox sono solo su Tailscale. Ogni servizio sceglie la strada coerente con chi deve raggiungerlo e da dove.
Latenze misurate, dati reali
Tre setup, stesso server di test in casa, client dalla stessa rete (FTTH 1 Gbps, Italia centrale):
- NAT classico su porta esposta: RTT medio 25 ms (TCP
:443, banda piena 600+ Mbit/s download). - Cloudflare Tunnel verso lo stesso server: RTT medio 85 ms (TCP
:443, banda 200-300 Mbit/s download, frenata principalmente dal singolo tunnel). - Tailscale fra MacBook in casa e server in casa: RTT medio 30 ms (path diretto LAN-to-LAN via hole punching, banda piena).
- Tailscale fra MacBook in 4G e server in casa: RTT medio 55 ms (path diretto se hole punching riesce, fino a 150 ms se cade su DERP relay).
Per un browser che apre una pagina HTML, il delta è invisibile. Per un editor remoto via SSH (mosh, eternalterminal), il delta è la differenza fra digitare scorrevole e digitare a singhiozzo. Per chi usa il NAS come storage di lavoro durante la giornata, il delta è quanto pesa stranamente uno ls su una cartella grande.
Postmortem: la mattina del CG-NAT
Tornando alla mattina di lunedì. Cosa è andato storto e cosa ho rifatto.
Cosa è andato storto. Il provider ha cambiato la mia linea da IP pubblico dinamico a CG-NAT, senza alert via mail (l’avviso era solo nel pannello web, che io guardo una volta al mese). Il record A puntava all’IP che il router mostrava all’interfaccia LAN, ma quell’IP non era più routabile dall’esterno (era un IP della subnet CG-NAT condivisa). Il monitoring di disponibilità del servizio è andato a zero perché probe esterne non rispondevano più, ma il container era acceso e funzionante. Quaranta minuti in cui non sapevo se il problema fosse cliente, server, rete locale o internet upstream.
Cosa ho fatto. Diagnosi in tre passi. Primo: curl ipify.org da un device della LAN mi ha mostrato un IP diverso da quello del router. Secondo: aperto pannello provider, letto avviso CG-NAT. Terzo: aperto runbook interno, sezione “fallback per CG-NAT”, copia-incolla del setup cloudflared di un altro nodo, modifica del file di config con i miei hostname e IP backend, lancio del service, modifica dei CNAME su Cloudflare. Tempo totale dal “ah, ecco perché” al “servizio online”: 14 minuti.
Cosa avrei dovuto fare prima. Avere il fallback già configurato. Il binario cloudflared installato sul server, la config pronta ma con il systemctl disable attivo, il tunnel registrato su Cloudflare ma con i CNAME non puntati. In questo modo il fallback si fa con due comandi: systemctl enable --now cloudflared e cambio dei CNAME via API Cloudflare. Tempo da incidente a online: 90 secondi. Era una settimana di lavoro evitabile in piedi che non ho fatto, e ho pagato 40 minuti di disservizio per una pigrizia da founder.
Cosa rifarei diversamente
Tre cose, in ordine.
Una. Tailscale dal day uno, anche con NAT classico funzionante. Costo: zero (free tier basta per uso personale e team piccolo). Beneficio: hai sempre un canale alternativo per accedere al server, anche se l’esposizione pubblica si rompe. Quando il NAT classico è down e devi diagnosticare, hai comunque la SSH via Tailscale che funziona. Fra l’altro Tailscale ti dà gratis la decentralizzazione del calcolo per il team che ho descritto sopra: vuoi davvero rinunciarci?
Due. Fallback Cloudflare Tunnel preconfigurato, anche su nodi che usano NAT classico in produzione. Binario installato, tunnel registrato, config in sleep. In caso di emergenza, due comandi e sei online sul fallback. È come gli airbag: speri di non usarli mai, ma il giorno che servono il delta di costo dell’averli sopra il delta del non averli è enorme.
Tre. Smettere di trattare NAT classico come default per setup nuovi. Per home lab montati nel 2026, parto da Cloudflare Tunnel di default. NAT classico lo lascio solo per scenari dove la latenza conta davvero (giochi, streaming a basso delay) e ho la garanzia di IP pubblico stabile. Il costo cognitivo del DDNS + port forwarding + cert management non vale più i 50 ms di latenza in meno per la maggioranza dei casi.
Quello che non ho coperto in questo post
Ce ne sono almeno due cose grosse che meritano un post a sé. Una: l’esposizione di servizi TCP non-HTTP (database remoti, SSH, mosh) attraverso queste tre strade. Tailscale vince a mani basse. Due: la combinazione di Tailscale + Cloudflare Tunnel sullo stesso nodo, dove il tunnel espone il pubblico e Tailscale gestisce l’amministrativo. È il setup che giro io sulla maggior parte dei nodi del fleet, e merita una scheda dettagliata.
Per ora il riassunto è questo. Tre strade, tre casi d’uso distinti, e un postmortem che mi ha insegnato a tenere sempre il fallback già pronto. Se devi esporre un home lab al pubblico nel 2026, parti da Cloudflare Tunnel come default per pubblico, Tailscale come default per personale e team, NAT classico solo se davvero ti serve la latenza. E configura il fallback prima che ti serva, non dopo.
