nginx sirve contenido desde un proxy incorrecto

nginx sirve contenido desde un proxy incorrecto

Tengo un único servidor que aloja varios sitios Rails. Todos los sitios se identifican mediante nombres de host únicos (todos los nombres de dominio se asignan a la misma dirección IP). Tanto Rails como nginx se ejecutan en contenedores Docker. Estoy usando nginx 1.23.1 ejecutándose en una imagen de Docker creada a partir de la imagen oficial de Docker (solo agregué certbot para el procesamiento de certificados TLS).

Después de agregar recientemente otro sitio, comenzó a suceder algo muy extraño. Inmediatamente después de iniciar nginx, todo funciona como se esperaba: todo el contenido devuelto coincide con el nombre de host en la solicitud. Pero después de unas horas, el contenido devuelto no coincide con el nombre de host solicitado. Pero esto sólo afecta al contenido del proxy; todos los recursos estáticos aún se sirven correctamente según el nombre del host.

Por ejemplo, cuando solicitohttps://www.meaaa.com(todos los dominios aquí son ejemplos, no dominios reales), obtengo el contenido HTML de bbb.me.com. Y dado que el contenido de bbb.me.com solicita estilos e imágenes que espera encontrar en bbb.me.com, el servidor responde a todas esas solicitudes con 404 (porque los activos estáticos se sirven desde elwww.meaaa.comarchivos, ya que el nombre del host de solicitud eswww.meaaa.com).

Y si lo pidohttps://bbb.me.com, obtengo el contenido HTML dewww.meaaa.com. Nuevamente, se espera que los activos especificados en el margen provengan de enwww.meaaa.com, pero como los activos estáticos se obtienen correctamente según el nombre de host bbb.me.com en la solicitud, no se encuentran.

Por lo tanto, el contenido Rails ascendente de los dos sitios parece haber intercambiado lugares, mientras que los activos estáticos se sirven correctamente.

He estado usando nginx que no es Docker durante años con varios sitios Rails y nunca había visto que esto sucediera. No se trata de solicitar un host indefinido; Ambos hosts se declaran en la configuración. Si un host dejó de ser reconocido, entonces podría asumir que el contenido devuelto era solo el servidor predeterminado, pero en realidad ambos son reconocidos, simplemente intercambiados. El hecho de que solo se cambie el contenido del proxy y no los activos estáticos muestra que se están reconociendo ambos nombres de host.

Para resumir los síntomas, esto es lo que muestra curl:

$ curl -s https://www.meaaa.com | grep '<title>'
  <title>BBB Site</title>

$ curl -s https://bbb.me.com | grep '<title>'
  <title>MEAAA Site</title>

Solicitandowww.meaaa.comactivos estáticos individualmente usando elwww.meaaa.comEl nombre de host funciona bien, al igual que solicitar activos de bbb.me.com desde bbb.me.com.

También me aseguré de que el problema no sea Docker. Dentro del contenedor nginx, puedo curvar cada back-end y obtener el contenido correcto:

$ curl -s http://aaa:3000 | grep '<title>'
  <title>MEAAA Site</title>

$ curl -s http://bbb:3000 | grep '<title>'
  <title>BBB Site</title>

Aquí está la configuración para elwww.meaaa.comsitio:

upstream aaa-rails {
    server aaa:3000;
}

