UD3: Servidores de Alto Rendimiento: Nginx
Ficha Técnica y Alcance de la Unidad
- Carga Lectiva: 4 horas presenciales
- Resultados de Aprendizaje: RA1 (criterios g, h, i)
- Tecnologías Involucradas: Nginx
- Referencias y Estándares: CIS Nginx Benchmark, OWASP Secure Headers Project, RFC 7239 (cabecera
Forwarded, la versión estándar de lasX-Forwarded-*)
📖 1. Fundamentos Teóricos y Arquitectura
1.1 ¿Qué es un proxy inverso y por qué necesitamos uno?
Un proxy es un intermediario que recibe peticiones en nombre de otro. Ya conoces el "proxy directo" (habitual en institutos/empresas): un servidor por el que el cliente pasa para salir a Internet, normalmente para filtrar contenido. Un proxy inverso hace lo contrario: se coloca delante del servidor, y es el propio servidor el que oculta su arquitectura interna detrás de él.
Imagina la recepción de un hotel: los huéspedes (clientes) siempre hablan con la recepción (proxy inverso), nunca directamente con el departamento de limpieza, mantenimiento o cocina (servidores internos). La recepción decide a qué departamento redirigir cada petición, y el huésped ni siquiera necesita saber cuántos departamentos hay ni dónde están físicamente.
En nuestra arquitectura, Nginx cumplirá ese papel: recibirá todas las peticiones de Internet, terminará el cifrado TLS ahí mismo, y decidirá si servir un fichero estático directamente o reenviar la petición a otro programa interno (Apache, o en UD5 una aplicación Python) que no está expuesto directamente a Internet.
graph LR
Internet -->|"HTTPS :443"| N["Nginx: TLS + estáticos"]
N -->|"location / → proxy_pass"| A["Backend: Apache en 127.0.0.1:8080"]
N -->|"location /static/ → alias"| S["/var/www/app/static/"]
1.2 ¿Por qué Nginx es más eficiente que el modelo de procesos e hilos de Apache?
En UD2 vimos los MPM de Apache: con prefork hay un proceso por conexión; con worker, un hilo por
conexión; y con event (el que viene de serie), un hilo por cada petición en curso, que solo se
libera de las conexiones keep-alive que están esperando sin hacer nada. En los tres casos, mientras
se atiende una petición hay un proceso o un hilo ocupado con ella, aunque esté parado esperando al
disco o a la red.
Nginx sigue un enfoque distinto: un modelo asíncrono orientado a eventos. En vez de "un trabajador dedicado por cliente", Nginx tiene un número reducido de procesos worker (normalmente uno por núcleo de CPU) que van atendiendo miles de conexiones sin bloquearse esperando a ninguna de ellas — de forma parecida a un único camarero muy ágil que va tomando nota en varias mesas mientras la cocina prepara cada plato, en vez de un camarero fijo esperando parado junto a cada mesa hasta que su plato esté listo.
Técnicamente, esto se apoya en mecanismos del kernel de Linux como epoll, que permiten a un
programa preguntar "¿hay alguna de estas mil conexiones con datos nuevos?" de forma muy eficiente, en
vez de tener que revisarlas una a una constantemente.
Esta arquitectura es idónea como proxy inverso / balanceador delante de Apache, PHP-FPM o aplicaciones Python (Gunicorn/Uvicorn, ver UD5), terminando TLS y sirviendo estático de forma muy eficiente sin sobrecargar a los servidores de aplicación con ese trabajo.
1.3 Lo que el backend deja de "ver" al estar detrás de un proxy
Cuando Nginx reenvía una petición al backend, esa conexión interna es nueva: el backend recibiría
como IP de origen la del propio Nginx, no la del cliente real, salvo que se lo digamos explícitamente
mediante cabeceras HTTP adicionales (esto se resuelve con X-Real-IP y X-Forwarded-*, que veremos
en la configuración). Es importante entenderlo bien porque es una fuente de errores muy habitual: un
log de acceso del backend que muestre siempre la misma IP casi siempre significa que faltan esas
cabeceras.
⚙️ 2. Instalación, Configuración Avanzada y Hardening
| Instalación en Debian/Ubuntu | |
|---|---|
1 2 | |
El paquete de Debian/Ubuntu organiza la configuración igual que Apache en UD2:
| Ruta | Para qué sirve |
|---|---|
/etc/nginx/nginx.conf |
Configuración global: workers, events {} y el bloque http {} |
/etc/nginx/conf.d/*.conf |
Se incluyen dentro de http {}: solo directivas de ese nivel (zonas de limit_req, log_format, gzip_types…). Un location aquí da error |
/etc/nginx/sites-available/ |
Un fichero por sitio (bloques server {}) |
/etc/nginx/sites-enabled/ |
Los sitios activos. No hay a2ensite: se activan con un enlace simbólico |
¿Y el repositorio oficial de nginx.org?
Si necesitas la última versión, nginx.org tiene su propio repositorio. Ten en cuenta que esos
paquetes no traen sites-available/sites-enabled: todo va en conf.d/. En clase usamos el
paquete de la distribución.
| /etc/nginx/sites-available/app.conf | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | |
| Activar el sitio | |
|---|---|
1 | |
Cada bloque location le dice a Nginx cómo tratar peticiones según la ruta pedida: el bloque /
reenvía (proxy_pass) todo lo que no sea explícitamente estático hacia el backend interno en el
puerto 8080; el bloque /static/ sirve directamente los ficheros de ese directorio sin pasar por el
backend, mucho más rápido para imágenes/CSS/JS. El segundo bloque server (puerto 80) simplemente
redirige cualquier visita sin cifrar hacia la versión HTTPS (301 = redirección permanente).
X-Real-IP, X-Forwarded-For y X-Forwarded-Proto le dicen al backend la IP y el esquema
(http/https) originales del cliente. No son un estándar oficial, sino una convención que entiende
casi todo el software; la versión estandarizada es la cabecera Forwarded (RFC 7239). Sin ellas,
los logs y el control de acceso del backend verían siempre 127.0.0.1, como se explicaba en el
punto 1.3. Ojo: si un location define su propio proxy_set_header, deja de heredar todos los
del server.
Apache y Nginx en la misma máquina
Solo un programa puede escuchar en cada puerto. Si Apache de UD2 sigue en el 80/443, Nginx no
arranca (Address already in use). Para ponerlo detrás de Nginx, en /etc/apache2/ports.conf
cambia a Listen 127.0.0.1:8080, pasa su VirtualHost a <VirtualHost *:8080> sin TLS
(el cifrado lo hace ahora Nginx), desactiva el sitio en el 443 y reinicia Apache.
HSTS con un certificado autofirmado
Con Strict-Transport-Security, el navegador recuerda durante max-age segundos que ese dominio
solo va por HTTPS y ya no deja aceptar el aviso del certificado autofirmado. En el laboratorio
usa un valor corto (max-age=300) o pruébalo solo con curl; el valor de dos años es para
producción con un certificado válido.
🛠️ 3. Laboratorio Práctico Dirigido
Antes de probar necesitas:
- El certificado de UD1/UD2 en
/etc/ssl/lab/lab.crtylab.key. - Un backend escuchando en
127.0.0.1:8080(por ejemplo, el Apache de UD2 movido a ese puerto). - Que
app.iaw.localapunte a tu máquina: añade127.0.0.1 app.iaw.locala/etc/hosts.
| Validar, recargar y comprobar | |
|---|---|
1 2 3 | |
nginx -t comprueba la sintaxis sin tocar el servicio: si falla, el && impide el reload y Nginx
sigue funcionando con la configuración anterior. systemctl reload (a diferencia de restart)
aplica la nueva configuración sin cerrar las conexiones ya activas: Nginx termina de atender las
peticiones en curso con la configuración antigua y empieza a usar la nueva para las siguientes —
imprescindible en un servidor en producción con usuarios conectados. La -k de curl acepta el
certificado autofirmado.
Si algo falla
502 Bad Gateway: Nginx funciona, pero no hay nada en el backend. Compruébalo conss -tlnp | grep 8080.Address already in use: otro programa (¿Apache?) ocupa el 80 o el 443."location" directive is not allowed here: has puesto unlocationfuera de unserver {}(por ejemplo, enconf.d/).- El error exacto, con fichero y línea, está en
/var/log/nginx/error.log.
🛡️ 4. Seguridad, Logs y Optimización de Rendimiento
| /etc/nginx/nginx.conf (extracto) | |
|---|---|
1 2 3 4 5 6 | |
- El número máximo de conexiones simultáneas es aproximadamente
worker_processes × worker_connections. Como proxy, cada cliente gasta dos conexiones (la del cliente y la del backend), así que caben la mitad de clientes. Si se quedan cortas, enerror.logapareceworker_connections are not enough. worker_rlimit_nofileeleva el límite de descriptores de fichero de cada worker (cada conexión es un descriptor); tiene que ser mayor queworker_connectionso apareceráToo many open files.- Logs:
/var/log/nginx/access.logyerror.log. Conlog_format … escape=json(en contextohttp) se pueden escribir en JSON, el formato recomendado para enviarlos a un SIEM. limit_req_zonemitiga brute-force y DoS de capa 7 sin depender del backend.
El limitador se define en dos sitios, porque cada directiva va en un contexto distinto:
| /etc/nginx/conf.d/limites.conf (contexto http) | |
|---|---|
1 2 | |
| Dentro del server {} de sites-available/app.conf | |
|---|---|
1 2 3 4 | |
Esta configuración limita a 10 peticiones por minuto por IP (rate=10r/m, una cada 6 segundos) la
ruta /login, con un margen de ráfaga de 5 peticiones más (burst=5) que se atienden al momento
(nodelay): un usuario legítimo que se equivoca un par de veces no nota nada, pero un script que
prueba contraseñas recibe enseguida 429 Too Many Requests. Sin limit_req_status 429, Nginx
respondería 503 Service Unavailable, que confunde porque parece que el servidor está caído.
❓ 5. Preguntas y Casos de Autoevaluación
-
¿Por qué Nginx soporta más conexiones concurrentes con menos RAM que Apache prefork? Resolución: Modelo asíncrono orientado a eventos (un worker atiende miles de conexiones no bloqueantes) frente a un proceso por conexión con su propio stack de memoria.
-
Los logs del backend muestran siempre la IP del proxy (
127.0.0.1) en vez de la del cliente. ¿Causa? Resolución: Faltan las cabecerasX-Real-IP/X-Forwarded-For(proxy_set_header), o el backend no está configurado para confiar en ellas (en Apache,mod_remoteipconRemoteIPHeader X-Forwarded-ForyRemoteIPInternalProxy 127.0.0.1). -
¿Qué diferencia hay entre
aliasyrooten un bloquelocationde Nginx? Resolución:rootañade la URI completa a la ruta base (root /var/www/app;+/static/logo.png→/var/www/app/static/logo.png);aliassustituye la parte de la URI que coincide con ellocation(location /static/ { alias /var/www/app/static/; }→ la misma ruta). Error común: poner la barra final en uno y no en el otro. -
Justifica el uso de
limit_req_zonefrente a bloquear IPs manualmente. Resolución: Es dinámico y basado en tasa (peticiones por segundo o minuto) sin mantenimiento de listas; complementa (no sustituye) un WAF/fail2ban para bloqueo persistente tras umbral de abuso. -
¿Qué relación hay entre
worker_connectionsyworker_rlimit_nofile? Resolución:worker_connectionsfija cuántas conexiones atiende cada worker; cada conexión es un descriptor de fichero, yworker_rlimit_nofilees el límite de descriptores del worker (el del sistema,ulimit -n, suele ser 1024). El segundo debe ser mayor que el primero; como proxy, cada cliente usa dos conexiones. -
nginx -tdice"location" directive is not allowed here in /etc/nginx/conf.d/limites.conf. ¿Qué pasa? Resolución: Los ficheros deconf.d/se incluyen dentro dehttp {}, y unlocationsolo puede ir dentro de unserver {}. La zona (limit_req_zone) se queda enconf.d/; ellocationconlimit_reqva en el fichero del sitio.