Salta al contenuto principale

Phoenix Protocol V2: Sicurezza Enterprise, Parallelismo e il Traguardo degli 8 Minuti

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

Phoenix Protocol V2: Sicurezza Enterprise, Parallelismo e il Traguardo degli 8 Minuti
#

Se il primo capitolo del Phoenix Protocol riguardava la validazione del dato e la sua immortalità attraverso il ripristino da S3, questa seconda tappa del viaggio nel Castello Effimero affronta una sfida ancora più ambiziosa: la perfezione del processo. Non basta che il cluster rinasca; deve farlo in modo deterministico, senza esitazioni umane e con un profilo di sicurezza che non ammette compromessi, nemmeno durante i pochi minuti in cui l’infrastruttura è “nuda” sotto il fuoco del bootstrap.

Oggi ho deciso di spingere il limite oltre la soglia psicologica dei dieci minuti. Per farlo, ho dovuto ripensare radicalmente il modo in cui il cluster “reclama” la propria identità e come i diversi layer si incastrano tra loro. Questo non è solo un esercizio di velocità, ma una ricerca di efficienza ingegneristica dove ogni secondo risparmiato è un’incertezza rimossa.


Il Mindset: La Sicurezza come Cemento, non come Vernice
#

Spesso, nei progetti HomeLab o nelle infrastrutture in fase di sviluppo, si tende a “far funzionare le cose” e poi, solo in un secondo momento, a blindarle. Ho deciso che questo approccio è intrinsecamente fallace. In un’architettura Zero-Knowledge, la sicurezza deve essere il cemento delle fondamenta. Se un segreto tocca il disco durante il bootstrap, quel disco è compromesso per sempre nella mia visione.

L’obiettivo della sessione è stato duplice: eliminare le dipendenze esterne instabili e garantire che nessun segreto “viaggi” in chiaro o risieda in modo persistente sull’host che orchestra la rinascita.


Fase 1: Lo Spostamento del “Root of Trust” (Addio GITHUB_TOKEN)
#

Uno dei rischi latenti nelle versioni precedenti era la presenza del GITHUB_TOKEN nelle variabili d’ambiente dell’host durante l’esecuzione di Terragrunt. Sebbene il token fosse iniettato in RAM, la sua esistenza nel guscio bash rappresentava un punto di attacco.

Il Ragionamento: Perché l’Internalizzazione dei Segreti?
#

Ho deciso di spostare la responsabilità del recupero dell’identità all’interno del cluster stesso. Invece di “consegnare” il token a Flux CD durante l’installazione, ho configurato il sistema affinché sia il cluster, appena nato, a “reclamare” il proprio accesso al codice.

L’alternativa sarebbe stata continuare a passare il token via variabile d’ambiente, ma questo avrebbe mantenuto il segreto esposto ai log di sistema dell’host e a potenziali dump della memoria dei processi figli. Usando l’External Secrets Operator (ESO) e una Machine Identity di Infisical, il cluster diventa autonomo.

Deep-Dive: Machine Identity
#

Una Machine Identity è un’entità di sicurezza progettata per sistemi automatizzati. A differenza di un token generato da un utente umano, essa è legata a un ruolo specifico con permessi granulari (Least Privilege) e può essere revocata o ruotata senza impattare le utenze reali. È il cuore del modello “Trust no one, verify internal identity”.

Implementazione Tecnica
#

Ho modificato il layer engine affinché prepari il terreno per Flux prima ancora che Flux venga installato. Il trucco risiede in un loop di attesa intelligente:

# modules/k8s-engine/main.tf

# 1. Creazione del namespace per Flux in anticipo
resource "kubernetes_namespace_v1" "flux_system" {
  metadata {
    name = "flux-system"
  }
}

# 2. Iniezione della Machine Identity per Infisical
resource "kubernetes_secret_v1" "infisical_machine_identity" {
  metadata {
    name      = "infisical-machine-identity"
    namespace = kubernetes_namespace_v1.external_secrets.metadata[0].name
  }
  data = {
    clientId     = var.infisical_client_id
    clientSecret = var.infisical_client_secret
  }
}

