在首次设置网站时排查"BAD CSRF"错误?

在一台已运行现有 Discourse 论坛(运行平稳,无错误)的专用物理服务器上,以独立的 Docker 容器部署一个新的 Discourse 论坛。

引导过程无错误,初始的“您已成功安装 Discourse!”消息正常显示,且我可以输入所需的用户名和密码而没有任何错误。然而,在提交表单后,Discourse 没有发送初始注册邮件,而是显示一个空白页面,左上角显示 [\"BAD CSRF\"]

我甚至不确定该从哪里开始排查问题。在此处的 meta 上搜索并未找到任何看似相关的结果。

关于应该从哪里开始排查,有什么建议吗?

It’s likely a problem with whatever is doing https. How’s that configured?

The web server hosts 4 domains and seven separate web sites, so universal SSL termination is done by HAProxy so I can keep the layers separated and provide caching even to SSL content.

The stack is HAProxy → Varnish (cache) → nginx reverse proxy → Discourse.

Worth noting that I had no issues setting up the first Discourse forum under this same configuration.

Edited to add - client connections are over https, but I’m proxying from nginx to the docker container’s HTTP port, not HTTPS (again, doing what works for the first Discourse instance). I can try changing that to the HTTPS port to see what happens, though, if that’ll help.

edit^2 - no, that didn’t help.

Looking through the Discourse production.log and this is what I see:

Started POST "/finish-installation/register" for 2601:2c4:c700:745f:216:3eff:0:11 at 2018-09-12 19:21:26 +0000
Processing by FinishInstallationController#register as HTML
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"[redacted]", "email"=>"redacted", "username"=>"redacted", "password"=>"[FILTERED]", "commit"=>"Register"}
Can't verify CSRF token authenticity.
  Rendering text template
  Rendered text template (0.0ms)
Filter chain halted as :verify_authenticity_token rendered or redirected
Completed 403 Forbidden in 2ms (Views: 0.3ms | ActiveRecord: 0.0ms)

Still looking through the other threads on meta where Can't verify CSRF token authenticity has come up.
I also see the 403 response in Chrome’s console:

Request URL: https://(forum url)/finish-installation/register
Request Method: POST
Status Code: 403 
Remote Address: [2607:fad0:3524:1::8]:443
Referrer Policy: strict-origin-when-cross-origin

This happens when SSL is badly configured. Most of the times a header is missing from the reverse proxy config.

2 个赞

^ this - the host header has to remain intact throughout, otherwise encryption can’t be established. Is it all IP/port based behind HAProxy?

Out of curiosity why are you using HAProxy in front of Varnish and then nginx behind?

Right, and my first guess was to make sure X-Forwarded-Proto was being appended properly by the reverse proxy—and it is. That’s the annoying thing here—the configuration between the working forum and the new one is identical.

And when I say “identical” I mean literally using the exact same processes and config files :smiley: They’re both on the same server, so other than the nginx configuration file in sites-available they’re even using the exact same set of configuration files. Everything’s the exact same.

The nginx config is pretty short—hard for me to screw that up:

server {
	server_name [redacted];
	listen 8881;
	listen 8882 http2;

	sendfile on;

	location / {
		access_log off;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header Host $http_host;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Proto https;
		proxy_redirect off;
		proxy_pass http://localhost:7996;
	}
}

The only differences between it and the config for the working forum is the server_name directive and the port I’m passing to.

Is HAProxy using a SAN cert, or unique IPs and separate certificates?

The overall config goals were to 1) encrypt everything and 2) cache everything. These are obviously opposing goals, so the way I went about it was to stratify things: SSL termination first, then a cache layer, then a web server that both serves static stuff and also functions as a reverse proxy as needed (for wordpress, discourse, and a few other things).

I initially had a fair amount of trouble with the “nginx sandwich” approach (nginx → varnish → nginx)—getting two separate instances of nginx to work properly with Upstart on ubuntu 14.04 proved to be very difficult and required a lot of screwing around, so I ditched nginx as my ssl termination layer and went with haproxy instead. If I were redoing this now, I’d go with Hitch, but ripping out haproxy at this point would require some research on how to do the transition.

edit:

HAProxy is using separate LetsEncrypt certificates (maintained via acme.sh), one per host. This is done mainly because the number of sites being hosted has changed over time and changing/updating a single SAN certificate proved to be kind of a pain in the ass. Additionally, I have a couple of tenant sites that would prefer to keep their SSL configs as separate from mine as possible.

Fair enough, they’re just separate sites though in the same instance of NGINX, it’s quite a common setup. Due to the nature of the app Discourse doesn’t really respond well or need external caching, HAProxy is only going to give you some port-redirection-fu there, which Nginx also covers.

Did you enable force_https in the second site?

Is there an easy way to do that via config file editing? I can’t log into the new site yet—I can’t get past the initial admin user registration step.

Yeah, I’m aware of Discourse’s cache behavior—this config has been live for a number of years. Discourse is not the only tenant application on the box, though, so its requirements get added into the mix along with everything else’s, and pretty much everything else on the box is very cache-friendly.

I honestly hadn’t thought about doing this all with a single instance of nginx. That’s definitely an interesting suggestion, though I’d need to sit down and whiteboard out the flow. Initial connections on port 443 (or 80 redirected to 443), proxying to varnish, proxying from there to nginx on a different port, I suppose, though I’m wary to rely on a single application for all three layers here. Feels like isolating errors and fixing them becomes considerably more complex.

(I’m aware nginx has serviceable cache, but it lacks varnish’s rich purge/ban functionality and makes manual object invalidation into a giant pain.)

This should do it:

./launcher enter app
cd /var/www/discourse
rails c
SiteSetting.force_https = true
3 个赞

No joy - set to true:

[1] pry(main)> SiteSetting.force_https
=> true
[2] pry(main)> 

Stopped & restarted the docker container just to be sure (not sure if that’s necessary or not but figured it couldn’t hurt), but still receiving the same error.

So your setup is:

HAProxy → Varnish (cache) → nginx reverse proxy → Docker

And SSL termination happens at HAProxy, right? Is the HAProxy config the same for both sites? With same header injection?

Exact same—traffic for both sites is going through the same haproxy frontend and same backend. Not doing any header injection with haproxy—in fact, I’m using HAproxy in TCP mode so that I can pass traffic via proxy-protocol-v2 to varnish, which lets me offer full HTTP/2 from the nginx reverse proxy at the bottom of the stack.

I do response header injection via varnish (hsts, referrer-policy, x-frame-options, x-content-type-options, a few others), and request header injection (like x-forwarded-protocol) with nginx.

edit - I totally understand the limits of free support—if this is going to turn complicated, I’ll bang on it a while on my own this weekend when I’ve got some spare time (and more importantly, some latitude to break things a bit). My hope was that this was going to be something really simple—and it still might be!—but I don’t want to be a pain.

1 个赞

给所有遇到同样问题、急得撞墙的朋友。请阅读以下内容。

我也遇到了类似的问题。在我的情况下,流量经过 Cloudflare,然后是 Nginx,且论坛部署在子文件夹下。

最终,以下组合方案生效了:

  • 在 Cloudflare 上禁用该子文件夹的缓存
  • 添加以下 Nginx 配置块
    location /folder {
        proxy_ssl_server_name       on;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_pass      http://localhost:1357/folder;
    }
2 个赞

很高兴您解决了!测试站点是否也使用了 Cloudflare?

1 个赞

另一个站点也使用了 Cloudflare,但未启用缓存。新站点的根目录已通过页面规则启用了激进缓存,该设置同样适用于子文件夹,因此导致了此问题。

浏览器缓存 TTL:一个月,始终在线:开启,缓存级别:缓存所有内容,边缘缓存 TTL:2419200 秒

我认为以下头部设置也至关重要。

        proxy_set_header X-Forwarded-Proto https;
3 个赞