Nginx delante, backend privado detrás de la VPN
El navegador habla con Nginx y Nginx abre una segunda conexión hacia el túnel. Enrute ambos, verifique ambos certificados y mantenga públicas las redirecciones públicas.
Siga una petición desde un teléfono en un tren hasta un backend que solo existe dentro de una red privada. El teléfono resuelve un nombre público, se conecta a una dirección pública en el puerto 443 y completa un handshake TLS normal. Nginx acepta esa conexión y luego abre una segunda, completamente separada, hacia una dirección a la que solo llega a través de una interfaz VPN. Nada del teléfono cambia, y el teléfono nunca se entera de que hubo un túnel.
Seguir una petición a través de dos conexiones
El tramo entrante es una transacción HTTP pública. Tiene un nombre público, un certificado público y una ruta de respuesta que debe funcionar para la dirección que el visitante tenga en ese momento. Cualquier cambio que altere esa ruta rompe a todos los visitantes a la vez. Por eso el tramo público es el que menos conviene experimentar.
El tramo saliente es privado. Nginx pasa a ser cliente, resuelve un nombre interno o usa una dirección fija, y llega a un servicio que rechazaría una conexión desde internet.
Public client -> frontend public address:443 (TLS, public name)
Frontend -> VPN private backend 10.40.0.20:8443 (TLS, private name)
Manténgalos separados en la cabeza y la configuración se vuelve obvia. Mézclelos y acabará intentando que un cliente público participe en un túnel, que es de donde viene la mayor parte de la confusión de este diseño. El tramo privado puede ser frágil. El público no.
Antes de construir nada de esto, confirme que publicar la aplicación es intencionado. Un proxy que funciona también es una exposición que funciona, y el modelo de autenticación tiene que ser una decisión y no un efecto secundario.
El nombre de la puerta de enlace tiene que sobrevivir al túnel
Hay una trampa sutil en la resolución de nombres. Si el DNS privado sobrescribe el mismo nombre que el perfil de VPN usa para su propia puerta de enlace, una reconexión puede acabar intentando llegar a esa puerta de enlace a través del túnel que se supone que debe establecer. El cliente necesita ese nombre resuelto antes de que exista el túnel. En cuanto se resuelve dentro del túnel, la conexión no se puede construir en primer lugar.
Use nombres aprobados distintos para la puerta de enlace y el backend, y limite la política del resolver para que cada nombre resuelva a la dirección que necesita su consumidor. Cambiar la cabecera Host de una aplicación no ayuda con este problema, porque quien hace la consulta es el cliente de VPN.
Después piense en el alcance. El frontend necesita una ruta a la dirección del backend y además debe seguir atendiendo a clientes públicos. Un túnel completo en ese host satisface el primer requisito y rompe el segundo.
Donde la política lo permita, un túnel a un destino concreto es el diseño más limpio. Donde la política imponga un túnel completo, el frontend pertenece a un contexto de red aparte en vez de parchearse con rutas ad hoc para visitantes conocidos.
Escribir el bloque de servidor para que ambos tramos verifiquen
Suponga que el backend presenta un certificado para backend.corp.example y responde en una dirección privada. Conectarse a una dirección mientras se verifica un nombre es exactamente para lo que están las opciones SNI y de nombre del módulo de 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;
}
}
Cada ruta y dirección de arriba es un marcador de posición. Las partes importantes son las que mantienen la verificación activada: un nombre que comprobar, un paquete de confianza contra el que comprobarlo y una profundidad que coincida con la cadena aprobada.
proxy_set_header Host app.example.com es deliberado. El backend debería configurarse para aceptar ese origen público, y debería confiar en las cabeceras reenviadas solo desde este frontend. Sobrescribir X-Forwarded-For con la dirección del cliente es correcto para un solo proxy y erróneo para una cadena, así que un despliegue con varios proxies necesita su propia disposición de direcciones de confianza.
Hacer que el origen público se comporte como tal
La aplicación debería emitir URL absolutas, redirecciones y cookies para el origen público. Si redirige a un visitante a backend.corp.example, ese visitante va a un nombre que no puede resolver.
Arregle eso mediante el ajuste de URL externa que la aplicación soporte y una política de redirección revisada, no reescribiendo nombres de host en los cuerpos de respuesta. La sustitución de cadenas en contenido arbitrario rompe cargas firmadas y crea errores difíciles de rastrear.
Las cookies merecen el mismo cuidado. Una cookie limitada al nombre privado nunca la enviará un navegador que solo conoce el público.
Confirme también que Nginx puede realmente ser dueño del socket público. Un servicio web existente en 443 tiene que moverse o el frontend tiene que vivir en otro sitio, porque dos procesos no pueden enlazar la misma dirección y el mismo puerto.
Comprobar cada tramo donde realmente corre
Valide la configuración con el analizador instalado antes de cualquier recarga, para que una errata no tumbe al listener que está sirviendo ahora mismo.
Después pruebe el tramo del backend desde el propio host del frontend, en el mismo contexto de red que usa Nginx. Un ping a través del túnel demuestra alcanzabilidad y nada sobre TLS, autorización o la aplicación.
Por último haga una petición pública nueva al nombre de host real con verificación de certificado normal, y observe la ruta de respuesta en lugar de solo el cuerpo.
Guarde ambas observaciones junto con la configuración desplegada y anote qué ocurre cuando el túnel no está disponible. El frontend debería devolver un error de upstream controlado, no recurrir en silencio a otro backend que nadie documentó. Un 502 con una línea de log clara es un buen resultado. Una página en caché de un origen no deseado no lo es.