Il tuo portatile mostra un tunnel verde, il log del client riporta che le rotte sono state spinte, e l’unico indirizzo che risponde è l’IP del tunnel del server VPN stesso. I ping oltre muoiono qualunque cosa tu prenda di mira, che sia un ClusterIP, l’indirizzo di un altro pod o un nome host pubblico. Quella combinazione è diagnostica, non vaga: la decapsulazione funziona, e il secondo salto no.

L’ho incontrato su un nodo Debian che eseguiva un’immagine OpenVPN installata tramite chart, e il tunnel era la parte facile. Il client ha preso una lease 10.8.0.x, 10.8.0.1 ha risposto, e 10.96.0.10, il servizio DNS del cluster, è andato in timeout. Nulla nel log del server sembrava malato, ed è proprio per questo che le persone iniziano a riscrivere regole iptables che non hanno mai avuto bisogno di toccare.

Un pod VPN è un router, e vale la pena dirlo chiaramente. I pacchetti cifrati arrivano, vengono scartati nel namespace di rete del pod stesso, e poi devono uscire da un’altra interfaccia. Linux rifiuta quel secondo passo a meno che l’inoltro sia abilitato dentro quello stesso namespace.

Cosa il client può e non può raggiungere

L’indirizzo che risponde appartiene all’endpoint del tunnel, e il processo VPN lo possiede localmente, quindi l’inoltro non c’entra affatto. Tutto il resto, compreso un altro pod, un VIP di servizio o Internet, richiede che il kernel sposti un pacchetto da tun0 a eth0. Quando quello spostamento viene rifiutato, il sintomo è un silenzio uniforme e non un rifiuto, perché nulla genera un errore ICMP lungo il tunnel. Ottieni timeout, non messaggi di irraggiungibilità, ed è l’indizio che nessuno sta instradando affatto.

Due minuti di classificazione restringono lo strato. Se i nomi DNS falliscono mentre i ping grezzi a un IP di servizio riescono, il problema è l’idraulica DNS, non l’inoltro. Se ogni destinazione oltre l’indirizzo del peer va in timeout, compreso un IP nudo, parti dal namespace.

Leggi l’impostazione dentro il namespace

Ogni pod riceve il proprio namespace di rete, e net.ipv4.ip_forward è un sysctl per namespace. Cambiarlo sul nodo non ti dice nulla sul container, ed è un modo affidabile di passare un pomeriggio sulla macchina sbagliata.

kubectl exec -it deploy/openvpn -- sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 0

Uno zero conferma la diagnosi. Il valore non viene nemmeno ereditato dalla configurazione di runtime del nodo: parte dal valore predefinito dell’immagine ogni volta che viene creato un container. Una correzione applicata a mano con kubectl exec ... sysctl -w sparisce quindi al riavvio successivo e torna come un guasto che sembra intermittente.

Abilitarlo perché il manifest lo conservi

L’opzione durevole è un container init che gira nello stesso pod sandbox e scrive il valore prima che parta il container VPN. I container di un pod condividono un namespace di rete, quindi l’impostazione passa al container principale.

initContainers:
- name: enable-forwarding
  image: busybox:1.36
  command: ["sysctl", "-w", "net.ipv4.ip_forward=1"]
  securityContext:
    privileged: true

Proprio privileged: true è la riga a cui i revisori si oppongono, e hanno ragione. Scrivere un sysctl richiede privilegio o una capability strettamente limitata, e un cluster che applica il Pod Security Standard ristretto può rifiutare del tutto un container init privilegiato. Se l’admission rifiuta il pod, lo scopri al momento della creazione, il che almeno è onesto e facile da leggere.

L’alternativa più pulita è la lista securityContext.sysctls a livello di pod, che consente al kubelet di applicare sysctl con namespace senza concedere privilegi a nessun container. Copre solo i nomi nella lista consentita del cluster. Che net.ipv4.ip_forward sia in quella lista dipende da come è stato configurato il kubelet, e un pod che chiede un sysctl fuori da essa viene rifiutato all’admission o resta bloccato in CreateContainerConfigError mentre il sandbox viene ritentato. Controlla la lista consentita prima di supporre che il campo sia rotto.

Prima di costruire a mano uno dei due approcci, guarda se il chart espone già l’interruttore, spesso come qualcosa tipo ipForwardInitContainer: true. Usare il valore del chart tiene piccolo il tuo file di override e sopravvive al prossimo aggiornamento, cosa che le modifiche al Deployment patchate a mano non fanno.

La seconda metà: percorso di ritorno e policy

L’inoltro attivo è necessario, non sufficiente. Se i client usano un pool privato per cui la rete del cluster non ha rotta, le risposte non trovano la via del ritorno nel tunnel, e il pod deve mascherare quel traffico verso la sua destinazione.

La policy si appoggia sopra. Gli oggetti NetworkPolicy e le regole del firewall cloud vengono valutati contro l’indirizzo sorgente che un pacchetto porta dopo la traduzione, quindi una regola scritta per il pool dei client può smettere in silenzio di corrispondere una volta attivato il mascheramento.

Quando i client devono raggiungere solo servizi dentro il cluster, far terminare la VPN sul VIP del servizio è un progetto più piccolo che instradare nella rete dei pod. Tiene anche il pool dei client completamente fuori dalla tabella di routing del CNI.

Isolare il salto che fallisce dal client

Lavora verso l’esterno e fermati al primo passo silenzioso. Inizia dal peer VPN, poi prova l’indirizzo di un pod, poi un VIP di servizio, e solo dopo una destinazione pubblica. Ogni successo sposta il confine di un salto, e ogni fallimento nomina lo strato che vale la pena ispezionare. Dopo qualsiasi rilascio, entra con exec e rileggi il sysctl, perché un pod sostituito riparte dal valore predefinito dell’immagine e una correzione che non è mai stata nel manifest è già sparita.

La documentazione Kubernetes su l’uso dei sysctl in un cluster elenca quali nomi possono essere impostati tramite il contesto di sicurezza del pod, ed è il primo posto da guardare quando l’admission rifiuta la modifica.

Articoli correlati