# 3. ExternalSecret che scarica il token GitHub
resource "kubectl_manifest" "github_token_external_secret" {
  yaml_body = <<YAML
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: github-api-token
  namespace: flux-system
spec:
  refreshInterval: 1h
  secretStoreRef:
    kind: ClusterSecretStore
    name: tazlab-secrets
  target:
    name: flux-system # Il nome che Flux si aspetta per il suo segreto di boot
  data:
    - secretKey: password
      remoteRef:
        key: GITHUB_TOKEN
YAML
  depends_on = [helm_release.external_secrets]
}

# 4. Il "Gancio" di sincronizzazione
resource "null_resource" "wait_for_github_token" {
  provisioner "local-exec" {
    command = "kubectl wait --for=condition=Ready externalsecret/github-api-token -n flux-system --timeout=60s"
  }
  depends_on = [kubectl_manifest.github_token_external_secret]
}

Fase 2: Segreti Effimeri e la Guerra ai Processi Zombie
#

Un problema tecnico ricorrente durante i test è stato il blocco dello script create.sh. Invocando ogni comando tramite infisical run, i processi Terragrunt diventavano spesso <defunct> (zombie).

L’Investigazione: L’illusione dell’automazione esterna
#

Ho osservato che in sessioni non interattive, il wrapper della CLI di Infisical faticava a gestire correttamente i segnali di uscita dei processi figli. Il risultato era un bootstrap che si “congelava” senza produrre log, costringendomi a intervenire manualmente.

Ho deciso di eliminare il wrapper. La nuova strategia, battezzata Vault-Native, prevede l’estrazione dei segreti dal vault RAM del TazPod (/home/tazpod/secrets) una sola volta all’inizio dello script.

Il Ragionamento: Perché i file in RAM?
#

I file in una directory montata come tmpfs (RAM) non toccano mai i piatti del disco. Sono protetti dalla cifratura del TazPod e spariscono istantaneamente allo spegnimento o allo smontaggio del vault. Questo mi permette di avere la velocità di un file locale con la sicurezza di un segreto cloud.

