Metti un client VPN in un container e ottieni un unico spazio dei nomi con un tunnel al suo interno, non un host che instrada tutto il suo traffico attraverso una VPN. Sono progetti separati, e trattarli come uno solo è il modo in cui i container finiscono privilegiati per errore. Questa guida copre solo il primo: un’immagine verificata, un profilo protetto e un processo client confinato nel proprio spazio dei nomi di rete.

Parti da un artefatto che qualcuno ha già approvato. I passaggi seguenti presuppongono un’immagine che contiene l’eseguibile openvpn e un profilo client completo con certificato e materiale della chiave già risolti all’interno del mount. Non costruiscono software, non emettono credenziali e non coprono ambienti senza root né container Windows nativi, ciascuno dei quali cambia la risposta.

Cosa riceve il container e cosa no

Tieni il profilo e tutto ciò a cui fa riferimento in una directory locale con permessi limitati, montata in sola lettura. I percorsi relativi dentro il profilo devono risolversi all’interno di quel mount; è un dettaglio che le persone trascurano quando un certificato si trova una directory sopra il profilo e il container non lo vede mai.

services:
  vpn:
    image: ${VPN_CLIENT_IMAGE:?set the reviewed image reference}
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    volumes:
      - ./vpn:/vpn:ro
    working_dir: /vpn
    entrypoint: ["openvpn"]
    command: ["--config", "/vpn/client.ovpn"]

Quel file concede tre cose e non chiede nient’altro. Il riferimento dell’immagine dovrebbe essere fissato tramite digest se la tua politica di distribuzione lo prevede, e il profilo dovrebbe essere quello verificato, non una copia che qualcuno ha modificato sul posto sull’host.

Sola lettura qui significa che il container non può modificare i tuoi file sull’host; non significa che non possa leggere la chiave privata che hai montato. È tutto il senso del mount, ed è anche per questo che i permessi dei file, la proprietà e il modo in cui il segreto è arrivato sull’host contano ancora. Non passare mai una password sulla riga di comando e non incorporarla in uno strato dell’immagine, perché entrambe sopravvivono in luoghi che non potrai ripulire dopo.

Capacità, entrypoint e la trappola tra le due

Servono due impostazioni e svolgono lavori non correlati. NET_ADMIN consente la manipolazione di interfacce e rotte all’interno dello spazio dei nomi del container. La voce di dispositivo /dev/net/tun consente al processo di aprire il dispositivo del tunnel. Concederne una senza l’altra produce un errore chiaro, il che è comodo: un dispositivo TUN mancante fallisce all’apertura, mentre una capacità mancante fallisce quando il client prova a configurare l’interfaccia.

Nessuna delle due giustifica privileged: true. Un container privilegiato riceve tutte le capacità più l’accesso ai dispositivi, ed è una concessione molto più ampia di quanto richieda questo carico di lavoro. La documentazione su privilegi e capacità di esecuzione spiega fin dove arriva quella concessione. Se un chart o un post di blog lo chiede, verifica prima se il requisito non fosse in realtà un percorso di dispositivo mancante o una capacità mancante.

La sostituzione di entrypoint è la parte che morde. Invocare openvpn direttamente è corretto per un’immagine comune e sbagliato per qualsiasi immagine di prodotto il cui script di avvio prepara regole del firewall, file del resolver o credenziali prima di eseguire il demone. Sostituire quello script salta la preparazione e ti lascia a fare debug di un tunnel che è salito in uno spazio dei nomi mezzo configurato.

Esegui il client in primo piano e lascia che sia il processo principale del container. Un processo che si demonizza esce subito, il container si riavvia in un ciclo, e i log che ti servono sono sparsi tra riavvii che sembrano tutti uguali.

Verifica dall’interno dello spazio dei nomi, non dall’host

Alza lo stack, conferma nel log che il client ha raggiunto uno stato connesso, poi ispeziona lo spazio dei nomi che contiene davvero il tunnel.

docker compose up -d
docker compose logs --tail 40 vpn
docker compose exec vpn ip -br addr
docker compose exec vpn ip route

L’elenco degli indirizzi dovrebbe mostrare un’interfaccia tunnel con un indirizzo dal pool, e la tabella delle rotte dovrebbe mostrare le destinazioni inviate che puntano a essa. Se l’interfaccia manca, il client non è mai arrivato a crearla. Se esiste ma nessuna rotta la usa, il posto da guardare sono il push o le opzioni di rotta.

Poi prova la destinazione che ti interessa, dallo spazio dei nomi che dovrebbe usare il tunnel, con l’applicazione e non solo con un ping. La risoluzione dei nomi è un guasto a sé che vale la pena testare esplicitamente: un indirizzo DNS inviato non aggiorna da solo il resolver di un container, e un client autonomo che ignora il push risolverà i nomi attraverso ciò che l’immagine ha configurato.

Lo stato disconnesso fa parte del contratto

Il comportamento di riconnessione richiede un proprio controllo, perché una politica di riavvio dimostra che il processo si riavvia e non dice nulla su cosa fa il traffico nel frattempo. Ferma il tunnel, prova la destinazione prevista e osserva se la connessione viene rifiutata, si blocca o riesce silenziosamente attraverso la rotta ordinaria del container. L’ultimo esito è quello che conta, ed è invisibile in un log sano.

Se il carico di lavoro non deve mai usare il percorso ordinario, progettalo esplicitamente con regole del firewall e un segnale di prontezza dentro lo spazio dei nomi. Un kill switch è una politica che scrivi e verifichi, non una proprietà che erediti eseguendo un client in un container.

Infine, ricorda il confine: queste rotte vivono nello spazio dei nomi del container e non sostituiscono la tabella di routing dell’host. Le regole di inoltro, traduzione e firewall di Docker restano parte del percorso esterno, e una porta pubblicata non è un’interfaccia VPN.

Articoli correlati