← Volver al blog
·9 min de lectura

Configurar Nginx como reverse proxy con SSL para aplicaciones Node.js

DevOpsNginxNode.js

Por que Nginx frente a Node.js

Node.js puede servir HTTP directamente, y frameworks como Express o Fastify lo hacen trivial. Entonces, por que agregar otra capa? La respuesta corta: Node.js es excelente ejecutando logica de aplicacion, pero no esta optimizado para las tareas que Nginx resuelve de forma nativa.

Servir archivos estaticos es el caso mas evidente. Nginx maneja archivos estaticos con sendfile(), una llamada al sistema que transfiere datos directamente del disco al socket de red sin pasar por el espacio de usuario. Node.js, en cambio, lee el archivo a un buffer en memoria y luego lo escribe al socket. Para una imagen de 2 MB servida a 1000 usuarios concurrentes, la diferencia en uso de memoria es significativa.

SSL termination es otro punto critico. Nginx procesa el handshake TLS y la encriptacion/desencriptacion en C compilado, optimizado para cada arquitectura de CPU. Esto libera al event loop de Node.js para que se dedique exclusivamente a la logica de negocio.

Ademas, Nginx aporta:

  • Balanceo de carga entre multiples instancias de Node.js (utiles si usas PM2 en modo cluster)
  • Rate limiting para proteger contra abuso sin consumir recursos de tu aplicacion
  • Security headers configurados en un solo lugar para todas las respuestas
  • Buffering de requests que protege a Node.js de clientes lentos

Instalacion en Ubuntu/Debian

sudo apt update
sudo apt install nginx -y
sudo systemctl enable nginx
sudo systemctl start nginx

Verifica que Nginx esta corriendo:

sudo systemctl status nginx
# o directamente
curl -I http://localhost

Deberias ver un HTTP 200 con el header Server: nginx. Los archivos de configuracion principales estan en /etc/nginx/:

  • /etc/nginx/nginx.conf — Configuracion global
  • /etc/nginx/sites-available/ — Archivos de configuracion por sitio
  • /etc/nginx/sites-enabled/ — Symlinks a los sitios activos
  • /var/log/nginx/ — Logs de acceso y errores

Configuracion basica de reverse proxy

Supongamos que tu aplicacion Node.js corre en el puerto 3001. Crea un archivo de configuracion:

sudo nano /etc/nginx/sites-available/mi-app

Con el siguiente contenido:

server {
    listen 80;
    server_name midominio.com www.midominio.com;

    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_http_version 1.1;
        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-Proto $scheme;
    }
}

Cada directiva proxy_set_header tiene un proposito concreto:

  • Host: Tu app Node.js necesita saber el dominio original para generar URLs correctas
  • X-Real-IP: La IP real del cliente (sin esto, req.ip en Express siempre seria 127.0.0.1)
  • X-Forwarded-For: Cadena completa de proxies por los que paso la request
  • X-Forwarded-Proto: Indica si la conexion original fue HTTP o HTTPS

Activa el sitio y verifica la configuracion:

sudo ln -s /etc/nginx/sites-available/mi-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

El comando nginx -t es tu mejor amigo. Ejecuitalo siempre antes de hacer reload. Si hay un error de sintaxis, te indica exactamente la linea y el archivo.

Configurar SSL con Certbot

Certbot automatiza la obtencion y renovacion de certificados Let's Encrypt. Instalacion:

sudo apt install certbot python3-certbot-nginx -y

Obtener el certificado:

sudo certbot --nginx -d midominio.com -d www.midominio.com

Certbot modifica automaticamente tu configuracion de Nginx para agregar las directivas SSL. Sin embargo, yo prefiero tener control total sobre la configuracion. Si optas por configurar SSL manualmente (por ejemplo, con certificados comprados o generados con acme.sh), la configuracion completa seria:

server {
    listen 80;
    server_name midominio.com www.midominio.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name midominio.com www.midominio.com;

    ssl_certificate /etc/letsencrypt/live/midominio.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/midominio.com/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_http_version 1.1;
        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-Proto $scheme;
    }
}

El primer bloque server escucha en el puerto 80 y redirige todo el trafico a HTTPS con un 301 permanente. El segundo bloque maneja las conexiones cifradas.

Sobre los protocolos: TLSv1.2 y TLSv1.3 son los unicos que deberias permitir. TLS 1.0 y 1.1 tienen vulnerabilidades conocidas y los navegadores modernos ya no los soportan.

Security headers

Agrega estos headers dentro del bloque server de HTTPS para mejorar la seguridad del sitio:

# Strict Transport Security: indica a navegadores que siempre usen HTTPS
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

# Evita que el sitio se cargue dentro de un iframe externo (clickjacking)
add_header X-Frame-Options "SAMEORIGIN" always;

# Evita que el navegador intente adivinar el MIME type
add_header X-Content-Type-Options "nosniff" always;

# Content Security Policy basica
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self';" always;

# Controla que informacion de referrer se envia
add_header Referrer-Policy "strict-origin-when-cross-origin" always;

# Controla el acceso a APIs del navegador
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

La directiva always garantiza que los headers se envien incluso en respuestas de error (4xx, 5xx). Sin ella, un error 502 cuando Node.js esta caido no tendria los headers de seguridad.

La Content Security Policy (CSP) merece atencion especial. La configuracion mostrada es restrictiva: solo permite recursos del mismo origen. Si usas fuentes de Google, analytics u otros servicios externos, necesitaras agregar sus dominios explicitamente. Empieza restrictivo y ve relajando segun necesites.

