Salta al contenuto principale

Il Canto del Cigno di DevPod: Scontro tra Automazione e Sicurezza Zero Trust

Roberto Tazzoli
Autore
Roberto Tazzoli
Appassionato di infrastrutture dichiarative, self-hosting e Kubernetes.

Introduzione: L’illusione del Controllo Totale
#

Nella prima parte di questo diario tecnico, ho delineato l’architettura di una workstation immutabile basata su DevPod. L’obiettivo era ambizioso: una “Golden Image” che contenesse ogni strumento necessario per l’orchestrazione del mio cluster Kubernetes (Proxmox, Talos, Longhorn), eliminando l’entropia della configurazione locale. Tuttavia, come ogni ingegnere sa, il passaggio dalla teoria alla pratica espone falle che nessuna pianificazione può prevedere completamente.

In questa sessione, mi sono posto un obiettivo ancora più estremo: trasformare il DevPod in un ambiente Zero Trust. Non volevo solo un container con i miei strumenti; volevo un’enclave sicura in cui i segreti critici (Kubeconfig, chiavi SSH, token API) non risiedessero mai su disco in chiaro, nemmeno all’interno del container isolato.

Il mindset della giornata era improntato alla paranoia costruttiva. Mi sono chiesto: “Se qualcuno compromettesse fisicamente il mio laptop o riuscisse a eseguire un comando non autorizzato nel container, cosa troverebbe?”. La risposta doveva essere: “Assolutamente nulla”.

Questa è la cronaca tecnica di come ho cercato di piegare DevPod a questa visione di sicurezza radicale, scontrandomi con la sua stessa architettura orientata alla comodità, fino a giungere alla decisione inevitabile di abbandonare lo strumento per ricominciare su basi diverse.


Fase 1: Refactoring dell’Immagine e l’Incubo della Cache
#

Prima di affrontare la sicurezza, ho dovuto risolvere un problema di efficienza architetturale. Il mio Dockerfile originale stava diventando un monolite ingestibile. Ogni piccola modifica ai dotfiles richiedeva una ricostruzione completa dell’intera immagine, un processo che consumava banda e tempo prezioso.

Il Ragionamento: L’Architettura a Layer
#

Ho deciso di decomporre l’immagine in tre layer logici distinti:

  1. Layer Base (Dockerfile.base): Il fondamento del sistema operativo, i tool di sicurezza (Infisical, SOPS) e i binari stabili (Eza, Neovim, Starship).
  2. Layer Kubernetes (Dockerfile.k8s): Lo stack specifico per l’orchestrazione (Kubectl, Helm, Talosctl).
  3. Layer AI (Dockerfile.gemini): La pesante CLI di Gemini, che richiede un runtime Node.js dedicato.

Deep-Dive Concettuale: Docker Layer Caching Il caching dei layer in Docker funziona secondo una logica deterministica: se il contenuto di un’istruzione (come un comando RUN o un COPY) non cambia, Docker riutilizza il layer precedentemente costruito. Questo è fondamentale per l’integrazione continua (CI/CD). Tuttavia, se un layer alla base della catena cambia, tutti i layer successivi vengono invalidati e devono essere ricostruiti. Separando i tool stabili da quelli pesanti o frequentemente aggiornati, ho cercato di massimizzare la velocità di iterazione.

Il Sintomo: La Cache “Invisibile”
#

Durante i test, sono incappato in un comportamento frustrante. Avevo aggiornato il tema di Starship nei dotfiles (passando da Gruvbox a un più riposante Pastel Powerline), ma nonostante lanciassi la build, il container continuava a presentarsi con il vecchio tema.

Controllando i log di build, ho notato l’infame etichetta => CACHED proprio sul comando COPY dotfiles/. Docker non rilevava che i file all’interno della cartella dell’host erano cambiati.

La Soluzione: Cache Busting Dinamico
#

Per forzare Docker a invalidare la cache nel punto esatto desiderato, ho introdotto un argomento di build dinamico.

# Dockerfile.base snippet
# ... tool stabili ...

# Argomento per forzare l'aggiornamento dei dotfiles
ARG CACHEBUST=1
RUN echo "Cache bust: ${CACHEBUST}"

# Ora Docker è costretto a rieseguire la copia se CACHEBUST cambia
COPY --chown=vscode:vscode dotfiles/ /home/vscode/

