Il Contesto#
Stavo lavorando al passaggio graduale della gestione dei segreti del cluster TazLab da Infisical a un’istanza Vault su una VM Hetzner. Il progetto — 09-vault-k8s-integration-prep — era ben avanzato: la VM era operativa, Vault inizializzato e funzionante, il nome host su Tailscale convergeva sul MagicDNS lushycorp-vault.magellanic-gondola.ts.net, e il playbook Ansible aveva superato i primi cicli completi di destroy/create.
Ma la pipeline non era ancora abbastanza solida. Ogni tanto il create.sh si bloccava. Non in modo casuale, quasi sempre sugli stessi punti: un restart del servizio Vault, un task di fetch, un apt install. All’inizio sembravano problemi di playbook: handler systemd_service che non tornavano, “sessione SSH morta durante la riconnessione”.
Dopo aver convertito in asincroni tutti i 7 task systemd_service sincroni e averlo spezzato in tre playbook separati — uno per l’installazione, uno per la convergenza vero e propria, uno per la finalizzazione post-convergenza — i miglioramenti c’erano, ma il problema non spariva del tutto.
Qualcosa non tornava.
La Soglia dei 76 Secondi#
Il passo successivo è stato rendere il problema misurabile. Ho costruito una matrice di test sistematica dall’interno del container TazPod verso la VM Hetzner, usando SSH puro:
| Operazione | Esito | Tempo |
|---|---|---|
echo ok | ✅ | immediato |
sleep 120 | ✅ | 2 minuti |
| output continuo per 2 minuti | ✅ | 2 minuti |
curl download di un file .deb da 8 MB | ✅ | ~1 minuto |
curl limitato a 100k/s (80s) | ✅ | 80 secondi |
apt-get update | ✅ | 4 secondi |
apt-get download awscli | ✅ | 2 secondi |
sudo apt-get install --reinstall -y awscli | ❌ | ~76 secondi |
Lo schema era chiarissimo: qualsiasi operazione superasse un certo pattern di I/O durante l’installazione dei pacchetti faceva collassare la connessione SSH.
Per escludere Ansible dalla diagnosi, ho ripetuto il test con SSH puro e verbose logging:
ssh -vvv -o ProxyCommand="tailscale --socket=... nc %h %p" \
admin@<tailnet-ip> \
"sudo apt-get install --reinstall -y awscli"Risultato: stessa morte dopo ~76 secondi, identica sia via Ansible che via SSH diretto.
Il log -vvv mostrava uno schema preciso:
debug1: channel 0: new session
debug1: Entering interactive session.
debug2: exec request accepted on channel 0
debug2: channel 0: read failed ... Broken pipe
debug2: channel 0: send eof
debug3: send packet: type 80
debug3: send packet: type 80
...
Timeout, server <ip> not responding.La connessione entrava in interactive session, il comando partiva, poi il canale SSH si rompeva con Broken pipe, seguito da ripetuti tentativi di keepalive e infine dal timeout.
Cosa Funziona e Cosa Non Funziona#
Il pattern escludeva molte ipotesi:
- Non era la durata della sessione:
sleep 120passava senza problemi - Non era il volume di traffico:
curldi 8 MB per 80 secondi passava - Non era la larghezza di banda ridotta:
curlthrottled a 100k/s passava - Non era Ansible: SSH puro falliva allo stesso modo
- Non era
aptin sé:apt-get updateeapt-get downloadfunzionavano - Non era l’ultimo task del playbook: il problema si manifestava anche nei primi task dopo la connessione
Il salto logico più importante era questo: il fallimento avveniva specificamente durante apt install, non durante download, upload, o sleep lunga. C’era qualcosa nel pattern di I/O generato dall’installazione — scrittura su disco, scripts post-install, aggiornamento del database di dpkg — che faceva collassare il trasporto SSH via tailscale nc.
Il Sospetto sul Trasporto#
Il container TazPod esegue Tailscale in una configurazione particolare. Quando lo crei su Docker, non c’è /dev/net/tun — quindi Tailscale deve funzionare in userspace networking mode, un loop software che emula WireGuard senza un’interfaccia kernel. L’SSH verso la VM raggiunge il peer attraverso un ProxyCommand:
ssh -o ProxyCommand="tailscale nc %h %p" ...Questo comando dice a SSH di non connettersi direttamente alla VM, ma di passare attraverso tailscale nc, che inoltra il traffico TCP sulla tailnet usando lo stack userspace.
La combinazione di tre livelli — Docker bridge network + Tailscale userspace + ProxyCommand “nc” — era un’architettura funzionale per comandi brevi, ma si rivelava fragile per operazioni che richiedevano una connessione stabile per minuti con burst di I/O.
La conferma più forte è arrivata quando ho confrontato lo stato del peer Tailscale tra sessioni “buone” e “cattive”. Nei log storici di create precedenti di successo, il peer era spesso in stato active; direct 178.104.84.205:41641 — cioè connessione diretta WireGuard. Nelle sessioni problematiche, il peer appariva in stato ambiguo, spesso via DERP relay, a volte con metadati inconsistenti tra ping e status.
Questo non provava che DERP fosse la causa, ma suggeriva che il path di trasporto non fosse pulito.
Il Test Definitivo: Uscire dall’Userspace#
A questo punto ho deciso di cambiare una variabile alla volta. La più grande era: “Cosa succede se eseguiamo Tailscale in modalità kernel, con un vero /dev/net/tun, invece che in userspace?”
Ho preparato un container di test con una configurazione diversa:
docker run -d --name tazpod-test \
--network host \
--cap-add NET_ADMIN \
--device /dev/net/tun \
tazzo/tazpod-ai:latest \
sleep infinityPoi ho avviato Tailscale in modalità TUN normale, con un helper script che ora fa parte dell’immagine:
tazpod-tailscale-upE ho ripetuto esattamente lo stesso test che prima falliva sempre:
ansible ... -m shell -a \
'sudo DEBIAN_FRONTEND=noninteractive apt-get install --reinstall -y awscli'Risultato: completato in 9 secondi.
Stessa VM, stesso comando, stesso Ansible, stessi secret. L’unica differenza era il trasporto: non più tailscaled --tun=userspace-networking + ProxyCommand tailscale nc, ma una connessione diretta su tailnet attraverso il kernel WireGuard.
Il problema non era nel playbook, non era in Ansible, non era in apt o dpkg. Era nella combinazione di userspace networking e ProxyCommand via nc che, per ragioni ancora da investigare a fondo, non reggeva il workload di installazione dei pacchetti.
La Pipeline Rinasce#
Con la causa isolata, le modifiche sono state sorprendentemente contenute.
Il runtime del container TazPod ora usa di default:
--network host— niente bridge Docker--cap-add NET_ADMIN— necessario per il TUN--device /dev/net/tun— l’interfaccia kernel
L’helper tazpod-tailscale-up avvia tailscaled in background, genera una chiave di autenticazione usando le stesse credenziali OAuth (con fallback su API key) già presenti nel vault, e connette il container alla tailnet.
L’inventory Ansible viene generato dinamicamente: se /dev/net/tun è presente, usa SSH diretto sul tailnet senza ProxyCommand; altrimenti torna alla vecchia via di tailscale nc. Questa logica di auto-rilevamento è nell’helper render-tailscale-inventory.sh.
Il playbook Vault, che prima era un monolite, è stato suddiviso in tre fasi con tempi separati:
| Fase | Durata |
|---|---|
| Installazione runtime (pacchetti, config, servizio) | 175s |
| Convergenza (classificazione, restore, unseal, health) | 90s |
| Post-convergenza (token, backup, persistenza) | 38s |
| Totale | ~344s (5.7 min) |
Il precedente tempo migliore era circa 1200 secondi (20 minuti) con frequenti blocchi. Il divario è sostanziale e, più importante, la pipeline è ora deterministica: zero timeout, zero UNREACHABLE, zero interventi manuali.
Cosa Abbiamo Imparato#
La prima lezione è stata metodologica. Il problema era nascosto sotto almeno tre strati di astrazione: creavo il container con TazPod, che avvia Docker, che non aveva /dev/net/tun, quindi Tailscale usava userspace, che obbligava a un ProxyCommand nc, e quello non reggeva certi pattern di traffico. Eravamo talmente abituati a questa configurazione da non considerarla più come possibile causa.
La seconda lezione è che i test di isolamento funzionano. Ridurre il problema fino a SSH puro, poi confrontare trasporti diversi (public SSH vs tailnet SSH, userspace vs TUN) ha dato una risposta chiara in poche ore. Se avessi continuato a “aggiustare” il playbook, sarei ancora al giro.
La terza lezione è che la modalità userspace-networking di Tailscale, pur straordinariamente utile per ambienti dove non hai privilegi di kernel (container su PaaS, Lambda, CI/CD), ha dei limiti operativi che ti si presentano solo dopo che il setup ha girato per ore. Non è un bug di Tailscale di per sé. È una combinazione di layer che insieme diventano fragili: Docker bridge nativo + userspace + ProxyCommand = una catena di dipendenze difficile da debuggare.
Stato Attuale#
La VM Hetzner Vault è operativa e la pipeline di creazione è stabile e misurabile. Il progetto 09-vault-k8s-integration-prep ha chiuso la Phase 1 (convergenza runtime + validazione trasporto) con successo e tempo di esecuzione noto.
Il prossimo passo — Phase 2 — riguarda il lato cluster: configurare CoreDNS per risolvere correttamente il nome lushycorp-vault.magellanic-gondola.ts.net nella tailnet, creare il ClusterSecretStore in Kubernetes per leggere i segreti da Vault, e verificare il tutto con uno smoke test ESO.
Ma questa è un’altra giornata di lavoro.


