Error crítico de Nginx: proxy_cache_key en multidominio

Publicado por Alejandro Escario el

Me pasó esta semana. Dos dominios distintos, mismo servidor, misma IP, ambos detrás de Cloudflare. Uno de ellos mostraba el contenido del otro. Así, sin más. Entrabas en el dominio A y veías la web del dominio B.

Lo comparto porque es un fallo silencioso, fácil de cometer y difícil de diagnosticar si no sabes dónde mirar. Y porque me llevó un rato dar con ello.

El montaje

La arquitectura no tiene nada de raro. Es la que usa medio internet:

Cloudflare por delante como CDN y proxy DNS. Detrás, un servidor con Nginx haciendo de proxy inverso y caché, que pasa las peticiones a un Apache con VirtualHosts separados para cada dominio. Panel tipo cPanel/aaPanel. Nada raro.

Los dos dominios apuntan a la misma IP. Cada uno tiene su VirtualHost en Apache, su directorio, su certificado SSL. Todo como debe ser.

Pero al abrir el dominio A en el navegador, aparecía el contenido del dominio B. Siempre. Da igual si purgabas caché del navegador, si probabas en incógnito, si cambiabas de red. Dominio A = contenido de B.

El proceso de descarte

Lo primero que piensas es que Cloudflare está mezclando respuestas. Dos dominios, misma IP, mismo servidor… tiene sentido como sospecha inicial. Así que hice Purge Everything en ambos dominios desde el dashboard. Nada, el problema seguía.

Después revisé los VirtualHosts de Apache. Desde el propio servidor lancé:

curl -H "Host: dominio-a.com" http://localhost:8288
curl -H "Host: dominio-b.com" http://localhost:8288

Cada curl devolvió el HTML correcto de su dominio. Apache estaba bien. Los VirtualHosts resolvían como tenían que resolver.

SSL/TLS también descartado: ambos dominios con el mismo modo en Cloudflare, certificados correctos en el servidor.

La pista falsa del fichero hosts

Aquí es donde la cosa se pone interesante y donde me despistó.

Si en mi máquina editaba el fichero hosts para apuntar ambos dominios directamente a la IP del servidor —saltándome Cloudflare—, todo funcionaba perfectamente. Cada dominio mostraba su contenido. «Pues es Cloudflare», pensé.

Pero no. La explicación es más sutil.

Cuando te saltas Cloudflare tocando el fichero hosts, cambias las condiciones de la petición. El tráfico real llega a Nginx a través de Cloudflare por HTTPS; las peticiones directas desde tu máquina llegan por HTTP (distinto $scheme). Eso ya cambia la clave de caché internamente, así que Nginx no encuentra una entrada cacheada y consulta a Apache, que responde bien.

Además, cuando pruebas manualmente visitas un dominio, esperas, visitas el otro… No generas el volumen de tráfico necesario para que el caché se «contamine». En producción, con cientos de peticiones por minuto, el primer dominio que recibe una visita llena el caché y todos los demás se llevan esa misma respuesta.

La moraleja: que algo funcione al saltar un componente de la cadena no significa que ese componente sea el culpable. En este caso Cloudflare no hacía absolutamente nada mal. Solo era la puerta por la que entraba el tráfico real que exponía el bug de Nginx.

La causa real: el proxy_cache_key

Con Apache y Cloudflare descartados, quedaba Nginx. Fui al proxy.conf y ahí estaba:

proxy_cache_path /www/server/nginx/proxy_cache_dir levels=1:2 
    keys_zone=cache_one:20m inactive=1d max_size=5g;

proxy_cache cache_one;

Caché activado globalmente. Ningún proxy_cache_key definido.

Cuando no defines un proxy_cache_key explícito, Nginx usa por defecto:

$scheme$proxy_host$request_uri

Y aquí está el problema. $proxy_host no es el dominio que pidió el visitante. Es la dirección del proxy_pass, o sea, el backend. Como ambos vhosts de Nginx envían las peticiones a http://127.0.0.1:8282, esa variable vale lo mismo para los dos dominios.

Vamos a verlo con un ejemplo concreto. Un GET a la raíz de cada dominio:

Variabledominio-a.comdominio-b.com
$schemehttphttp
$proxy_host127.0.0.1:8282127.0.0.1:8282
$request_uri//
Clave resultantehttp127.0.0.1:8282/http127.0.0.1:8282/

Misma clave. Para Nginx, pedir la home de dominio-a.com o de dominio-b.com es exactamente la misma petición. El primero que llega gana, y el segundo se come su contenido cacheado.

La secuencia completa del fallo

Para que quede claro lo que ocurre en producción:

  1. Alguien visita dominio-b.com. Nginx no tiene caché para http127.0.0.1:8282/, así que le pregunta a Apache.
  2. Apache devuelve la web de dominio-b.com. Nginx la guarda en caché con esa clave.
  3. Alguien visita dominio-a.com. Nginx calcula la clave: http127.0.0.1:8282/. Ya la tiene.
  4. Nginx devuelve el contenido de dominio-b.com sin consultar a Apache.

El dominio que se visite primero después de un reinicio o una limpieza de caché es el que «gana». Los demás quedan secuestrados.

La solución

Una línea. En proxy.conf, después de proxy_cache cache_one;, añades:

proxy_cache_key "$host$scheme$request_method$request_uri";

La diferencia es que ahora la clave incluye $host, que sí es el dominio que pidió el visitante:

DominioClave de caché
dominio-a.comdominio-a.comhttpsGET/
dominio-b.comdominio-b.comhttpsGET/

Claves distintas. Cada dominio tiene su propio espacio en caché. Fin del problema.

Después de editar:

# Limpiar el caché existente
rm -rf /www/server/nginx/proxy_cache_dir/*

# Comprobar que la config es válida
nginx -t

# Recargar
nginx -s reload

Y un Purge Everything en Cloudflare en ambos dominios para eliminar copias cacheadas con la respuesta incorrecta.

Lo que aprendí (o reaprendí)

Llevo años montando servidores y esta me pilló. Es un fallo que no da error en logs, no lanza ningún warning, nginx -t dice que todo está perfecto. Simplemente sirve contenido equivocado en silencio.

Hay varias cosas que ahora hago siempre:

Definir proxy_cache_key de forma explícita en cualquier servidor que aloje más de un dominio. El default de Nginx no incluye $host y eso es una bomba de relojería en entornos multidominio.

Comprobar que proxy_set_header Host $host está en cada bloque location que haga proxy_pass. Sin eso, Apache recibe un host incorrecto y puede devolver el VirtualHost por defecto.

Añadir este header para monitorizar el comportamiento del caché:

add_header X-Cache-Status $upstream_cache_status;

Con eso puedes ver si una respuesta fue HIT (caché), MISS (fue al backend) o BYPASS. Si ves un HIT en un dominio que acaba de cambiar de contenido, sabes que tienes un problema de clave.

Y sobre todo: no fiarte de las pruebas con el fichero hosts. Cambian demasiadas variables respecto al tráfico real como para que sean representativas.


SíntomaVarios dominios muestran el contenido de uno solo
Causaproxy_cache_key por defecto no incluye $host
Soluciónproxy_cache_key "$host$scheme$request_method$request_uri";
Tiempo real de corrección2 minutos, una vez identificado
Categorías: Web

0 comentarios

Deja una respuesta

Marcador de posición del avatar

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *