DiscourseConnect generates HTTP redirects even with force_https on

AI-generated summary

A user is experiencing issues with DiscourseConnect generating HTTP redirects despite having force_https enabled. The issue arises because the request from Traefik to the Discourse container is sent over plain-text HTTP, causing the Rails app to return a plain-text HTTP redirect.

The user thinks the issue lies in the application_controller#redirect_to_login code and suggests that Discourse should always generate HTTPS URLs when force_https is enabled.

Others suggest setting the X-Forwarded-Proto header to https to signal the protocol to NGINX. matiasgarciaisaia tried this but found that NGINX’s discourse.conf overrides the header. To solve the issue, matiasgarciaisaia edited their containers/app.yml to hard-code the headers to https.

supermathie points out that Discourse should honor the force_https setting, regardless of the reverse proxy setup. They provide code snippets showing that Discourse does indeed use the force_https setting to determine the base protocol for URLs.

I’m running Discourse behind Traefik in a custom setup - giving Discourse its own VM is not an option here.

My Discourse doesn’t have SSL/Let’sEncrypt templates enabled, since Traefik won’t let plain HTTP requests reach the container - it’s set to redirect HTTP requests to HTTPs.

I’m having issues setting up DiscourseConnect, because, since the Traefik -> nginx[Discourse] request is sent over plain-text HTTP (because nginx doesn’t have SSL set up), the rule in /etc/nginx/conf.d/discourse.conf that tries to preserve the proto, must be in http context makes Discourse (the Rails app) to receive a plain-text HTTP request, thus returning a plain-text HTTP redirect to /session/sso - even if I have force_https enabled.

I think that’s the bug: regardless of my setup, with force_https enabled, Discourse should always generate HTTPs URLs - which it’s not doing.

I think the offending code is application_controller#redirect_to_login, but I haven’t dug that much into Discourse source code to be sure.

Is this solvable in the code itself?

As a workaround, I’m trying to add a rule patching the nginx’s discourse.conf to remove that rule.

We should have some support for forwarded for headers you would use them to signal to NGINX what the origin is

Did you set

proxy_set_header X-Forwarded-Proto https;

What was easiest for me was to set an extra label in Discourse’s app.yml to tell my Traefik to add an X-Forwarded-Proto: https header, but then nginx would override that parameter with it’s own version.

And Discourse’s nginx config plays a role here:

There Discourse tries to guess the protocol from the original request (which, in my setup, is always plain-text since that’s what Traefik sends). And then uses that to set the X-Forwarded-Proto multiple times.

In the end, I edited my containers/app.yml to hard-code those headers to https:

run:
  - exec: echo "Beginning of custom commands"
  ## If you want to set the 'From' email address for your first registration, uncomment and change:
  ## After getting the first signup email, re-comment the line. It only needs to run once.
  # - exec: rails r "SiteSetting.notification_email='no-reply@forum.cabana.network'"
  - replace:
     filename: "/etc/nginx/conf.d/discourse.conf"
     from: /# attempt to preserve the proto, must be in http context\nmap \$http_x_forwarded_proto \$thescheme {\n  default \$scheme;\n  "~https\$" https;\n\}/
     to: |
       # force https scheme so Discourse generates HTTPs links and redirects (ie, `/login`)
  - replace:
     filename: "/etc/nginx/conf.d/discourse.conf"
     from: "$thescheme"
     global: "true"
     to: "https"
  - exec: echo "End of custom commands"

Once again, I think if there’s a force_https setting, Discourse-the-rails-app should honor it, regardless of what the reverse proxy or other parties handle or not.

This is how we do it on our hosting platform; we have a load balancer layer that sets X-Forwarded-Proto for the downstream nginx+Discourse to consume.

We don’t need any additional shenanigans to make it work - I’m not sure what’s going wrong for you here.

This is indeed what happens:

  def self.generate_sso(return_path = "/", secure_session:)
    sso = new(secure_session: secure_session)
    sso.nonce = SecureRandom.hex
    sso.register_nonce(return_path)
    sso.return_sso_url = Discourse.base_url + "/session/sso_login"
    sso
  end

and base_url comes from:

  def self.base_protocol
    SiteSetting.force_https? ? "https" : "http"
  end

  def self.base_url_no_prefix
    "#{base_protocol}://#{current_hostname_with_port}"
  end

  def self.base_url
    base_url_no_prefix + base_path
  end