Proxy inverso con Nginx: guía práctica

Nginx como proxy inverso: subidas de archivos grandes, timeouts, rate limiting, gzip y el clásico 502 por SELinux en RHEL.

Qué es un proxy inverso

Un proxy inverso recibe las peticiones de los clientes y las reenvía al servidor interno correspondiente. El cliente nunca se conecta directamente al backend. Esto permite centralizar HTTPS, añadir cabeceras de seguridad, cachear contenido y distribuir carga. Es habitual usarlo delante de servicios autoalojados como Nextcloud o Gitea para exponerlos con un dominio propio y HTTPS en vez de un puerto suelto.

Clientes (HTTPS, 443)


Nginx — proxy inverso, certificado TLS único

   ├── app.ejemplo.com  → 127.0.0.1:3000        (Nextcloud, Gitea...)
   ├── api.ejemplo.com  → 127.0.0.1:8080
   └── otro.ejemplo.com → upstream backend_app   (varios backends,
                                                    balanceo de carga)

Instalación

# RHEL / Oracle Linux / Rocky
sudo dnf install nginx

# Ubuntu/Debian
sudo apt install nginx
sudo systemctl enable --now nginx

Configuración básica

Un proxy que redirige todo el tráfico de un dominio a una aplicación interna en el puerto 3000:

server {
    listen 80;
    server_name app.ejemplo.com;

    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-Proto $scheme;
    }
}

Las cabeceras X-Real-IP y X-Forwarded-For permiten que la aplicación conozca la IP real del cliente en lugar de ver siempre 127.0.0.1.

Añadir HTTPS con Let’s Encrypt

Aquí el flujo específico para Nginx; para entender cómo funciona Certbot y la renovación automática en más detalle, consulta la guía de certificados SSL con Certbot y Let’s Encrypt.

# RHEL/Rocky/Oracle Linux
sudo dnf install epel-release -y
# En RHEL/Rocky 9 puede ser necesario habilitar también el repositorio crb:
# sudo dnf config-manager --set-enabled crb
sudo dnf install certbot python3-certbot-nginx
sudo certbot --nginx -d app.ejemplo.com

Certbot modifica automáticamente la configuración de Nginx para escuchar en el puerto 443 y redirigir el tráfico HTTP a HTTPS.

Para renovar automáticamente:

sudo systemctl enable --now certbot.timer

Múltiples aplicaciones en un servidor

# App principal
server {
    listen 443 ssl;
    server_name app.ejemplo.com;
    ssl_certificate /etc/letsencrypt/live/app.ejemplo.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.ejemplo.com/privkey.pem;

    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-Proto $scheme;
    }
}

# API
server {
    listen 443 ssl;
    server_name api.ejemplo.com;
    ssl_certificate /etc/letsencrypt/live/api.ejemplo.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.ejemplo.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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;
    }
}

Balanceo de carga

Si tienes varios backends para la misma aplicación:

upstream backend_app {
    server 192.168.1.10:3000;
    server 192.168.1.11:3000;
    server 192.168.1.12:3000 backup;
}

server {
    listen 443 ssl;
    server_name app.ejemplo.com;

    location / {
        proxy_pass http://backend_app;
        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 tercer servidor con backup solo recibe tráfico si los dos primeros están caídos.

Subidas grandes y timeouts

Dos ajustes por defecto de Nginx sorprenden en cuanto proxeas algo más que una web ligera: el tamaño máximo de subida (client_max_body_size) es de solo 1 MB, y los timeouts de proxy son de 60 segundos. Detrás de un Nextcloud, por ejemplo, ambos límites se alcanzan enseguida — el primero al subir cualquier archivo de más de 1 MB (error 413), el segundo en operaciones largas como sincronizar una carpeta con muchos archivos:

server {
    listen 443 ssl;
    server_name app.ejemplo.com;

    client_max_body_size 512M;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_read_timeout 300s;
        proxy_connect_timeout 300s;
        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;
    }
}

client_max_body_size puede ajustarse en http, server o location — cuanto más cerca del location concreto que lo necesita, menos abres el límite en el resto del sitio.

Limitar la tasa de peticiones

Para frenar abuso o fuerza bruta contra un endpoint concreto (un login, una API pública) sin depender solo de fail2ban a nivel de conexión:

# en el bloque http {} — define la zona compartida y el límite
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;

server {
    # ...
    location /login {
        limit_req zone=login burst=5 nodelay;
        proxy_pass http://127.0.0.1:3000;
    }
}

rate=1r/s fija el ritmo sostenido permitido por IP; burst=5 deja pasar hasta 5 peticiones de golpe por encima de ese ritmo antes de empezar a rechazar, y nodelay las procesa inmediatamente en vez de encolarlas artificialmente.

Compresión con gzip

gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml;
gzip_min_length 1024;

Comprime las respuestas de texto antes de enviarlas, reduciendo el tráfico en respuestas de API y assets estáticos que el backend no comprima ya por su cuenta. gzip_min_length evita gastar CPU comprimiendo respuestas tan pequeñas que no compensa.

Cabeceras de seguridad

Añade estas cabeceras a nivel global o por servidor:

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

## Nota

No incluyas X-XSS-Protection: es una cabecera deprecada que los navegadores modernos ignoran (Chrome eliminó su filtro XSS en 2019, Firefox nunca lo implementó, Safari lo retiró en 2022), y en algunos casos llegó a introducir vulnerabilidades XSS en sitios que sin ella eran seguros. La protección real hoy la da una Content-Security-Policy bien configurada.

WebSockets

Si tu aplicación usa WebSockets, necesitas pasar las cabeceras de upgrade:

location /ws {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
}

Verificar la configuración

## Importante

Ejecuta siempre nginx -t antes de recargar. Si la configuración tiene un error de sintaxis y recargas a ciegas, nginx puede fallar al aplicar los cambios y tumbar todos los sitios que proxea, no solo el que estabas editando.

sudo nginx -t
sudo systemctl reload nginx

502 Bad Gateway: las dos causas más comunes

Un 502 significa que Nginx sí recibió la petición, pero no consiguió comunicarse con el backend. Antes de sospechar de la aplicación:

  1. El backend no está escuchando — confírmalo con curl http://127.0.0.1:3000 desde el propio servidor; si falla ahí, el problema es la aplicación o su puerto, no Nginx.
  2. SELinux bloqueando la conexión saliente (RHEL/Rocky/Oracle Linux con SELinux en modo enforcing) — la política por defecto no permite que Nginx abra conexiones de red salientes hacia el backend. Se ve en el log de Nginx como connect() failed (13: Permission denied), y se soluciona habilitando el booleano correspondiente:
sudo setsebool -P httpd_can_network_connect 1

## Nota

httpd_can_network_connect permite conectar a cualquier puerto TCP; si tu backend corre en un puerto ya etiquetado como HTTP (80, 443, 8008, 8443, 9000), el booleano más restrictivo httpd_can_network_relay cubre el caso sin abrir tanto. Para el resto de puertos (3000, 5000, 8080…) necesitas httpd_can_network_connect.

Conclusión

Nginx como proxy inverso es el estándar para exponer aplicaciones internas de forma segura. Con HTTPS centralizado, cabeceras de seguridad y balanceo de carga, un solo Nginx puede gestionar decenas de servicios internos sin que cada uno tenga que preocuparse por certificados o seguridad a nivel de transporte.

## Nota

✍️ Transparencia: Este artículo ha sido creado con el apoyo de herramientas de inteligencia artificial. Toda la información técnica ha sido revisada y validada por el autor antes de su publicación.

$ curl tengoping.com/rss.xml | subscribe [suscribirse por RSS →]