February 19th, 2020

How to configure Rails 6 with force_ssl using Nginx and Let's Encrypt


force_ssl is a very useful setting in Ruby on Rails 6 (and below), but can be tricky to setup. In this guide I’ll walk you through how to configure it.

It’s very useful for security purposes, of which can be summed up nicely with this StackOverflow answer:

Quote on force_ssl

"It doesn't just force your browser to redirect HTTP to HTTPS. It also sets your cookies to be marked "secure", and it enables HSTS, each of which are very good protections against SSL stripping."

Read full answer

Overall it’s a great configuration for your Rails application to implement, and with the following guide, it’s quick to setup as well.

Covered in this guide:

  • Setting up force_ssl
  • Install and setup certificates on server

Let’s begin:


1. Enable force_ssl

In your Rails application, simply go to /config/enviorments/production.rb and update the following line (it may be commented out):

/config/enviorments/production.rb
Rails.application.configure do
    # ...

    # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
    config.force_ssl = true

    # ...
end

Then save and you’re set for now.

2. Configure Nginx

Heads up!

If you haven't already, take a moment to configure your server with Let's Encrypt SSL certificates. Here's a great tutorial.

When prompted to redirect all traffic to HTTPS or do nothing, choose "Do Nothing" (option 1). If you've already picked to redirect, we can fix it below.

Server Deployments

If you're looking for a good tutorial on how to deploy your Rails application, or need some more info on the nginx.conf configuration, check out my previous tutorial about it.

Once you’ve setup HTTPS with Certbot, we need to configure our nginx.conf file. For your nginx.conf file, update it with the following (adjusting with your appropriate configuration):

Production Server: /etc/nginx/sites-enabled
# This configuration uses Puma. If using another rack server, substitute appropriate values throughout.
upstream puma {
  server unix:///home/root/apps/app_name/shared/tmp/sockets/app_name-puma.sock;
}

# We need to be listing for port 80 (HTTP traffic). 
# The force_ssl option will redirect to port 443 (HTTPS)
server {
  listen 80 default_server deferred;

  # Update this
  server_name example.com;

  # Don't forget to update these, too.
  # For help with setting this part up, see:
  # http://localhost:4000/2018/09/18/deploying-ruby-on-rails-for-ubuntu-1804.html
  root /home/user_name/apps/app_name/current/public;
  access_log /home/user_name/apps/app_name/current/log/nginx.access.log;
  error_log /home/user_name/apps/app_name/current/log/nginx.error.log info;

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  try_files $uri/index.html $uri @puma;
  location @puma {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_redirect off;

    proxy_pass http://puma;
  }

  error_page 500 502 503 504 /500.html;
  client_max_body_size 10M;
  keepalive_timeout 10;
}

# This is the configuration for port 443 (HTTPS)
server {

  listen 443 ssl;

  # Update this
  server_name example.com;

  # Don't forget to update these, too.
  # I like to update my log files to include 'ssl' in the name.
  # If there's ever any need to consult the logs, it's handy to have HTTP and HTTPS traffic separated.
  root /home/user_name/apps/app_name/current/public;
  access_log /home/user_name/apps/app_name/current/log/nginx.ssl.access.log; # Updated file name
  error_log /home/user_name/apps/app_name/current/log/nginx.ssl.error.log info; # Updated file name

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  try_files $uri/index.html $uri @puma;
  location @puma {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

    # This is an important line to help fix some redirect issues.
    proxy_set_header X-Forwarded-Proto https; 
    
    proxy_set_header Host $http_host;
    proxy_redirect off;

    proxy_pass http://puma;
  }

  error_page 500 502 503 504 /500.html;
  client_max_body_size 10M;
  keepalive_timeout 10;

  # The following is most likely added by Certbot. Simply copy that config over to here.
  ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
  include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
  ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}

# If you chose Certbot to redirect all traffic to HTTPS, this will be in your current config. 
# Remove it or you'll run into redirection errors:
# server {
#     if ($host = example.com) {
#         return 301 https://$host$request_uri;
#     } # managed by Certbot
# 
# 
#   listen 80 default_server deferred;
#   server_name example.com;
#     return 404; # managed by Certbot
# }

Don’t forget to update your local nginx.conf file as well if it’s part of your deployments.

Once done, save it and let’s test to make sure it’s working:

Production Server Terminal
sudo nginx -t

If everything succeeds, reload the Nginx config:

Production Server Terminal
sudo systemctl reload nginx

Now simply deploy your new changes and everything will work!

Redirect Error

If you run into a "Redirection" error, it's most likely due to Certbot and Rails fighting trying to redirect all traffic to HTTPS (port 443). Make sure your nginx.conf file isn't redirecting.