Обработка "цепочки доверия" реального IP-адреса конечного пользователя

Введение

Discourse должен знать реальный IP-адрес конечного пользователя.

Однако конечный пользователь никогда не подключается к Discourse напрямую, поскольку всегда присутствует один или несколько вышестоящих веб-серверов (nginx, работающий в контейнере Discourse). Таким образом, нам необходим способ надежной передачи этой информации в Discourse.

Решением является заголовок x-forwarded-for. В этой теме я опишу конкретные механизмы корректной обработки этой информации и то, как мы ожидаем её распространения.

Шаблоны

Различные шаблоны для доверия вышестоящим прокси-серверам (например, cloudflare.template.yml или fastly.template.yml) были обновлены, чтобы использовать предсказуемые имена файлов в outlet-ах, вместо того чтобы полагаться на подстановку текста (которая является ненадежной).

Имена файлов

server/real-ip-header.conf

Этот файл содержит заголовок, который nginx, работающий в контейнере, будет использовать как источник истины, например:

real_ip_header x-forwarded-for;

или как указано в шаблоне Cloudflare:

real_ip_header cf-connecting-ip;

server/real-ip-recursive.conf

Если этот файл существует, он управляет рекурсией при обработке заголовка «реального IP». Вам нужно включить это, если перед контейнером Discourse стоит более одного прокси-сервера.

real_ip_recursive on;

Пример:

Cloudflare → Балансировщик нагрузки → Контейнер Discourse (который включает nginx и сам Discourse)

При такой настройке nginx получит x-forwarded-for, который будет выглядеть так:

x-forwarded-for: real_end_user_ip, cloudflare_ip

при подключении с IP-адреса балансировщика нагрузки.

Для обработки этого, сначала nginx определяет, является ли адрес источника подключения (IP-адрес балансировщика нагрузки) доверенным (см. set_real_ip_from), и если да, обрабатывает последний IP-адрес в заголовке x-forwarded-for.

Поскольку этот IP-адрес — cloudflare_ip, nginx должен сделать это снова, проверяя, является ли cloudflare_ip доверенным, и используя следующий IP-адрес, который является real_end_user_ip.

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

Этот файл содержит директивы, указывающие nginx, каким IP-адресам доверять, и мы можем иметь столько файлов и директив, сколько необходимо.

Шаблоны в discourse_docker создают эти файлы по мере необходимости (например, set-real-ip-from-cloudflare.conf), и если у вас есть дополнительные потребности, вы можете добавить свои собственные.

Пример:

Если вы работаете в AWS и у вас есть ALB перед контейнером Discourse, вы можете добавить дополнительный файл, добавив следующее в определение вашего контейнера (адаптированное под вашу среду):

run:
  - file:
      path: /etc/nginx/conf.d/outlets/server/set-real-ip-from-aws.conf
      chmod: 644
      # AWS VPC — 10.42.0.0/16, доверяем любым подключениям из сетей 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 лайков

У меня есть файл /data/lc-manager-playbook/discourse/docker-templates/allow-local-proxy.template.yml со следующим содержимым:

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 {

Похоже, что он до сих пор работает.

Есть ли какая-то причина не распространять такой шаблон?

1 лайк

Этот шаблон вполне подходит, он будет работать сегодня и в обозримом будущем.

По сути, дело в будущей совместимости и устойчивости.

Если файл когда-либо изменится, то есть если мы удалим или изменим строку types {, которую вы ищете, он просто перестанет работать.

Если вы измените его, чтобы использовать вывод before-server или server, он продолжит работать даже в этом случае.

2 лайка

Какова сейчас цель установки X-Forwarded-For вообще? Предполагается, что он содержит не только IP-адрес клиента (насколько он известен), но и цепочку прокси.

В сообщении к коммиту вы пишете:

and might end using `client_ip` or `proxyA_ip` depending on codepath.

Разве это не баг в Discourse, который не парсит/использует общепринятое значение этого заголовка последовательно (соответственно, правильно для той или иной задачи, где он парсится), а проблема заключается в значении заголовка (и в том, что Nginx как прокси обычно добавляет к нему данные)?

Если Discourse не нуждается в знании цепочки прокси, а предполагаемая настройка заключается в передаче только истинного IP-адреса клиента (насколько он известен), то X-Forwarded-For теряет свой смысл. Заголовок X-Real-IP уже установлен, соответствующий этой цели, что делает X-Forwarded-For избыточным.

Справедливое замечание, мы могли бы просто удалить его и получить тот же результат (кроме… см. ниже)

Граница приложения на самом деле находится в самом nginx, а не в Discourse или rails. Поэтому решение о том, каким именно удалённым прокси доверять, принимается на входе в приложение, то есть в nginx. Затем он может передать это решение в Discourse.

По умолчанию Rails доверяет только локальным адресам при обработке x-f-f, поэтому мы делаем это в другом месте, где мы можем легко это контролировать.

На самом деле, оказывается, Rails даже не смотрит на заголовок x-real-ip… заголовки которые он смотрит это

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

Неизвестно как это дошло до самого начала

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

    Initial release of 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;

Придётся покопаться, но пока что ответ «оно работает». Думаю, именно так мы и оказались в такой ситуации.

Похоже, может быть, какой-то гем его использует?

1 лайк

Понял, так что дело больше в том, как Rails или другие гемы обрабатывают заголовки, а не в том, что делает код Discourse.

Интересно, что Rails не использует X-Real-IP, который, вероятно, встречается реже, чем X-Forwarded-For, но определённо более известен, чем Forwarded и Client-IP :thinking:.

Возможно, X-Real-IP теперь устарел в конфигурации Nginx. Discourse использует его вместе с X-Forwarded-For в логах, если я правильно понимаю? Я не нашёл других явных упоминаний или использования в коде:

Нижеприведённый фрагмент выглядел неверным по двум причинам, когда я наткнулся на него при отладке общей ограничения частоты запросов (rate limiting) и записывал ошибки о невалидном IP-адресе клиента «unix:» после обновления нашего Discourse (мы используем прокси через UNIX-сокет перед контейнером и полагаемся на X-Forwarded-For).

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

Но я понимаю аргумент «это работает», чтобы надёжно сделать $remote_addr единственным источником истины, а real_ip_header — каноническим способом для администраторов контролировать единственный IP-адрес, который получает Discourse/Rails. Вижу, что это уже было добавлено в Serve Discourse from a subfolder (path prefix) instead of a subdomain :+1:.