反向代理和 HTTPS 背后的讨论

我尝试在 Apache 反向代理后面部署 Discourse,但无法正确配置 HTTPS。

在达到目前状态的过程中,我遇到了很多问题。目前,Discourse 运行在一台服务器上,前面有一台 Apache 服务器作为反向代理。最初,让 Discourse 在反向代理后运行就遇到了很多困难,因为 Discourse 总是试图重定向到 app.yaml 中设置的 hostname。

不过,不知怎么的,我现在让它运行起来了,但在浏览器中却出现了混合内容警告。

我在 Apache 中配置了从 HTTP 到 HTTPS 的重定向,这部分工作正常。但 Discourse 仍然通过 HTTP 提供部分内容,我似乎无法找到方法强制其改为 HTTPS。

例如,favicon 是通过 HTTP 提供的,而我不知道如何更改这一点。

我能否让 Discourse 将所有链接更改为 HTTPS,而无需让 Discourse 直接处理 HTTPS 流量?

我尝试在 Apache 中设置:

Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains"

但这似乎没有帮助。

在 Discourse 中勾选“强制 HTTPS”选项也无济于事,因为这会导致网站无法正常工作,因为它会忽略所有 HTTP 请求。

我该如何解决混合内容问题?

Apache2 会带来很多麻烦。建议切换到 nginx、caddy、traefik 或 haproxy。

2 个赞

我在测试环境中成功部署了 Apache2,将其作为反向代理连接到容器内的 Unix 套接字,过程“毫无问题”:

我找到的唯一区别(注意:仅测试了几个小时,尚不完整)是:

  • Apache2 无法与容器共享卷中指向 Unix 套接字的符号链接配合使用;
  • 在粗略测试中,Apache2 的速度稍慢,但差异不大。

就个人而言,我不喜欢针对技术的无谓争论;因此,我不同意“Apache2 会带来很多问题”的说法。在我的测试过程中,Apache2 并未出现任何负面问题。

以下是我使用 Apache2 的核心配置(HTTP 工作正常,顺便提一下,LETSENCRYPT 也完全兼容):

# cat discourse.example.conf
<VirtualHost *:80>
  ServerAdmin webmaster@localhost
  ServerName  discourse.example.com
  DocumentRoot /website/discourse

  RewriteEngine On
  ProxyPreserveHost On
  ProxyRequests Off
  ProxyPass / unix:/var/discourse/shared/socket-only/nginx.http.sock|http://localhost/
  ProxyPassReverse  / unix:/var/discourse/shared/socket-only/nginx.http.sock|http://localhost/
  ErrorLog /var/log/apache2/discourse.error.log
  LogLevel warn
  CustomLog /var/log/apache2/discourse.access.log combined

  RewriteCond %{SERVER_NAME} =discourse.example.com
  RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
</VirtualHost>

注意:我们唯一遇到问题的情况是,即使设置了 force_https,HTTP 仍然被提供,那是因为 /uploads 目录中缺少文件,但这(当然)与使用 Apache2 还是 nginx 作为反向代理无关。

1 个赞

谢谢您的回复,但我并没有在运行 Discourse 的同一台服务器上安装 Apache。我可能之前表达得不够清楚。

我有一台现有的 Apache 服务器,上面托管着多个网站。我需要它作为反向代理来转发位于另一台服务器上的 Discourse,因此无法使用套接字(sockets)。

谢谢。

我尚未尝试过这种方法,但你可以考虑挂载远程文件系统,看看是否能通过这种方式访问 Unix 套接字,特别是如果这些服务器位于同一数据中心,且广域网网络性能不是问题的话。

如果不提供具体的架构、操作系统详情、网络配置等信息,很难给出回复,甚至可能超出了 meta discourse 的讨论范围。

发挥创意吧!

1 个赞

上次我尝试用该配置配合 Apache2 时,连接 Discourse 消息总线失败。不过那已经是去年以前的事了。看起来站点加载正常,但在 F12 开发者控制台中明确显示,WSS 连接在几次初始尝试后超时。

1 个赞

在我发布的示例配置中可以清楚地看到,我们并未使用 wss

wss 并不等同于 apache2 反向代理(这只是其中一种实现方式,而我们并未使用 wss)。

事实上,我们仅使用基于 unix 域套接字nginxapache2 反向代理配置,原因如下:

  • 我比较懒,喜欢简单且易于调试的配置。
  • unix 域套接字 简单且易于调试。
  • nginx 中,我们可以通过符号链接在反向代理和任意容器之间切换。
  • apache2(反向代理到容器)不支持符号链接,因此需要重启 Web 服务器。

不过,@Grunskin 询问的是我们尚未配置的内容:在一台主机上进行反向代理,而在另一台主机上运行容器。

等我有空时,我会在同一数据中心内对 nginxapache2 进行测试,看看是否可以通过挂载远程文件系统并使用 unix 套接字 来实现这一功能。

在那之前……

注意:依我看,此问题与 nginxapache2 无关,因为它们仅充当反向代理(但如前所述,我们尚未测试远程访问配置,因此无法进一步评论)。

1 个赞