Lanciando la build con --build-arg CACHEBUST=$(date +%s), ho iniettato il timestamp attuale nel processo. Poiché il comando RUN echo cambiava ad ogni secondo, Docker era matematicamente obbligato a ricostruire quel layer e tutti i successivi, garantendo l’iniezione dei nuovi file di configurazione.


Fase 2: L’Enclave in RAM e il Conflitto col Kernel
#

Risolto il problema della cache, sono passato al cuore del progetto: il Vault Cifrato. L’idea era creare un volume LUKS (Linux Unified Key Setup) all’interno del container.

Il Ragionamento: Perché LUKS in un Container?
#

Normalmente, i container si affidano all’isolamento del namespace del kernel. Ma i file all’interno di un container sono accessibili a chiunque abbia privilegi di root sull’host o possa eseguire un docker exec. Crittografando una porzione di filesystem con LUKS e sbloccandola solo tramite una passphrase inserita manualmente, i segreti vengono protetti da una chiave crittografica che risiede solo nella memoria RAM (e nella mente dell’utente).

Deep-Dive Concettuale: Linux Unified Key Setup (LUKS) LUKS è lo standard per la crittografia dei dischi in Linux. Funziona creando un layer tra il dispositivo fisico (o un file immagine) e il filesystem. Questo layer gestisce la decifratura al volo dei blocchi di dati. Nel contesto di un container, l’uso di LUKS richiede l’accesso al Device Mapper del kernel host, un’operazione intrinsecamente complessa da isolare.

L’Indagine: Il Fallimento del Loop Device
#

Il primo tentativo di creare il vault in RAM tramite tmpfs ha sbattuto contro un errore del kernel: Attaching loopback device failed (loop device with autoclear flag is required).

In un ambiente Docker, anche se il container è lanciato con il flag --privileged, il comando cryptsetup spesso non riesce ad allocare automaticamente i loop device (quei dispositivi virtuali che permettono di trattare un file come un disco rigido). Questo accade perché i nodi in /dev/loop* non vengono creati dinamicamente all’interno del container.

La Soluzione: Mknod e Losetup Manuale
#

Ho dovuto implementare una procedura di sblocco robusta che preparasse il terreno per il kernel:

# Snippet dello script di sblocco (devpod-zt.sh)
echo "🛠️  Preparing loop devices (0-63)..."
sudo mknod /dev/loop-control c 10 237 2>/dev/null || true
for i in $(seq 0 63); do
    sudo mknod /dev/loop$i b 7 $i 2>/dev/null || true
done

echo "💾 Engaging Secure Enclave (RAM)..."
# Montaggio tmpfs dedicato per evitare i limiti di /dev/shm
sudo mount -t tmpfs -o size=256M tmpfs "$VAULT_BASE"

# Associazione manuale del loop device
LOOP_DEV=$(sudo losetup -f --show "$VAULT_IMG")
echo -n "$PLAIN_PASS" | sudo cryptsetup luksFormat --batch-mode "$LOOP_DEV" -
echo -n "$PLAIN_PASS" | sudo cryptsetup open "$LOOP_DEV" "$MAPPER_NAME" -

Questa mossa è stata cruciale. Creando manualmente i nodi dei dispositivi e gestendo l’associazione losetup al di fuori dell’automatismo di cryptsetup, sono riuscito a superare le restrizioni del runtime di Docker e a montare finalmente un filesystem cifrato funzionante in ~/secrets.


Fase 3: Lo Scontro tra Automazione e Hardening
#

Con il vault funzionante, ho cercato di automatizzare il processo. Volevo che il container chiedesse la password immediatamente all’ingresso. Ho implementato una Trap-Shell nel .bashrc: uno script che intercettava l’avvio della sessione e lanciava la procedura di sblocco.

Il Sintomo: I “Fantasmi” nei Log
#

Non appena attivata la Trap-Shell, ho iniziato a vedere un output incessante ogni 30 secondi nei log di devpod up: 00:32:47 debug Start refresh ... Device secrets_vault already exists.

L’Analisi: Il Ciclo di Vita del DevPod Agent
#

Qui ho scoperto la vera natura del DevPod Agent. Per fornire funzionalità come il port forwarding e il sync dei file, l’agent di DevPod mantiene un canale SSH o un socket aperto verso il container. Ogni 30 secondi, l’agent esegue dei comandi di “refresh” (come update-config) lanciando nuove shell nel container.

Poiché la mia Trap-Shell era nel .bashrc, ogni volta che l’agent entrava per un controllo di routine, lo script di sicurezza partiva, cercava di chiedere una password (che l’agent non poteva dare) o provava a rimontare un volume già attivo, generando errori a catena.

