Nginx vooraan, een privébackend achter de VPN
De browser praat met Nginx, en Nginx opent een tweede verbinding de tunnel in. Routeer beide, verifieer beide certificaten en houd openbare redirects openbaar.
Volg één verzoek van een telefoon in een trein naar een backend die alleen binnen een privénetwerk bestaat. De telefoon resolvet een openbare naam, verbindt met een openbaar adres op poort 443 en rondt een normale TLS-handshake af. Nginx accepteert die verbinding en opent vervolgens een tweede, volledig aparte verbinding naar een adres dat het alleen via een VPN-interface bereikt. Aan de telefoon verandert niets, en de telefoon komt er nooit achter dat er een tunnel aan te pas kwam.
Eén verzoek door twee verbindingen volgen
Het inkomende been is een openbare HTTP-transactie. Het heeft een openbare naam, een openbaar certificaat en een antwoordpad dat moet werken voor welk adres de bezoeker op dat moment ook heeft. Elke wijziging die dat pad verstoort, breekt alle bezoekers tegelijk. Daarom is het openbare been het been waarop je het minst wilt experimenteren.
Het uitgaande been is privé. Nginx wordt client, resolvet een interne naam of gebruikt een vast adres, en bereikt een dienst die een verbinding vanaf het internet zou weigeren.
Public client -> frontend public address:443 (TLS, public name)
Frontend -> VPN private backend 10.40.0.20:8443 (TLS, private name)
Houd ze gescheiden in je hoofd en de configuratie wordt vanzelf duidelijk. Voeg ze samen en je eindigt met de poging een openbare client aan een tunnel te laten deelnemen, en daar komt het grootste deel van de verwarring in dit ontwerp vandaan. Het privébeen mag kwetsbaar zijn. Het openbare been niet.
Voordat je hier iets van bouwt, bevestig je dat het publiceren van de applicatie de bedoeling is. Een werkende proxy is ook een werkende blootstelling, en het authenticatiemodel moet een beslissing zijn en geen bijeffect.
De gateway-naam moet de tunnel overleven
Er zit een subtiele valkuil in naamresolutie. Als privé-DNS dezelfde naam overschrijft die het VPN-profiel voor zijn eigen gateway gebruikt, kan een herverbinding uiteindelijk proberen die gateway te bereiken via de tunnel die ze juist moet opzetten. De client heeft die naam nodig voordat de tunnel bestaat. Zodra die binnen de tunnel resolvet, kan de verbinding überhaupt nooit worden opgebouwd.
Gebruik afzonderlijke goedgekeurde namen voor de gateway en de backend, en begrens het resolverbeleid zodat elke naam naar het adres resolvet dat de gebruiker ervan nodig heeft. Het wijzigen van de Host-header van een applicatie helpt hier niet, want de VPN-client doet de opzoeking.
Denk daarna aan de reikwijdte. De frontend heeft een route naar het backendadres nodig en moet ook openbare clients blijven beantwoorden. Een volledige tunnel op die host kan aan de eerste eis voldoen en de tweede breken.
Waar het beleid het toestaat, is een tunnel naar een specifieke bestemming het schonere ontwerp. Waar het beleid een volledige tunnel voorschrijft, hoort de frontend thuis in een aparte netwerkcontext in plaats van te worden gepatcht met ad-hocroutes voor bekende bezoekers.
Het serverblok zo schrijven dat beide benen verifiëren
Stel dat de backend een certificaat presenteert voor backend.corp.example en op een privéadres antwoordt. Verbinden met een adres terwijl je een naam verifieert, is precies waar de SNI- en naamopties van de proxymodule voor zijn.
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;
}
}
Elk pad en elk adres hierboven is een plaatsaanduiding. De belangrijke delen zijn de onderdelen die verificatie aan laten staan: een naam om te controleren, een trustbundel om die tegen te controleren en een diepte die bij de goedgekeurde keten past.
proxy_set_header Host app.example.com is bewust. De backend moet zo worden geconfigureerd dat hij die openbare oorsprong accepteert, en hij mag doorgestuurde headers alleen van deze frontend vertrouwen. Het overschrijven van X-Forwarded-For met het clientadres is juist voor één proxy en verkeerd voor een keten, dus een implementatie met meerdere proxies heeft zijn eigen regeling voor vertrouwde adressen nodig.
De openbare oorsprong zich als zodanig laten gedragen
De applicatie moet absolute URL's, redirects en cookies voor de openbare oorsprong uitgeven. Als hij een bezoeker doorstuurt naar backend.corp.example, wordt die bezoeker naar een naam gestuurd die hij niet kan resolven.
Los dat op via de ondersteunde instelling voor externe URL's van de applicatie en een herzien redirectbeleid, niet door hostnamen in antwoordlichamen te herschrijven. Tekenreeksvervanging over willekeurige inhoud breekt ondertekende payloads en veroorzaakt bugs die moeilijk te traceren zijn.
Cookies verdienen dezelfde zorg. Een cookie die aan de privénaam is gebonden, wordt nooit verzonden door een browser die alleen de openbare naam kent.
Bevestig ook dat Nginx de openbare socket echt kan bezitten. Een bestaande webservice op 443 moet verhuizen of de frontend moet elders leven, want twee processen kunnen niet hetzelfde adres en dezelfde poort binden.
Elk been controleren waar het daadwerkelijk draait
Valideer de configuratie met de geïnstalleerde parser vóór elke herlaadactie, zodat een typefout niet de listener omlegt die op dat moment dient.
Test daarna het backendbeen vanaf de frontendhost zelf, in dezelfde netwerkcontext die Nginx gebruikt. Een ping door de tunnel bewijst bereikbaarheid en niets over TLS, autorisatie of de applicatie.
Doe ten slotte een verse openbare aanvraag naar de echte hostnaam met normale certificaatverificatie, en observeer het antwoordpad in plaats van alleen de antwoordbody.
Bewaar beide observaties bij de uitgerolde configuratie en leg vast wat er gebeurt als de tunnel niet beschikbaar is. De frontend hoort een gecontroleerde upstreamfout terug te geven, niet stilletjes terug te vallen op een andere backend die niemand heeft gedocumenteerd. Een 502 met een duidelijke logregel is een goede uitkomst. Een gecachte pagina van een onbedoelde oorsprong is dat niet.