为什么这是必要的?

Discourse 是一个应用程序,而非静态网站。一旦初始的 JavaScript 负载已传输至您的浏览器,许多功能便依赖于与 Discourse 服务器的快速连接。通过其他系统进行代理会引入延迟,严重损害用户体验。

您能否说明一下您的需求背后的原因?

2 个赞

使用反向代理有很多理由。
例如,如果我只拥有一个公网 IP,却需要让多个 Web 服务器在 80/443 端口对外提供服务,而且我无法在这个特定的 Web 服务器上运行 Discourse(以此为例)。
将其用作 SSL 卸载,这样后端服务器就不必处理加密问题。
通过将后端服务器置于反向代理之后,可以减少其暴露的攻击面。
这有很多正当的理由,我实在想不通为什么这不可能实现。

过了一段时间,我想起自己已经有另一台服务器在 Apache 后面运行 Discourse,并且至少两年一直运行正常。我用同样的方式配置了新的 Discourse,但无法让它停止通过 HTTP 提供某些内容。这两台 Discourse 服务器唯一的区别是旧版运行的是 2.4 版本,而新版是 2.5 版本,因此我不确定这之间是否存在差异。

正如我在第一篇文章中提到的,force_https 会导致网站无法正常使用,使得无法登录、接受邀请等。看起来某些 JavaScript 无法运行,因为它们可能是通过 HTTP 提供的。
force_https 改为重写所有 HTTP 链接为 HTTPS,而不是直接丢弃它们,这样不是更合理吗?至少应该将其作为一个可选项。

公开部署 Discourse 的推荐方式是什么?是否应该在 DMZ 中部署一台拥有独立外部 IP 的服务器?

我建议看看 Traefik。它运行出色,并能自动管理 SSL 证书。

有很多理由需要在反向代理后面运行。我相当确定 Discourse.org 的基础设施就是在 HAproxy 后面运行的。

我使用 Traefik 和 Caddy Server,过去也成功配置过 nginx、HAproxy 甚至 Apache(用于子目录安装 WordPress 的查询)。

这就是你的问题所在。你需要启用 force_https 并找出它导致问题的原因。关闭它并不是一个可行的选项。你寻求的是免费支持,而回复你的人没有针对 Apache 的解决方案,因此你必须自己成为 Apache 团队的领头人。

2 个赞

是的,我们完全同意。

目前,除了用于主要迁移测试的服务器外,我们在所有站点(生产、测试和预发布环境)都采用“双容器加反向代理”的架构。

这是因为当 PostgreSQL、MySQL 和 Discourse 应用代码全部位于单个容器中时,运行各种迁移脚本会更加方便。这属于非生产环境,更易于调试。但当我们确认无误后,会将备份迁移到生产环境并进行恢复。

这样做存在一个问题:即使是超级完善的双容器加反向代理架构,也无法弥补数据库恢复期间的停机时间,因为目前只有一个数据库。

也许未来我们会尝试采用两个数据库容器的架构,这样我们就可以恢复其中一个,并在数据层面来回切换……

或者更好的方案是,我们将数据恢复到另一个名称的数据库中,并实时切换,但我们目前还不知道如何实现。如果您知道在代码的哪个位置可以将生产数据库的名称从 discourse 更改为 discourse2 而无需重新构建整个应用,那将非常有帮助 :slight_smile: 也许富有创意且创新的 @pfaffman 超级顾问知道?

更新:答案就在 templates/postgres.template.yml:slight_smile:

(我确实看到里面有一些有趣的 mv PostgreSQL 文件夹操作 :slight_smile: :)。)

独立部署和多容器部署各有优缺点;但对于生产环境,我们完全倾向于采用双容器加反向代理的配置。而在迁移测试和预发布环境中,独立部署对我们来说是最好的选择。

关于 Apache2,我希望能有时间在一台服务器上设置并测试反向代理,而在另一台服务器上运行容器。对此非常抱歉……

此外,我还需要想出一个办法,确保在双容器配置下进行数据库恢复时站点仍能保持在线。我现在注意到,templates/postgres.template.yml 模板主要是针对单个独立容器配置设计的。

1 个赞

只想补充一点,Discourse 并不使用 WSS。Message Bus 使用的是带有分块编码(流式传输)的长轮询。

4 个赞

你必须将该主机名设置为访问 Discourse 时使用的主机名。如果 app.yml 中的域名不是用户在浏览器中输入以访问你网站的那个域名,它将无法正常工作。

你的 app.yml 中没有 ssl 或 letsencrypt 模板,对吧?

你确实需要开启 force_https

此外,你还需要费一番周折才能让长轮询正常工作。我记不清具体步骤了。

1 个赞

你好 @Grunskin

我们理解你的沮丧。然而,当你设置:

force_https = true

这本身并不是 问题。正如 @pfaffman 在上面提到的(至少两次,他是 Meta 社区顶级的迁移专家之一):

你“必须”保持 force_https = true,然后找出真正的 问题

如果我是你,基于我所读到的内容:

