Guías · Nivel 3

Nginx y Apache: publicar una aplicación interna como proxy inverso, con TLS, cabeceras, timeouts y un 502 Bad Gateway

Virtual hosts y server blocks, TLS con un certificado corporativo o de Let's Encrypt, proxy_pass y ProxyPass con sus cabeceras, timeouts, logs con el tiempo del backend, nginx -t y apachectl configtest, reload o restart, y un 502 y un 504 de principio a fin, SELinux incluido.

Qué vas a conseguir

Publicar una aplicación interna (la de fichajes, en 127.0.0.1:8080) detrás de Nginx o Apache: con su nombre, HTTPS, las cabeceras que la aplicación necesita, timeouts pensados y logs que dicen cuánto tardó el backend. Y, cuando aparezca el 502 Bad Gateway, saber en cinco minutos si es la aplicación, SELinux o un timeout.

Antes de empezar

  • Un servidor RHEL 9 (o Ubuntu) con Nginx o Apache: uno de los dos en el 443, no ambos.
  • Dónde está cada cosa: Nginx en /etc/nginx/conf.d/*.conf y logs en /var/log/nginx/. Apache en RHEL en /etc/httpd/conf.d/ con logs en /var/log/httpd/; en Debian y Ubuntu en /etc/apache2/sites-available/, que se activa con a2ensite, y logs en /var/log/apache2/.
  • El nombre en DNS: fichajes.corp.example apuntando al servidor.
  • El certificado: de la CA corporativa (certificado, cadena intermedia y clave) para nombres internos, o de Let's Encrypt si el nombre es público.

1. Un sitio por nombre: server blocks y virtual hosts

Nginx elige el bloque server por server_name; Apache, el VirtualHost por ServerName. El puerto 80 solo redirige:

/etc/nginx/conf.d/fichajes.conf
server {    listen 80;    server_name fichajes.corp.example;    return 301 https://$host$request_uri;}server {    listen 443 ssl;    server_name fichajes.corp.example;    ssl_certificate     /etc/pki/tls/certs/fichajes-fullchain.pem;    ssl_certificate_key /etc/pki/tls/private/fichajes.key;    ssl_protocols TLSv1.2 TLSv1.3;    access_log /var/log/nginx/fichajes.access.log upstream;    error_log  /var/log/nginx/fichajes.error.log;    location / {        proxy_pass http://127.0.0.1:8080;        include /etc/nginx/proxy-headers.conf;    }}
/etc/httpd/conf.d/fichajes.conf
<VirtualHost *:80>    ServerName fichajes.corp.example    Redirect permanent / https://fichajes.corp.example/</VirtualHost><VirtualHost *:443>    ServerName fichajes.corp.example    SSLEngine on    SSLCertificateFile    /etc/pki/tls/certs/fichajes-fullchain.pem    SSLCertificateKeyFile /etc/pki/tls/private/fichajes.key    ProxyPreserveHost On    ProxyPass        / http://127.0.0.1:8080/ connectiontimeout=5 timeout=60    ProxyPassReverse / http://127.0.0.1:8080/    RequestHeader set X-Forwarded-Proto "https"    ErrorLog  /var/log/httpd/fichajes_error.log    CustomLog /var/log/httpd/fichajes_access.log proxy</VirtualHost>
  • Un nombre que no coincide con ningún sitio cae en el sitio por defecto: default_server en Nginx, el primer VirtualHost del puerto en Apache. apachectl -S los lista.
  • El formato upstream del log de acceso y el proxy de Apache se definen en el apartado 5: sin ellos, el test de configuración falla.

2. TLS: certificado corporativo o Let's Encrypt

  • El fichero del certificado lleva primero el del servidor y después los intermedios. Sin el intermedio, el navegador de IT, que lo tiene en caché, funciona; el móvil de un usuario, no.
  • En Apache 2.4.8 y posteriores, SSLCertificateFile admite la cadena completa; SSLCertificateChainFile está obsoleto.
  • La clave privada con permisos 600. En RHEL, Apache necesita mod_ssl.
  • Let's Encrypt con certbot: certbot --nginx -d fichajes.example.com (o --apache) emite, configura y deja programada la renovación. Hoy los certificados duran 90 días y Let's Encrypt ya ha anunciado que irán siendo más cortos: la renovación tiene que ser automática, nunca manual. Comprueba que funciona con certbot renew --dry-run.
Bash · qué certificado entrega el servidor
openssl s_client -connect fichajes.corp.example:443 -servername fichajes.corp.example </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer -enddatesubject=CN = fichajes.corp.exampleissuer=CN = Contoso Issuing CA 01notAfter=Sep 14 23:59:59 2027 GMT

Sin -servername (SNI) verás el certificado del sitio por defecto.

3. El proxy inverso: las cabeceras

La aplicación ve todas las peticiones llegar desde 127.0.0.1. Para saber el nombre que pidió el usuario, su IP y que venía por HTTPS, el proxy se lo dice en cabeceras:

/etc/nginx/proxy-headers.conf
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_http_version 1.1;
  • Sin Host y X-Forwarded-Proto, la aplicación redirige a http://127.0.0.1:8080/login y el usuario acaba en una página que no carga. En Apache, ProxyPreserveHost On pasa el Host, ProxyPassReverse reescribe las redirecciones y mod_proxy añade X-Forwarded-For por su cuenta.

4. Timeouts

Qué controlaNginx (por defecto)Apache (por defecto)
Conectar con el backendproxy_connect_timeout (60 s)connectiontimeout= en ProxyPass
Esperar la respuestaproxy_read_timeout (60 s)timeout= en ProxyPass, o ProxyTimeout; si no, Timeout (60 s)
Tamaño máximo de una subidaclient_max_body_size (1 MB)LimitRequestBody
  • La conexión, corta (5 segundos): si el backend no acepta en ese tiempo, no lo va a hacer. La lectura, la que necesite la página más lenta de verdad, y solo en la location de esa página, no en todo el sitio.
  • Un fichero de más de 1 MB en Nginx devuelve 413 antes de llegar a la aplicación.

5. Logs que sirven

El log de acceso por defecto no dice cuánto tardó el backend. Añádelo en /etc/nginx/nginx.conf, dentro de http:

nginx.conf · formato con el tiempo del backend
log_format upstream '$remote_addr [$time_local] "$request" $status $body_bytes_sent '                    'rt=$request_time uct=$upstream_connect_time urt=$upstream_response_time '                    'us=$upstream_status ua=$upstream_addr';
  • rt es el tiempo total y urt el del backend: si rt es alto y urt bajo, el lento es el cliente o la red, no la aplicación. us=- indica que la petición ni siquiera llegó al backend.
  • En Apache, %D (microsegundos) da el tiempo total: LogFormat "%h %t \"%r\" %>s %b %D" proxy.
  • El log de errores (error_log, ErrorLog) es donde aparece la causa de un 502. Es lo primero que se abre.

6. Probar y aplicar: configtest, reload o restart

Bash · antes de aplicar un cambio
sudo nginx -tnginx: the configuration file /etc/nginx/nginx.conf syntax is oknginx: configuration file /etc/nginx/nginx.conf test is successfulsudo systemctl reload nginxsudo apachectl configtestSyntax OKsudo systemctl reload httpd
  • Siempre el test antes. nginx -T vuelca la configuración completa con todos los include.
  • reload relee la configuración sin cortar: Nginx arranca procesos nuevos y deja terminar a los viejos; en Apache es un reinicio graceful. Si la configuración nueva tiene un error, Nginx sigue con la anterior.
  • restart para y arranca: corta las conexiones abiertas. Solo cuando el proceso no responde o tras una actualización del paquete.

7. El ticket: "la aplicación de fichajes da 502 Bad Gateway"

Lunes, 9:02. Toda la plantilla ficha a la vez y la página devuelve 502 Bad Gateway. Un 502 lo genera el proxy: Nginx funciona, lo que falla es la conexión con el backend. El log de errores dice cuál de los casos es:

Bash · fichajes.error.log
sudo tail -n 3 /var/log/nginx/fichajes.error.log2026/10/26 09:02:11 [error] 2211#2211: *8812 connect() failed (111: Connection refused) while connecting to upstream, client: 10.20.4.37, server: fichajes.corp.example, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8080/"ss -ltnp | grep 8080systemctl status fichajes --no-pagerActive: failed (Result: oom-kill) since Mon 2026-10-26 08:58:40 CET
  1. 111: Connection refused: nadie escucha en el 8080. ss no devuelve nada y el servicio de la aplicación ha muerto por falta de memoria. Se arranca, se confirma que responde (curl -I http://127.0.0.1:8080/) y se abre un ticket con el equipo de la aplicación con la hora del oom-kill.
  2. 13: Permission denied while connecting to upstream, en un RHEL recién montado y con el backend escuchando: es SELinux. ausearch -m avc -ts recent muestra el denied { name_connect } de nginx (o httpd). La solución es setsebool -P httpd_can_network_connect 1, no setenforce 0. En Apache aparece como (13)Permission denied: AH00957.
  3. 504 Gateway Time-out, solo en el informe mensual: upstream timed out (110: Connection timed out) while reading response header from upstream y urt=60.001. El backend tarda más que proxy_read_timeout: se sube a 300 s solo en location /informes/ y se pide que el informe sea asíncrono. En Apache es AH01102: error reading status line from remote server.

Antes de tocar el proxy, curl -I http://127.0.0.1:8080/ desde el propio servidor. Si el backend no responde ahí, el proxy no tiene nada que arreglar.

Siguientes pasos

  • La guía de Red Hat Enterprise Linux del Nivel 2 explica los servicios, el cortafuegos y SELinux que aparecen en el ticket.
  • La de Docker cubre las aplicaciones en contenedores que suelen estar detrás de este proxy.
  • La de IIS es la versión Windows de todo esto: sitios, enlaces, certificados y el 503.
  • Vigilar desde fuera que el certificado no caduca y que el sitio responde es lo que hacen las sondas TLS y HTTP del repositorio 08-edge-health-slo-monitor de mi GitHub.

¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.

Tech Career Lab · newsletter

¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.

Apúntate y llévate las 33 ideas →