Gestionar la "cadena de confianza" de la IP real del usuario final

Antecedentes

Discourse debe conocer la dirección IP real del usuario final.

Sin embargo, un usuario final nunca se conecta directamente a Discourse, ya que siempre hay uno o más servidores web upstream (nginx ejecutándose en el contenedor de Discourse) de por medio. Por lo tanto, necesitamos una forma de transmitir esa información a Discourse de manera confiable.

La solución es el encabezado x-forwarded-for. En este tema describiré los mecanismos específicos para manejar adecuadamente esa información y cómo esperamos que se propague.

Plantillas

Las diversas plantillas para confiar en proxies upstream (por ejemplo, cloudflare.template.yml o fastly.template.yml) se han actualizado para usar nombres de archivo predecibles en outlets, en lugar de depender de la sustitución de texto (que es frágil).

Nombres de archivo

server/real-ip-header.conf

Este archivo contiene el encabezado que nginx, ejecutándose en el contenedor, usará como fuente de verdad, por ejemplo:

real_ip_header x-forwarded-for;

o como se establece en la plantilla de Cloudflare:

real_ip_header cf-connecting-ip;

server/real-ip-recursive.conf

Si este archivo existe, controla la recursividad al procesar el encabezado «real IP». Deberás habilitarlo si hay más de un proxy delante del contenedor de Discourse.

real_ip_recursive on;

Ejemplo:

Cloudflare → Balanceador de carga → Contenedor de Discourse (que es nginx + Discourse en sí)

Con esta configuración, nginx recibirá un x-forwarded-for que se verá así:

x-forwarded-for: real_end_user_ip, cloudflare_ip

en una conexión desde una IP de balanceador de carga.

Para procesar esto, primero nginx determina si la dirección de origen de la conexión (la IP del balanceador de carga) es confiable (ver set_real_ip_from) y, de ser así, procesará la última IP del encabezado x-forwarded-for.

Dado que esa dirección IP es cloudflare_ip, nginx luego debe hacer esto otra vez, verificando si cloudflare_ip es confiable y usando la siguiente dirección IP, que es real_end_user_ip.

server/set-real-ip-from-ENVIRONMENT.conf

Este archivo contiene directivas que le dicen a nginx qué direcciones IP confiar, y podemos tener tantos archivos y directivas como sea necesario.

Las plantillas en discourse_docker crean estos archivos según sea necesario (por ejemplo, set-real-ip-from-cloudflare.conf) y si tienes necesidades adicionales, puedes agregar los tuyos propios.

Ejemplo:

Si estás ejecutando en AWS y tienes un ALB delante del contenedor de Discourse, puedes agregar un archivo adicional añadiendo lo siguiente a tu definición de contenedor (adaptado a tu entorno):

run:
  - file:
      path: /etc/nginx/conf.d/outlets/server/set-real-ip-from-aws.conf
      chmod: 644
      # La VPC de AWS es 10.42.0.0/16, confía en cualquier conexión de las redes del ALB
      contents: |
        set_real_ip_from 10.42.66.0/24;
        set_real_ip_from 10.42.67.0/24;
  - file:
      path: /etc/nginx/conf.d/outlets/server/real-ip-header.conf
      chmod: 644
      contents: |
        real_ip_header x-forwarded-for;
5 Me gusta

Tengo un archivo /data/lc-manager-playbook/discourse/docker-templates/allow-local-proxy.template.yml con