首先,我会将反向代理设置在运行 Discourse 容器(或容器组)的同一台服务器上,以简化问题,这仅用于测试和故障排查。确保这个简单的测试用例能完美运行。然后,当它在同一主机上正常工作后,关闭该测试用的反向代理,再迁移到你期望的双服务器架构。

这是一个有趣的问题。只要你保持 force_https = true,并采用结构化、循序渐进的故障排查方法,你就能解决这个问题。这类问题不过是等待被破解的 IT 谜题。

你可以做到的。


附言:如果你对这个谜题感到沮丧或厌倦,也可以向 @pfaffman 或其他经验丰富的 Meta 社区成员支付一些费用,请他们帮你跨过这个障碍,迎接更广阔的前景。


备注:我们在同一台服务器上配置 Apache2 作为通过 Unix 域套接字通信的反向代理时,完全没有遇到任何问题。很抱歉,我目前没有个人时间去搭建双服务器环境,并让 Apache2 反向代理方案在两台不同的服务器上运行成功。我们正忙于最终的迁移任务,清理那些“疯狂的 BBCode 滥用”导致的 Markdown 问题,这比我们最初预想的要花费更多时间。

1 个赞

我决定采用在本地主机 8443 端口上通过 SSL 部署 Discourse,使用 UFW(防火墙)锁定 8443 端口,并通过 apache2 将 443 端口代理到 8443 端口的方案。正在尝试中。

                ProxyPass / "https://localhost:8443"
                ProxyPassReverse  / "https://localhost:8443"
1 个赞

我迁移了我们的 discourse 到新的设置,现在在登录时遇到 http 403 POST session 问题。我猜测是 CSRF 问题,但目前我不知道从哪里开始调试,而且 /var/log/discourse-var-log/production.log 和 production_error.log 文件大小都是 0 字节……

有什么办法可以正确调试这个问题吗?

当前设置:
1. haproxy 作为中心 https 负载均衡器/加速器(用于 discourse 和其他服务)

forum >> develd apache rev. proxy p.82
backend forum-backend
   mode http
   server forum.netzwissen.de 10.10.10.14:83 cookie A check
   http-request set-header X-Forwarded-Port %[dst_port]
   http-request add-header X-Forwarded-Proto https if { ssl_fc }
   # HSTS header, 16000000 秒:略多于 6 个月
   http-response set-header Strict-Transport-Security "max-age=16000000; includeSubDomains; preload;"

2. 本地 apache 作为反向代理

   <IfModule proxy_module>
    ## <https://meta.discourse.org/t/running-other-websites-on-the-same-machine-as-discourse/17247>
    ProxyPreserveHost On
    # ProxyRequests Off     
    RequestHeader set X-Forwarded-Proto expr=%{REQUEST_SCHEME}
    RequestHeader set X-Real-IP expr=%{REMOTE_ADDR}
    ProxyPass /  unix:/var/discourse/shared/web-only/apache.http.sock|http://localhost/
    ProxyPassReverse  / unix:/var/discourse/shared/web-only/apache.http.sock|http://localhost/
    </IfModule>

3) Discourse 使用独立的 web_only 和 data 容器运行,web_only 使用 - “templates/web.socketed.template.yml” 部署

这是登录时失败的会话请求:

POST /session HTTP/1.1
Host: forum.netzwissen.de
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:97.0) Gecko/20100101 Firefox/97.0
Accept: */*
Accept-Language: de,en-US;q=0.7,en;q=0.3
Accept-Encoding: gzip, deflate, br
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Content-Length: 89
X-CSRF-Token: dtV0N6faVQSWZsg6z9ZGOxQBjuTpBZk6tAMRxaXJdwozF1kObw9UuiFnxbLf5OGDeL1DWDgZ5W3oJP7CY+LwRw==
Discourse-Present: true
X-Requested-With: XMLHttpRequest
Origin: https://forum.netzwissen.de
Sec-Fetch-Dest: empty
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
Referer: https://forum.netzwissen.de/
Connection: keep-alive

来自 web_only 容器的 webserver 的回复:

HTTP/1.1 403 Forbidden
date: Sun, 13 Mar 2022 16:41:56 GMT
server: nginx
content-type: text/plain; charset=utf-8
x-frame-options: SAMEORIGIN
x-xss-protection: 1; mode=block
x-content-type-options: nosniff
x-download-options: noopen
x-permitted-cross-domain-policies: none
referrer-policy: strict-origin-when-cross-origin
vary: Accept
x-request-id: 778da942-3c1c-493b-946b-478984f53a8c
x-runtime: 0.003623
transfer-encoding: chunked
strict-transport-security: max-age=16000000; includeSubDomains; preload;

我对 csrf 和 nginx(外部 web 服务器是 apache 2.4)都不熟悉,但我很确定 CSRF 是我的问题,因为 discourse 在不登录的情况下运行正常,只有用于登录的 POST 请求在这里失败。我的中心 haproxy 的内部 IP 是 10.10.10.21,因此我在 web_only 容器的 discourse.conf 的 yml 中设置了

set_real_ip_from 10.10.10.21/24;

我也尝试使用默认的 127.0.0.1/24;,但两者在登录时都会导致相同的 403 错误。