Why am I getting infinite redirect loop with force_ssl in my Rails app?

19,373

Solution 1

You're not forwarding any information about whether this request was an HTTPS-terminated request or not. Normally, in a server, the "ssl on;" directive will set these headers, but you're using a combined block.

Rack (and force_ssl) determines SSL by:

  • If the request came in on port 443 (this is likely not being passed back to Unicorn from nginx)
  • If ENV['HTTPS'] == "on"
  • If the X-Forwarded-Proto header == "HTTPS"

See the force_ssl source for the full story.

Since you're using a combined block, you want to use the third form. Try:

proxy_set_header X-Forwarded-Proto $scheme;

in your server or location block per the nginx documentation.

This will set the header to "http" when you come in on a port 80 request, and set it to "https" when you come in on a 443 request.

Solution 2

Try setting this directive in your nginx location @unicorn block:

proxy_set_header X-Forwarded-Proto https;

I had this same issue and investigating the Rack middleware handler (not force_ssl but similar) I could see that it was expecting that header to be set to determine if the request was already processed as being SSL by nginx.

Share:
19,373
Jakub Arnold
Author by

Jakub Arnold

Experienced software engineer with a background in machine learning and computer science and over 7 years of commercial practice of software development, looking to work on production quality software.

Updated on June 02, 2022

Comments

  • Jakub Arnold
    Jakub Arnold almost 2 years

    I want to have my API controller use SSL, so I added another listen directive to my nginx.conf

    upstream unicorn {
      server unix:/tmp/unicorn.foo.sock fail_timeout=0;
    }
    
    server {
      listen 80 default deferred;
      listen 443 ssl default;
      ssl_certificate /etc/ssl/certs/foo.crt;
      ssl_certificate_key /etc/ssl/private/foo.key;
    
      server_name foo;
      root /var/apps/foo/current/public;
    
      try_files $uri/system/maintenance.html $uri/index.html $uri @unicorn;
    
      location @unicorn {
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $http_host;
        proxy_redirect off;
        proxy_pass http://unicorn;
      }
    
      error_page 502 503 /maintenance.html;
      error_page 500 504 /500.html;
      keepalive_timeout 5;
    }
    

    which passes the nginx conftest without any problems. I also added a force_ssl directive to my ApiController

    class ApiController < ApplicationController
      force_ssl if Rails.env.production?
    
      def auth
        user = User.authenticate(params[:username], params[:password])
        respond_to do |format|
          format.json do
            if user
              user.generate_api_key! unless user.api_key.present?
              render json: { key: user.api_key }
            else
              render json: { error: 401 }, status: 401
            end
          end
        end
      end
    
      def check
        user = User.find_by_api_key(params[:api_key])
        respond_to do |format|
          format.json do
            if user
              render json: { status: 'ok' }
            else
              render json: { status: 'failure' }, status: 401
            end
          end
        end
      end
    end
    

    which worked just fine when I wasn't using SSL, but now when I try to curl --LI http://foo/api/auth.json, I get properly redirected to https, but then I keep on getting redirected to http://foo/api/auth ending in an infinite redirect loop.

    My routes simply have

    get "api/auth"
    get "api/check"
    

    I'm using Rails 3.2.1 on Ruby 1.9.2 with nginx 0.7.65

  • Chris Heald
    Chris Heald about 12 years
    This will mark all requests as SSL requests, even port 80 requests, since it's a combined server block.
  • A Fader Darkly
    A Fader Darkly about 8 years
    Pure magic. :) In my case I was moving a Rails site over to using Varnish. The site was pure HTTPS, and as Varnish doesn't support SSL and Passenger exposes a socket rather than a port, two Nginx server configs were required, one either side of Varnish. Distancing the Passenger socket connection from the HTTPS config on port 443 caused a redirect loop. And this fixed it. Thank you!
  • Cheyne
    Cheyne over 4 years
    Spent hours looking for this answer. Thank you!