Gerenciando a "cadeia de confiança" do IP real do usuário final

Contexto

O Discourse precisa ter conhecimento do endereço IP real do usuário final.

No entanto, um usuário final nunca se conecta diretamente ao Discourse, pois sempre há um ou mais servidores web upstream (nginx rodando no container do Discourse) em funcionamento. Assim, precisamos de uma maneira de passar essas informações para o Discourse de forma confiável.

O cabeçalho x-forwarded-for é a solução. Neste tópico, descreverei os mecanismos específicos para lidar adequadamente com essas informações e como esperamos que sejam propagadas.

Modelos

Os vários modelos para confiar em proxies upstream (por exemplo, cloudflare.template.yml ou fastly.template.yml) foram atualizados para usar nomes de arquivos previsíveis em outlets, em vez de depender de substituição de texto (que é frágil).

Nomes de arquivos

server/real-ip-header.conf

Este arquivo contém o cabeçalho que o nginx rodando no container usará como fonte de verdade, por exemplo:

real_ip_header x-forwarded-for;

ou conforme definido no modelo Cloudflare:

real_ip_header cf-connecting-ip;

server/real-ip-recursive.conf

Se este arquivo existir, ele controla a recursão no processamento do cabeçalho “IP real”. Você precisará habilitar isso se houver mais de um proxy na frente do container do Discourse.

real_ip_recursive on;

Exemplo:

Cloudflare → Balanceador de carga → Container do Discourse (que é nginx + Discourse em si)

Com essa configuração, o nginx receberá um x-forwarded-for que parece:

x-forwarded-for: ip_real_usuario_final, ip_cloudflare

em uma conexão de um IP de balanceador de carga.

Para processar isso, primeiro o nginx determina se o endereço de origem da conexão (o IP do balanceador de carga) é confiável (ver set_real_ip_from) e, se for, processará o último IP do cabeçalho x-forwarded-for.

Como esse endereço IP é ip_cloudflare, o nginx então precisa fazer isso novamente, verificando se ip_cloudflare é confiável e usando o próximo endereço IP, que é ip_real_usuario_final.

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

Este arquivo contém diretivas que dizem ao nginx quais endereços IP confiar, e podemos ter quantos arquivos e diretivas forem necessários.

Os modelos em discourse_docker criam esses arquivos conforme necessário (por exemplo, set-real-ip-from-cloudflare.conf) e, se você tiver necessidades adicionais, pode adicionar os seus próprios.

Exemplo:

Se você estiver executando no AWS e tiver um ALB na frente do container do Discourse, pode adicionar um arquivo adicional adicionando o seguinte à sua definição de container (adaptado ao seu ambiente):

run:
  - file:
      path: /etc/nginx/conf.d/outlets/server/set-real-ip-from-aws.conf
      chmod: 644
      # A VPC da AWS é 10.42.0.0/16, confie em qualquer conexão das redes do 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 curtidas

Tenho um /data/lc-manager-playbook/discourse/docker-templates/allow-local-proxy.template.yml com

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 {

Parece que ainda funciona hoje em dia.

Há alguma razão para não distribuir um modelo assim?

1 curtida

Este modelo está totalmente adequado, funcionará hoje e no futuro previsível.

Basicamente, preparação para o futuro e resiliência.

Se o arquivo mudar, ou seja, se removermos ou alterarmos a string types { que você está procurando, ele simplesmente deixará de funcionar.

Se você alterá-lo para usar um ponto de saída before-server ou server, ele continuará funcionando mesmo assim.

2 curtidas

Qual é o objetivo de definir X-Forwarded-For agora? É comum e pretendido que ele contenha não apenas o IP do cliente (na medida em que é conhecido), mas também a cadeia de proxies.

Na mensagem do commit, você escreve:

e pode acabar usando `client_ip` ou `proxyA_ip` dependendo do caminho do código.

Não seria isso um bug no Discourse, por não analisar/uso o valor comum desse cabeçalho de forma consistente (ou corretamente, dependendo da tarefa para a qual ele é analisado), em vez de o valor do cabeçalho (e o Nginx como proxy, que normalmente o anexa) ser o problema?

Se o Discourse não precisa conhecer a cadeia de proxies e a configuração pretendida é, em vez disso, passar apenas o IP real do cliente (na medida em que é conhecido), então o X-Forwarded-For perde seu propósito. O cabeçalho X-Real-IP já está definido, atendendo a esse objetivo, o que torna o X-Forwarded-For redundante.

Essa é uma boa observação, poderíamos simplesmente excluí-lo e chegar ao mesmo resultado (exceto… veja abaixo)

O limite da aplicação realmente é o próprio nginx, não o Discourse ou Rails. Portanto, a decisão sobre exatamente quais proxies remotos confiar é tomada no ponto de entrada da aplicação, que é o nginx. Ele pode então passar essa decisão para o Discourse.

Por padrão, o Rails só confia em endereços locais ao processar x-f-f, então fazemos isso em um lugar diferente onde podemos controlá-lo facilmente.

Na verdade, descobrimos que o Rails nem mesmo olha para o cabeçalho x-real-ip… os cabeçalhos que ele analisa são

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

De alguma forma, isso chegou até aqui desde o início…

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

    Lançamento inicial do 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;

Teremos que investigar mais, mas por enquanto a resposta é “funciona”. Acho que foi assim que acabamos nessa situação.

Parece que talvez algum gem o use?

1 curtida

Entendido, então trata-se mais do que o Rails ou outras gems fazem com os cabeçalhos, e menos do que o código do Discourse faz.

É interessante que o Rails não utilize X-Real-IP, que provavelmente é menos comum do que X-Forwarded-For, mas certamente mais conhecido do que Forwarded e Client-IP :thinking:.

Provavelmente X-Real-IP está então obsoleto na configuração do Nginx. O Discourse o usa junto com X-Forwarded-For nos logs, se eu interpretei corretamente? Não consegui encontrar nenhum outro uso/menção explícito no código:

O trecho abaixo pareceu errado de duas formas quando o vi durante a depuração da limitação de taxa compartilhada e os erros registrados sobre o IP de cliente “unix:” inválido após a atualização do nosso Discourse (usamos um proxy de socket UNIX à frente do container e dependemos de X-Forwarded-For).

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

Mas entendo o “funciona” para tornar $remote_addr o único ponto de verdade, e real_ip_header a maneira canônica para administradores controlarem o único IP que o Discourse/Rails recebe. Vejo que já foi adicionado em Serve Discourse from a subfolder (path prefix) instead of a subdomain :+1:.