server {
    server_name www.meaaa.com source.meaaa.com aaa.meinternal.com;
    root /var/www/aaa-rails/public;
    index index.html index.htm;

    location /cms {
      deny 172.22.188.2; # public network interface
      try_files $uri @app;
    }
    location / {
        try_files $uri/index.html $uri @app;
    }

    location @app {
        proxy_pass http://aaa-rails;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Origin $scheme://$http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-Host $host;
        proxy_redirect off;
    }

    location ~* ~/assets/ {
        try_files $uri @app;
    }

    listen 80;

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/www.meaaa.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/www.meaaa.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

Y aquí está la configuración para el sitio bbb.me.com:

upstream bbb-rails {
    server bbb:3000;
}

server {
    server_name bbb.me.com bbb-source.me.com bbb.meinternal.com;
    root /var/www/bbb-rails/public;
    index index.html index.htm;

    client_max_body_size 50m;

    location / {
        try_files $uri @app;
    }

    location @app {
        proxy_pass http://bbb-rails;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_set_header Origin $scheme://$http_host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-Port $server_port;
        proxy_set_header X-Forwarded-Host $host;
        proxy_redirect off;
    }

    location ~* ~/assets/ {
        try_files $uri @app;
    }

    listen 80;

    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/bbb.me.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/bbb.me.com/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}

Para mí, lo más extraño es que reiniciar nginx soluciona el problema, pero solo temporalmente. No creo que haya ningún almacenamiento en caché y no veo ningún error en los registros de nginx. Cualquier sugerencia sobre qué mirar sería muy apreciada.

ACTUALIZAR

Los dos sitios que cambiaron de lugar fueron los que usaban HTTPS. Modifiqué varios otros para usar HTTPS, y ahora tres de ellos están equivocados en una especie de round-robin: pide aaa, obtén bbb; pide bbb, obtén ccc; pida ccc, obtenga aaa. Otros dos sitios simplemente no responden. Es como si algún evento impredecible hiciera que nginx corrompiera las tablas de enrutamiento que utiliza para servir contenido proxy.

Por ahora, dado que se trata de un servidor de producción, reinicio nginx cada 60 minutos. Estoy intentando configurar un servidor de prueba como un duplicado del servidor de producción, con la esperanza de que surja el mismo problema allí para poder intentar resolver el problema sin cerrar los sitios.

Respuesta1

Resulta que el problema surgió del hecho de que nginx solo resuelve cada nombre de back-end una vez (cuando carga su configuración), y luego asume que la dirección IP nunca cambiará. Gracias a Ángel de la lista de correo de nginx, quien señaló esto.

En mi caso, dado que cada back-end es una aplicación web, tengo que ejecutar logrotate diariamente, de modo que si los registros crecen lo suficiente, los archivos de registro se rotarán y comprimirán, y la aplicación Rails correspondiente se reiniciará.

Puede suceder fácilmente que dos o más aplicaciones Rails se reinicien más o menos simultáneamente, pero no se garantiza que se reinicien precisamente en el orden correcto para que recuperen su dirección IP anterior en la red interna de Docker. Eso explica lo que estaba sucediendo: de vez en cuando, las aplicaciones Rails intercambiaban direcciones IP, pero nginx no lo sabía, por lo que continuó reenviando solicitudes a las direcciones IP antiguas y, al mismo tiempo, sirviendo activos estáticos correctamente. Simulé esto deteniendo y reiniciando dos aplicaciones y pude reproducir el problema exactamente. Además, agregué un script que monitorearía los sitios cada cinco minutos, y los registros mostraban que el problema siempre parecía ocurrir aproximadamente al mismo tiempo, lo cual era consistente con que logrotate fuera el desencadenante.

Ángel señaló que, en lugar de reiniciar nginx, es posible simplemente hacer que recargue la configuración, lo que debería resultar en una menor interrupción para los visitantes de los sitios. También señaló que es posible forzar a nginx a buscar nombres cada pocos minutos en lugar de solo una vez (verhttps://forum.nginx.org/read.php?2,215830,215832#msg-215832).

Para resolver el problema, al menos por ahora he configurado Docker para asignar una dirección IP fija a todos los contenedores. De esa manera, nginx puede continuar resolviendo nombres solo una vez. Sin embargo, esto tiene algunas consecuencias, ya que ahora no puedo ejecutar comandos como db:migrate o activos:precompile usando el mismo archivo docker-compose.yml que la aplicación Rails en ejecución (obtiene el error "la dirección está en uso"); Por ahora, estoy usando docker compose execen lugar de run, pero esto parece tener un efecto en el docker compose restartcomando (aparece "la dirección está en uso" durante unos segundos después de ejecutar exec). Si esto se convierte en un problema, puedo volver a direcciones IP no fijas y hacer que nginx recargue su configuración como parte del proceso logrotate.

información relacionada