after_bundle_exec:
  - replace:
    filename: /etc/nginx/conf.d/discourse.conf
    from: "types {"
    to: |
      set_real_ip_from 192.168.1.0/24;
      set_real_ip_from 192.168.11.0/24;
      set_real_ip_from 172.16.0.0/12;
      set_real_ip_from 10.0.0.0/8;
      real_ip_recursive on;
      real_ip_header X-Forwarded-For;
      types {

Eso parece seguir funcionando hoy.

¿Hay alguna razón para no distribuir una plantilla como esta?

1 me gusta

Esta plantilla está perfectamente bien, funcionará hoy y en un futuro previsible.

Básicamente, es una cuestión de preparación para el futuro y resiliencia.

Si el archivo cambia en algún momento, es decir, si eliminamos o modificamos la cadena types { que estás buscando, simplemente dejará de funcionar.

Si la modificas para que utilice un punto de extensión before-server o server, seguirá funcionando incluso en ese caso.

2 Me gusta

¿Cuál es el propósito de establecer X-Forwarded-For en absoluto ahora? Está destinado/comúnmente a contener no solo la IP del cliente (en la medida en que se conoce) sino también la cadena de proxies.

En el mensaje del commit escribes:

y podría terminar usando `client_ip` o `proxyA_ip` dependiendo de la ruta de código.

¿No es esto un error en Discourse, no analizar/usar consistentemente el valor común de ese encabezado (o correctamente para cualquier tarea donde se analice), en lugar de que el valor del encabezado (y Nginx como proxy comúnmente agregando a él) sea un problema?

Si Discourse no necesita conocer la cadena de proxies, y la configuración prevista es en su lugar pasar solo la IP real del cliente (en la medida en que se conoce), entonces X-Forwarded-For pierde su propósito. El encabezado X-Real-IP ya está configurado, coincidiendo con el propósito, lo que ahora hace que X-Forwarded-For sea redundante.

Es justo, podríamos simplemente eliminarlo y llegar al mismo resultado (excepto… ver abajo)

El límite de la aplicación realmente es nginx en sí mismo, no Discourse ni rails. Por lo tanto, la decisión sobre exactamente qué proxies remotos confiar se toma en el punto de entrada de la aplicación, que es nginx. Luego puede pasar esa decisión a Discourse.

Por defecto, Rails solo confía en direcciones locales al procesar x-f-f, así que lo hacemos en un lugar diferente donde podemos controlarlo fácilmente.

De hecho, resulta que Rails ni siquiera mira el encabezado x-real-ip… los encabezados que mira son

  • forwarded
  • client-ip
  • x-forwarded-for

De alguna manera eso ha llegado desde

commit 21b562852885f883be43032e03c709241e8e6d4f (tag: v0.8.0)
Author: Robin Ward
Date:   Tue Feb 5 14:16:51 2013 -0500

    Lanzamiento inicial de Discourse

diff --git a/config/nginx.sample.conf b/config/nginx.sample.conf
new file mode 100644
index 00000000..62fabf4a
--- /dev/null
+++ b/config/nginx.sample.conf
…
+    proxy_set_header  X-Real-IP  $remote_addr;

Tendremos que hacer algunas investigaciones, pero por ahora la respuesta es “funciona”. Supongo que es así como terminamos en esta situación.

Parece que quizás un gem lo usa?

1 me gusta

Entendido, así que se trata más de lo que Rails u otras gemas hacen con los encabezados, y menos de lo que hace el código de Discourse.

Es interesante que Rails no utilice X-Real-IP, que probablemente sea menos común que X-Forwarded-For, pero sin duda más conocido que Forwarded y Client-IP :thinking:.

Probablemente X-Real-IP sea entonces obsoleto en la configuración de Nginx. ¿Discourse lo expande y usa junto con X-Forwarded-For en los registros, si lo interpreto correctamente? No pude encontrar ningún otro uso/mención explícito en el código:

Lo siguiente simplemente me pareció incorrecto de dos maneras cuando lo vi mientras depuraba la limitación de tasa compartida y registraba errores sobre la dirección IP de cliente «unix:» no válida después de nuestra actualización de Discourse (usamos un proxy de socket UNIX delante del contenedor y confiamos en X-Forwarded-For).

proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;

Pero entiendo el «funciona» para hacer de $remote_addr el único punto de verdad, y real_ip_header la forma canónica para que los administradores controlen la única dirección IP que Discourse/Rails recibe. Veo que ya se añadió a Serve Discourse from a subfolder (path prefix) instead of a subdomain :+1:.