Compresion gzip

La compresion reduce significativamente el tamano de respuestas de texto (HTML, CSS, JS, JSON). Agrega esto dentro del bloque server o en nginx.conf para aplicarlo globalmente:

gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
    text/plain
    text/css
    text/xml
    text/javascript
    application/json
    application/javascript
    application/xml
    application/rss+xml
    image/svg+xml;

gzip_comp_level 6 es un buen balance entre compresion y CPU. Niveles superiores a 6 dan mejoras marginales con un costo de CPU desproporcionado. gzip_min_length 256 evita comprimir respuestas tan pequenas que el overhead de gzip las haria mas grandes.

Cache de archivos estaticos

Si tu aplicacion sirve assets estaticos desde una carpeta publica, configura Nginx para servirlos directamente y con cache agresivo:

location /static/ {
    alias /var/www/mi-app/public/;
    expires 30d;
    add_header Cache-Control "public, immutable";
    access_log off;
}

location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2?)$ {
    proxy_pass http://127.0.0.1:3001;
    expires 7d;
    add_header Cache-Control "public";
}

La primera directiva sirve archivos directamente desde el filesystem sin pasar por Node.js. La segunda permite que assets servidos por Node.js tengan headers de cache apropiados. access_log off evita llenar los logs con peticiones de assets estaticos.

Rate limiting

Para proteger tu aplicacion de abuso, Nginx ofrece rate limiting nativo:

# Definir la zona de rate limiting (en el bloque http, dentro de nginx.conf)
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

# Aplicar a rutas especificas
location /api/ {
    limit_req zone=api burst=20 nodelay;
    proxy_pass http://127.0.0.1:3001;
    proxy_http_version 1.1;
    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-Proto $scheme;
}

rate=10r/s permite 10 requests por segundo por IP. burst=20 permite rafagas de hasta 20 requests antes de rechazar. nodelay procesa las requests del burst inmediatamente en lugar de encolarlas.

Soporte para WebSockets

Si tu aplicacion usa WebSockets (por ejemplo, con Socket.IO), necesitas configuracion adicional porque WebSocket requiere un upgrade del protocolo HTTP:

location /socket.io/ {
    proxy_pass http://127.0.0.1:3001;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    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-Proto $scheme;
    proxy_read_timeout 86400;
}

Las lineas clave son Upgrade y Connection, que permiten el cambio de protocolo de HTTP a WebSocket. proxy_read_timeout 86400 (24 horas) evita que Nginx cierre conexiones WebSocket inactivas prematuramente.

Depuracion y diagnostico

Cuando algo no funciona, este es el orden de verificacion que sigo:

# 1. Verificar sintaxis de la configuracion
sudo nginx -t

# 2. Revisar que Nginx esta corriendo
sudo systemctl status nginx

# 3. Revisar logs de error
sudo tail -f /var/log/nginx/error.log

# 4. Revisar logs de acceso para ver las requests
sudo tail -f /var/log/nginx/access.log

# 5. Verificar que Node.js esta respondiendo
curl -I http://127.0.0.1:3001

# 6. Verificar puertos en uso
sudo ss -tlnp | grep -E '(80|443|3001)'

Los errores mas comunes que encuentro son:

  • 502 Bad Gateway: Node.js no esta corriendo o esta en un puerto diferente al configurado en proxy_pass
  • 504 Gateway Timeout: Node.js tarda demasiado en responder. Aumenta proxy_read_timeout
  • 403 Forbidden: Problemas de permisos en archivos estaticos
  • ERR_TOO_MANY_REDIRECTS: Loop de redireccion, generalmente porque X-Forwarded-Proto no esta configurado y la app redirige a HTTPS infinitamente

Configuracion completa de produccion

Uniendo todo lo anterior, esta es una configuracion completa lista para produccion:

limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;

server {
    listen 80;
    server_name midominio.com www.midominio.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name midominio.com www.midominio.com;

    ssl_certificate /etc/letsencrypt/live/midominio.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/midominio.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;

    # Security headers
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # Compresion
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_min_length 256;
    gzip_types text/plain text/css text/xml text/javascript
               application/json application/javascript application/xml
               application/rss+xml image/svg+xml;

    # Assets estaticos directos
    location /static/ {
        alias /var/www/mi-app/public/;
        expires 30d;
        add_header Cache-Control "public, immutable";
        access_log off;
    }

    # Rate limiting para API
    location /api/ {
        limit_req zone=api burst=20 nodelay;
        proxy_pass http://127.0.0.1:3001;
        proxy_http_version 1.1;
        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-Proto $scheme;
    }

    # Aplicacion Node.js
    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_http_version 1.1;
        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-Proto $scheme;
        proxy_read_timeout 60s;
        proxy_connect_timeout 10s;
    }
}

Esta configuracion cubre el 90% de los escenarios de produccion para aplicaciones Node.js. A partir de aqui, ajusta segun tus necesidades: agrega mas reglas de CSP si usas servicios externos, configura upstream blocks si balanceas carga entre multiples instancias, o agrega bloques de location especificos para rutas con requerimientos distintos.

Lo importante es entender que cada directiva tiene un proposito. No copies configuraciones ciegamente de Stack Overflow sin comprender que hace cada linea. Nginx es predecible cuando entiendes su modelo de procesamiento de requests y la jerarquia de sus bloques de configuracion.