Publicar una aplicación directamente en Internet expone su puerto, simplifica demasiado la frontera de seguridad y obliga al proceso de la aplicación a gestionar TLS, redirecciones y controles HTTP.
NGINX puede actuar como reverse proxy: recibe las conexiones públicas, termina TLS, añade cabeceras de seguridad y reenvía las solicitudes hacia una aplicación accesible únicamente desde el servidor.
El flujo resultante será:
Cliente HTTPS → NGINX:443 → HTTP local → Aplicación:3000
La aplicación sigue trabajando con HTTP dentro del host, mientras NGINX administra la conexión pública.
¿Cuándo conviene utilizar este enfoque?
Resulta especialmente útil para:
- Aplicaciones Node.js, Python, PHP o Go detrás de un puerto local.
- Paneles internos publicados mediante un dominio.
- Aplicaciones que necesitan HTTPS sin implementar TLS directamente.
- Servicios que requieren redirecciones, límites de solicitudes o cabeceras comunes.
- Despliegues donde se desea renovar certificados sin reiniciar la aplicación.
Arquitectura de seguridad
NGINX será el único componente expuesto mediante los puertos 80 y 443. La aplicación escuchará exclusivamente en 127.0.0.1:3000.
Esto evita que un cliente pueda omitir NGINX y conectarse directamente con la aplicación, saltándose TLS, cabeceras, controles de acceso o rate limiting.
Ver la Imagen 1: arquitectura del reverse proxy y terminación TLS.
Requisitos previos
Antes de comenzar, verifica que dispones de:
- Un servidor Linux con NGINX instalado.
- Una aplicación operativa en
127.0.0.1:3000. - Un dominio, por ejemplo
app.ejemplo.com. - Registros DNS A y, si corresponde, AAAA apuntando al servidor.
- Puertos TCP 80 y 443 permitidos en el firewall local y perimetral.
- Certbot y su plugin para NGINX.
- Acceso con privilegios administrativos.
El puerto 80 debe permanecer accesible si Certbot utiliza el desafío HTTP-01. Si existe un registro AAAA, también debe dirigir el tráfico IPv6 hacia el servidor correcto.
Paso 1. Verificar la aplicación y los puertos
Comprueba dónde escucha la aplicación:
sudo ss -ltnp | grep -E ':(80|443|3000)\b'El resultado esperado para el backend debe mostrar 127.0.0.1:3000 o [::1]:3000, no 0.0.0.0:3000.
Prueba el backend directamente:
curl -fsS -o /dev/null \
-w 'Backend HTTP: %{http_code}\n' \
http://127.0.0.1:3000/Si esta solicitud falla, corrige primero la aplicación. NGINX no puede solucionar un backend detenido, un puerto incorrecto o una aplicación que todavía no responde.
Asegúrate también de que TCP 3000 no esté permitido en el firewall externo.
Paso 2. Verificar el DNS
Comprueba los registros publicados:
dig +short A app.ejemplo.com
dig +short AAAA app.ejemplo.comLas direcciones deben corresponder al servidor. Si acabas de modificar el DNS, espera la propagación definida por el TTL antes de solicitar el certificado.
Comprueba desde una red externa que el puerto 80 sea alcanzable. Una prueba ejecutada únicamente desde el propio servidor no detecta bloqueos en firewalls perimetrales, NAT o reglas del proveedor.
Paso 3. Crear el virtual host HTTP
En Debian y Ubuntu suele utilizarse:
/etc/nginx/sites-available/app.ejemplo.comEn distribuciones como RHEL, Rocky Linux o AlmaLinux es habitual utilizar:
/etc/nginx/conf.d/app.ejemplo.com.confCrea inicialmente un virtual host HTTP:
server {
listen 80;
listen [::]:80;
server_name app.ejemplo.com;
access_log /var/log/nginx/app.ejemplo.com.access.log;
error_log /var/log/nginx/app.ejemplo.com.error.log;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 5s;
proxy_read_timeout 60s;
}
}proxy_pass define el backend. Las cabeceras X-Forwarded-* permiten que la aplicación conozca el host, la IP original y el protocolo utilizado por el cliente. La sintaxis y el comportamiento de estas directivas están documentados en el módulo proxy oficial de NGINX.
Los timeouts deben ajustarse al comportamiento real de la aplicación. Procesos largos, streaming o cargas grandes pueden necesitar valores diferentes.
En Debian o Ubuntu, habilita el sitio si la instalación utiliza sites-enabled:
sudo ln -s /etc/nginx/sites-available/app.ejemplo.com \
/etc/nginx/sites-enabled/app.ejemplo.comNo ejecutes este comando si el enlace ya existe o si tu distribución carga directamente los archivos de conf.d.
Paso 4. Validar el proxy HTTP
Valida toda la configuración antes de recargar:
sudo nginx -tSolo si el resultado es correcto:
sudo systemctl reload nginxNGINX comprueba la nueva configuración durante la recarga y mantiene los workers anteriores si no puede aplicarla. Las conexiones existentes pueden terminar de forma controlada. Documentación oficial sobre recargas.
Prueba el virtual host localmente:
curl -I -H 'Host: app.ejemplo.com' http://127.0.0.1/Después prueba mediante el dominio:
curl -I http://app.ejemplo.com/No continúes con TLS hasta que el proxy HTTP responda correctamente.
Paso 5. Comprobar el plugin de Certbot
Verifica que Certbot reconozca el instalador de NGINX:
sudo certbot pluginsLa salida debe incluir el plugin nginx. Si no aparece, instala el paquete correspondiente siguiendo las instrucciones de Certbot para tu distribución.
Antes de permitir que Certbot modifique la configuración, conserva una copia versionada o un respaldo de /etc/nginx.
Paso 6. Emitir e instalar el certificado
Solicita el certificado:
sudo certbot --nginx -d app.ejemplo.comCertbot validará el dominio, solicitará el certificado e incorporará las directivas TLS en el virtual host. Si pregunta por la redirección, selecciona la opción que redirige HTTP hacia HTTPS.
Una vez finalizado:
sudo nginx -t
sudo systemctl reload nginx
sudo certbot certificatesComprueba el acceso:
curl -I https://app.ejemplo.com/
curl -I http://app.ejemplo.com/La primera solicitud debe responder mediante HTTPS. La segunda debe devolver una redirección permanente hacia la URL segura.
Evita copiar certificados o claves privadas fuera de /etc/letsencrypt. NGINX debe referenciar los archivos administrados por Certbot.
Paso 7. Configurar cabeceras de seguridad
Añade las cabeceras dentro del bloque HTTPS creado o actualizado por Certbot:
add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "DENY" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;Estas cabeceras:
- Evitan determinadas interpretaciones incorrectas del tipo MIME.
- Impiden cargar el sitio dentro de un frame.
- Limitan la información enviada en el encabezado
Referer. - Deshabilitan APIs del navegador que la aplicación no utiliza.
Si la aplicación debe mostrarse dentro de un iframe autorizado o necesita cámara, micrófono o geolocalización, ajusta las políticas antes de aplicarlas.
El parámetro always hace que NGINX incluya la cabecera también en respuestas de error. Revisa además la herencia: si un bloque hijo contiene otra directiva add_header, puede cambiar qué cabeceras recibe esa ubicación.
Paso 8. Probar CSP antes de aplicarla
La política del borrador:
Content-Security-Policy: default-src 'self'puede bloquear scripts inline, estilos, fuentes, WebSockets, APIs o recursos alojados en otros dominios.
Empieza en modo de observación:
add_header Content-Security-Policy-Report-Only
"default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
always;En este modo el navegador registra infracciones sin bloquear los recursos. Para recolectarlas centralmente debes configurar Reporting-Endpoints y report-to; durante una revisión manual también pueden inspeccionarse en las herramientas de desarrollo del navegador. Guía de CSP de MDN.
Después de identificar y autorizar los orígenes necesarios, reemplaza la cabecera por una política aplicada:
add_header Content-Security-Policy
"default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'"
always;No habilites simultáneamente ambas versiones por error: la cabecera normal se aplica aunque también exista una política Report-Only.
Paso 9. Activar HSTS con precaución
Cuando HTTPS y la renovación estén verificados, puedes agregar:
add_header Strict-Transport-Security "max-age=31536000" always;HSTS indica al navegador que debe utilizar HTTPS durante el período configurado. No agregues includeSubDomains ni preload hasta confirmar que todos los subdominios funcionan permanentemente con HTTPS; una configuración incorrecta puede volverlos inaccesibles para usuarios que ya recibieron la cabecera. Referencia de HSTS en MDN.
HSTS debe enviarse únicamente desde el virtual host HTTPS.
Paso 10. Validar y aplicar el endurecimiento
Comprueba la configuración:
sudo nginx -tSi es válida:
sudo systemctl reload nginxVerifica las cabeceras:
curl -sSI https://app.ejemplo.com/Comprueba específicamente:
curl -sSI https://app.ejemplo.com/ |
grep -iE 'strict-transport|content-security|x-frame|content-type|referrer|permissions'Ver la Imagen 2: secuencia segura de despliegue.
Paso 11. Verificar la renovación automática
Ejecuta una renovación de prueba:
sudo certbot renew --dry-runConsulta el mecanismo programado:
systemctl list-timers --all | grep -i certbotSegún el método de instalación, la renovación puede usar un timer de systemd, cron u otro planificador. Certbot conserva las opciones utilizadas al emitir el certificado y renew --dry-run permite probarlas contra el entorno de staging. Documentación oficial de renovación.
Comprueba también las fechas publicadas:
echo |
openssl s_client \
-connect app.ejemplo.com:443 \
-servername app.ejemplo.com 2>/dev/null |
openssl x509 -noout -issuer -subject -datesMonitoriza la fecha de expiración real. Ejecutar periódicamente renew --dry-run no sustituye una alerta de vencimiento.
Confianza en las cabeceras del proxy
La aplicación debe estar configurada para confiar únicamente en el proxy esperado. De lo contrario, podría ignorar X-Forwarded-Proto y generar redirecciones HTTP, o confiar en cabeceras falsificadas.
La configuración depende del framework:
- Express utiliza
trust proxy. - Django puede utilizar
SECURE_PROXY_SSL_HEADER. - Flask puede utilizar
ProxyFix. - Otros frameworks tienen opciones equivalentes.
No habilites una confianza global sin revisar cuántos proxies existen y desde qué direcciones pueden conectarse. Mantener la aplicación en loopback reduce este riesgo porque NGINX se convierte en su único origen de red.
Soporte para WebSocket
Si la aplicación utiliza WebSocket, añade en el contexto http:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}Y dentro de location /:
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;La directiva map no debe colocarse dentro de server ni de location.
Problemas frecuentes
Certbot no puede validar el dominio
Comprueba:
- Registros A y AAAA.
- Propagación DNS.
- Acceso externo a TCP 80.
- NAT y firewalls del proveedor.
- Que NGINX esté ejecutándose.
- Que
server_namecoincida exactamente. - Que un CDN o proxy DNS no esté interfiriendo.
NGINX devuelve 502 Bad Gateway
Verifica:
curl -v http://127.0.0.1:3000/
sudo ss -ltnp | grep ':3000'
sudo tail -n 100 /var/log/nginx/app.ejemplo.com.error.logLas causas habituales son una aplicación detenida, un puerto incorrecto, uso de IPv4 frente a IPv6, timeouts o controles obligatorios del sistema operativo.
La aplicación genera enlaces HTTP
Comprueba X-Forwarded-Proto y la configuración de confianza del framework. Un proxy configurado correctamente no garantiza que la aplicación interprete las cabeceras.
Aparece un bucle de redirecciones
Revisa si NGINX y la aplicación fuerzan HTTPS simultáneamente sin reconocer el protocolo original. Confirma que la aplicación confíe en el proxy antes de redirigir.
Las cabeceras no aparecen
Busca bloques location con sus propias directivas add_header, verifica que los cambios estén dentro del virtual host HTTPS y confirma que la configuración se haya recargado.
CSP rompe estilos o scripts
Vuelve temporalmente a Content-Security-Policy-Report-Only, inspecciona las infracciones y autoriza únicamente los orígenes necesarios. Evita resolverlo con comodines amplios o con 'unsafe-inline' sin evaluar alternativas como nonces o hashes.

Buenas prácticas para producción
- Expón únicamente 80 y 443.
- Mantén el backend en loopback o en una red privada.
- Valida con
nginx -tantes de cada recarga. - Versiona los virtual hosts sin incluir claves privadas.
- Prueba las renovaciones y monitoriza la expiración.
- Despliega CSP inicialmente en modo Report-Only.
- Activa HSTS únicamente cuando HTTPS sea estable.
- Limita el tamaño de solicitudes y los timeouts según la aplicación.
- Aplica rate limiting medido en endpoints sensibles.
- Revisa access logs, error logs y códigos
4xxy5xx. - Mantén NGINX, Certbot y el sistema operativo actualizados.
Conclusión
NGINX permite centralizar TLS y los controles HTTP sin modificar la aplicación, pero la seguridad depende de mantener una frontera clara: solo NGINX debe ser público y el backend debe permanecer en una interfaz local o privada.
El procedimiento recomendado es: comprobar → configurar HTTP → validar → activar TLS → endurecer → comprobar nuevamente.