# create.sh - Nuova logica di risoluzione
resolve() {
    local var_name=$1
    local vault_file="/home/tazpod/secrets/${2:-$1}"
    if [[ -f "$vault_file" ]]; then
        export "$var_name"=$(cat "$vault_file" | tr -d "'" ")
    else
        # Fallback se il segreto è già in env ma punta a un file
        local val="${!var_name}"
        [[ -f "$val" ]] && export "$var_name"=$(cat "$val" | tr -d "'" ")
    fi
}

resolve "PROXMOX_TOKEN_ID" "proxmox-token-id"
resolve "GITHUB_TOKEN" "github-token"

Fase 3: Ingegneria del Parallelismo (Il “Turbo Flow”)
#

Il bootstrap sequenziale è il nemico della velocità. Nella versione V1, i layer nascevano uno dopo l’altro: secrets -> platform -> engine -> networking -> storage -> gitops.

L’Analisi del Collo di Bottiglia
#

Ho notato che mentre MetalLB (Networking) negoziava gli IP, Flux (GitOps) e Longhorn (Storage) stavano semplicemente “guardando”. Non c’è un motivo tecnico per cui lo storage debba aspettare che il LoadBalancer sia pronto; entrambi hanno bisogno solo che l’API Server del cluster sia vivo.

La Soluzione: Parallelismo spinto
#

Ho slegato le dipendenze in Terragrunt e modificato l’orchestratore per lanciare i tre layer pesanti simultaneamente.

# create.sh - Turbo Acceleration
echo "🚀 [TURBO] Launching Networking, GitOps, and Storage in PARALLEL..."
( cd "$LIVE_DIR/networking" && $TG apply --auto-approve ) &
PID_NET=$!
( cd "$LIVE_DIR/gitops" && $TG apply --auto-approve ) &
PID_GITOPS=$!
( cd "$LIVE_DIR/storage" && $TG apply --auto-approve ) &
PID_STORAGE=$!

wait $PID_NET $PID_GITOPS $PID_STORAGE

Questo cambiamento ha ridotto il tempo di “ferro” di oltre il 30%. Ma la vera sfida era gestire il caos che questo parallelismo introduceva in Kubernetes.


Fase 4: La Trappola dei Percorsi Flux e la Scomposizione Granulare
#

Nel tentativo di rendere tutto più veloce, ho deciso di spezzare il monolite degli operatori di Flux. Invece di un unico blocco infrastructure-operators, ho creato tre unità: core (Traefik/Cert-Manager), data (Postgres) e namespaces.

Lo “Struggle”: Not a Directory
#

Dopo il push, Flux è andato in errore: kustomization.yaml: not a directory. L’analisi del fallimento è stata immediata: Kustomize richiede che ogni risorsa sia una directory contenente un indice. Spostando i file, avevo rotto i riferimenti relativi. Ho dovuto ricostruire la struttura ad albero:

infrastructure/operators/
├── core/
│   └── kustomization.yaml (con ../cert-manager)
├── data/
│   └── kustomization.yaml (con ../postgres-operator)
└── namespaces/
    └── kustomization.yaml

Questo mi ha insegnato che la velocità richiede ordine. La granularità non deve mai sacrificare la struttura logica del repository.


Fase 5: Resilienza Asincrona e il “Fast-Track” del Blog
#

L’ultimo ostacolo era il tempo di attesa delle applicazioni. Perché il Blog Hugo, una semplice immagine Nginx con file statici, doveva aspettare il restore di un database da 10GB?

La Soluzione: InitContainers e RBAC
#

Ho implementato una “corsia preferenziale”. Ho slegato il Blog (apps-static) da ogni dipendenza pesante. Per le app che invece hanno bisogno del database (Mnemosyne, PGAdmin), ho introdotto un InitContainer.

Deep-Dive: InitContainers
#

Un InitContainer è un container specializzato che viene eseguito prima dei container dell’applicazione in un Pod. Deve completarsi con successo prima che il container principale possa partire. È lo strumento perfetto per gestire le dipendenze asincrone.

Invece di far fallire il Pod con un CreateContainerConfigError (perché il segreto della password non esiste ancora), l’InitContainer interroga l’API di Kubernetes:

# apps/base/mnemosyne-mcp/deployment.yaml
initContainers:
  - name: wait-for-db-secret
    image: bitnami/kubectl:latest
    command:
      - /bin/sh
      - -c
      - |
        until kubectl get secret tazlab-db-pguser-mnemosyne; do
          echo "waiting for database user secret..."
          sleep 5
        done

Questo richiede un ServiceAccount con permessi minimi di lettura (get, list) sui segreti, configurato tramite un apposito file rbac.yaml. Il risultato è un cluster che “converge” in modo organico: le parti leggere salgono subito, le parti pesanti si auto-configurano non appena i dati sono pronti.


Risultato Finale: 8 Minuti e 43 Secondi
#

La validazione finale ha prodotto una telemetria impressionante. Siamo passati dagli 11:38 ai 8:43 per avere il Blog online e sicuro.

LayerTempoStato
Secrets (RAM)10sOttimizzato
Platform (Iron)1m 53sStabile
Parallel Layers1m 56sTURBO
GitOps Fast-Track1m 31sRECORD

Totale: 8 minuti e 43 secondi.

Dopo altri 4 minuti, anche il database e l’MCP server erano pronti, completando l’intero stack in meno di 13 minuti totali, includendo il restore dei dati da S3.


Riflessioni Post-Lab: La Bellezza del Determinismo
#

Questo setup non è solo “veloce”. È deterministico. La rimozione di wrapper instabili, la gestione intelligente delle attese e la scomposizione dei componenti hanno trasformato il bootstrap da una sequenza di speranze in un protocollo ingegneristico.

Cosa ho imparato oggi:
#

  1. Meno è Meglio: Rimuovere tool intermedi (come Infisical CLI in esecuzione costante) riduce la superficie d’attacco e i punti di fallimento.
  2. L’Asincronia è Forza: Non costringere il cluster a essere un monolite. Lascia che ogni componente gestisca la propria pazienza.
  3. La Sicurezza accelera: Implementare pratiche enterprise (Machine Identity, RBAC, RAM Vault) ha reso lo script più pulito e, di conseguenza, più veloce da eseguire e facile da debuggare.

L’infrastruttura di TazLab ha raggiunto una nuova soglia di maturità tecnica. Il protocollo di rinascita non è più soltanto un meccanismo di ripristino, ma un sistema ingegneristico ottimizzato per garantire resilienza, sicurezza e precisione assoluta in ogni fase del ciclo di vita del cluster.


Cronaca Tecnica a cura di Taz - HomeLab DevOps & Architect


Comments