Deep-Dive Concettuale: Shell Interattive vs Non-interattive In Bash, le shell possono essere interattive (collegate a un terminale/TTY) o non-interattive (eseguite da uno script o un demone). L’agent di DevPod lancia shell non-interattive. Ho cercato di risolvere il problema filtrando l’esecuzione dello script di sicurezza:

# Modifica nel .bashrc
if [[ $- == *i* ]]; then
    # Esegui sblocco solo se l'utente è davanti allo schermo
    tazpod-unlock
fi

Sebbene questo abbia ridotto il rumore, non ha risolto il problema di fondo: DevPod Agent continuava a “litigare” con il mio ambiente blindato.


Fase 4: La Caduta di SSH e la Scoperta del “Fail-Open”
#

L’ultimo chiodo sulla bara dell’approccio basato su DevPod è stato il tentativo di blindare l’accesso SSH. Volevo che anche dopo aver sbloccato il pod, l’uscita dalla shell smontasse tutto e che il rientro richiedesse di nuovo la password.

Ho provato a rimuovere le chiavi SSH iniettate da DevPod (rm ~/.ssh/authorized_keys). Risultato? L’agent di DevPod è andato in panico, perdendo la capacità di gestire il workspace. Ho provato a implementare un Watchdog in background che contasse i processi bash attivi e smontasse il vault al termine dell’ultima sessione. Ma la complessità stava scalando esponenzialmente rispetto ai benefici.

La Vulnerabilità “Ctrl+C”
#

Durante un test di penetrazione manuale, ho scoperto una falla imbarazzante: se premevo Ctrl+C durante la richiesta della password di Infisical, lo script veniva interrotto ma la shell mi dava comunque il prompt dei comandi. Era un sistema di sicurezza che poteva essere bypassato con un semplice tasto.

Ho risposto implementando una Trap SIGINT brutale:

# Nel .bashrc
trap "echo '❌ Interrupted. Exiting.'; exit 1; kill -9 $$" INT

Funzionava. Ma a quel punto, il mio ambiente di sviluppo era diventato una ragnatela di hack, script Bash fragili che cercavano di gestire segnali del kernel, e conflitti perenni con l’agente di orchestrazione di DevPod.


Fase 5: La Resa e il Cambio di Paradigma
#

Dopo ore passate a combattere contro il Device already exists del Device Mapper e i refresh infiniti dell’agente, sono giunto a una conclusione dolorosa ma necessaria: DevPod non è lo strumento adatto per un’enclave Zero Trust.

DevPod è costruito sulla filosofia della Convenience-First. Vuole che tu sia operativo in un click, che le tue chiavi SSH siano sincronizzate ovunque, che il tuo ambiente sia “sempre pronto”. La mia visione di sicurezza, invece, richiede un ambiente che sia “mai pronto” finché l’utente non lo decide esplicitamente.

La Decisione: Ho deciso di buttare via tutto il lavoro fatto con DevPod. Ho deciso di eliminare l’agente, le chiavi SSH automatiche e il server VS Code integrato.

Il nuovo approccio sarà basato su:

  1. Pure Docker: Un container Debian Slim lanciato manualmente con script di avvio controllati al 100%.
  2. Go CLI: Una CLI dedicata scritta in Go (che chiameremo tazpod) per gestire in modo robusto e atomico l’intero ciclo di vita della sicurezza, eliminando la fragilità degli script Bash.
  3. Terminal-Only Workflow: Abbandono di VS Code in favore di Neovim (LazyVim), eliminando la necessità di canali SSH persistenti per l’IDE.

Conclusioni: Cosa abbiamo imparato in questa tappa
#

Questa sessione, apparentemente un fallimento, è stata in realtà una lezione magistrale di ingegneria dei sistemi. Ho imparato che:

  • L’automazione non è sempre alleata della sicurezza estrema.
  • Il kernel host e il container hanno un rapporto di dipendenza molto stretto quando si parla di crittografia, e gli intermediari rendono il debug impossibile.
  • Saper rinunciare a uno strumento quando non risponde più ai requisiti è una competenza senior fondamentale quanto saperlo configurare.

L’Officina Immutabile non è morta; sta solo cambiando pelle. Nel prossimo post, documenterò la nascita della CLI TazPod in Go e il passaggio a un ambiente Pure Docker, dove il controllo non è più un’opzione, ma il fondamento stesso dell’architettura.


Comments