处理终端用户真实IP的“信任链”

背景

Discourse 需要能够识别最终用户的真实 IP 地址。

然而,由于始终存在一个或多个上游 Web 服务器(运行在 Discourse 容器中的 nginx),最终用户从不直接连接到 Discourse。因此,我们需要一种以可信的方式将该信息传递给 Discourse 的方法。

x-forwarded-for 头信息就是解决方案。在本主题中,我将描述正确处理该信息的具体机制,以及我们期望它是如何传播的。

模板

用于信任上游代理的各种模板(例如 cloudflare.template.ymlfastly.template.yml)已更新为在 outlets 中使用可预测的文件名,而不是依赖文本替换(这种方式容易出错)。

文件名

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),如果是,则处理 x-forwarded-for 头信息中的最后一个 IP。

由于该 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 中运行,并且在 Discourse 容器前面有一个 ALB,你可以通过将以下内容添加到你的容器定义中(根据你的环境进行调整)来添加一个额外的文件:

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-serverserver 输出,即使在那种情况下它仍能继续工作。

2 个赞

那么现在设置 X-Forwarded-For 的目的是什么?它通常旨在包含不仅限于客户端 IP(就已知而言),还包括代理链。

你在提交信息中写道:

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

这难道不是 Discourse 的 bug 吗?即没有一致地(或在解析该标头的任务中正确地)解析/使用该标头的通用值,而不是标头的值(以及 Nginx 作为代理通常附加到其中的内容)本身有问题?

如果 Discourse 不需要知道代理链,且预期的配置是仅传递真实的客户端 IP(就已知而言),那么 X-Forwarded-For 就失去了其意义。X-Real-IP 标头已经设置,符合该目的,从而使 X-Forwarded-For 变得多余。

这确实是个好问题,我们直接删除它也能得到相同的结果(不过……请看下文)

应用边界实际上是 nginx 本身,而不是 Discourse 或 Rails。因此,关于信任哪些远程代理的具体决策是在应用入口点(即 nginx)做出的。然后,nginx 可以将该决策传递给 Discourse。

默认情况下,Rails 在处理 x-f-f 时只信任本地地址,因此我们在另一个可以方便控制的地方进行处理。

实际上,Rails 甚至不查看 x-real-ip header……它查看的 header

  • 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;

我们需要进一步调查,但目前的回答是“它能工作”。我想这就是我们最初变成这样的原因。

看起来可能是某个 gem 在使用它?

1 个赞

明白了,所以这更多是关于 Rails 或其他 gem 如何处理 headers,而不是 Discourse 代码本身的行为。

有趣的是,Rails 并没有使用 X-Real-IP,它可能不如 X-Forwarded-For 常用,但肯定比 ForwardedClient-IP 更为人熟知 :thinking:

那么 X-Real-IP 在 Nginx 配置中可能已经过时了。如果我的理解正确的话,Discourse 在日志中会同时使用 X-Real-IPX-Forwarded-For 来扩展记录?我在代码中没有找到其他显式的使用或提及:

我在调试共享速率限制并记录关于无效“unix:”客户端 IP 的错误时,看到下面的配置觉得有两处不对劲(我们在容器前面使用了 UNIX socket 代理,并且依赖 X-Forwarded-For):

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

但我理解让 $remote_addr 成为唯一的真实来源,并通过 real_ip_header 作为管理员控制 Discourse/Rails 获取单个 IP 的标准方式,确实能“正常工作”。我看到这个方案已经在 Serve Discourse from a subfolder (path prefix) instead of a subdomain 中被添加进来了 :+1: