Nginx davanti, backend privato dietro la VPN
Il browser parla con Nginx e Nginx apre una seconda connessione nel tunnel. Instrada entrambe, verifica entrambi i certificati e mantieni pubblici i redirect pubblici.
Segui una richiesta da un telefono su un treno fino a un backend che esiste solo dentro una rete privata. Il telefono risolve un nome pubblico, si connette a un indirizzo pubblico sulla porta 443 e completa un handshake TLS normale. Nginx accetta quella connessione e poi ne apre una seconda, del tutto separata, verso un indirizzo che raggiunge solo tramite un’interfaccia VPN. Nulla del telefono cambia, e il telefono non scopre mai che c’è stato un tunnel.
Seguire una richiesta attraverso due connessioni
Il tratto in ingresso è una transazione HTTP pubblica. Ha un nome pubblico, un certificato pubblico e un percorso di risposta che deve funzionare per qualunque indirizzo abbia il visitatore in quel momento. Qualsiasi modifica che alteri quel percorso rompe tutti i visitatori in una volta. Per questo il tratto pubblico è quello su cui conviene sperimentare meno.
Il tratto in uscita è privato. Nginx diventa client, risolve un nome interno o usa un indirizzo fisso e raggiunge un servizio che rifiuterebbe una connessione da Internet.
Public client -> frontend public address:443 (TLS, public name)
Frontend -> VPN private backend 10.40.0.20:8443 (TLS, private name)
Tienili separati in testa e la configurazione diventa ovvia. Uniscili e finirai per provare a far partecipare un client pubblico a un tunnel, ed è da lì che viene gran parte della confusione di questo progetto. Il tratto privato può essere fragile. Quello pubblico no.
Prima di costruire qualcosa, conferma che pubblicare l’applicazione sia voluto. Un proxy funzionante è anche un’esposizione funzionante, e il modello di autenticazione deve essere una decisione, non un effetto collaterale.
Il nome del gateway deve sopravvivere al tunnel
C’è una trappola sottile nella risoluzione dei nomi. Se il DNS privato sovrascrive lo stesso nome che il profilo VPN usa per il proprio gateway, una riconnessione può finire per tentare di raggiungere quel gateway attraverso il tunnel che dovrebbe stabilire. Il client ha bisogno che quel nome sia risolto prima che il tunnel esista. Una volta che si risolve dentro il tunnel, la connessione non può mai essere costruita.
Usa nomi approvati distinti per gateway e backend, e limita la politica del resolver in modo che ogni nome si risolva nell’indirizzo che serve al suo consumatore. Cambiare l’header Host di un’applicazione non aiuta qui, perché è il client VPN a fare la risoluzione.
Poi pensa all’ambito. Il frontend ha bisogno di una rotta verso l’indirizzo del backend e deve anche continuare a rispondere ai client pubblici. Un tunnel completo su quell’host soddisfa il primo requisito e rompe il secondo.
Dove la politica lo consente, un tunnel verso una destinazione specifica è il progetto più pulito. Dove la politica impone un tunnel completo, il frontend appartiene a un contesto di rete separato invece di essere rattoppato con rotte ad hoc per visitatori noti.
Scrivere il blocco server in modo che entrambi i tratti verifichino
Supponi che il backend presenti un certificato per backend.corp.example e risponda su un indirizzo privato. Connettersi a un indirizzo verificando un nome è esattamente lo scopo delle opzioni SNI e di nome del modulo proxy.
upstream private_app {
server 10.40.0.20:8443;
}
server {
listen 443 ssl;
server_name app.example.com;
ssl_certificate /etc/nginx/tls/app-fullchain.pem;
ssl_certificate_key /etc/nginx/tls/app-key.pem;
location / {
proxy_pass https://private_app;
proxy_ssl_server_name on;
proxy_ssl_name backend.corp.example;
proxy_ssl_trusted_certificate /etc/nginx/trust/backend-ca.pem;
proxy_ssl_verify on;
proxy_ssl_verify_depth 3;
proxy_set_header Host app.example.com;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $remote_addr;
}
}
Ogni percorso e indirizzo sopra è un segnaposto. Le parti importanti sono quelle che tengono attiva la verifica: un nome da controllare, un bundle di fiducia con cui controllarlo e una profondità che corrisponda alla catena approvata.
proxy_set_header Host app.example.com è voluto. Il backend dovrebbe essere configurato per accettare quell’origine pubblica e dovrebbe fidarsi degli header inoltrati solo da questo frontend. Sovrascrivere X-Forwarded-For con l’indirizzo del client è giusto per un singolo proxy e sbagliato per una catena, quindi un deployment con più proxy ha bisogno della propria disposizione di indirizzi fidati.
Far comportare l’origine pubblica come tale
L’applicazione dovrebbe emettere URL assoluti, redirect e cookie per l’origine pubblica. Se reindirizza un visitatore a backend.corp.example, quel visitatore viene mandato a un nome che non può risolvere.
Risolvi la cosa con l’impostazione di URL esterno supportata dall’applicazione e una politica di redirect rivista, non riscrivendo i nomi host nei corpi delle risposte. La sostituzione di stringhe su contenuti arbitrari rompe i payload firmati e crea bug difficili da tracciare.
I cookie meritano la stessa cura. Un cookie limitato al nome privato non verrà mai inviato da un browser che conosce solo quello pubblico.
Conferma anche che Nginx possa davvero possedere il socket pubblico. Un servizio web esistente sulla 443 deve spostarsi, oppure il frontend deve stare altrove, perché due processi non possono legare lo stesso indirizzo e la stessa porta.
Verificare ogni tratto dove viene realmente eseguito
Valida la configurazione con il parser installato prima di qualsiasi reload, così un refuso non abbatta il listener che sta servendo in quel momento.
Poi prova il tratto backend dall’host frontend stesso, nello stesso contesto di rete usato da Nginx. Un ping attraverso il tunnel prova la raggiungibilità e nulla su TLS, autorizzazione o applicazione.
Infine fai una nuova richiesta pubblica al nome host reale con verifica normale del certificato, e osserva il percorso di risposta e non solo il corpo.
Conserva entrambe le osservazioni insieme alla configurazione distribuita e annota cosa succede quando il tunnel non è disponibile. Il frontend dovrebbe restituire un errore upstream controllato, non ripiegare in silenzio su un altro backend che nessuno ha documentato. Un 502 con una riga di log chiara è un buon esito. Una pagina in cache da un’origine non voluta non lo è.