Using Rails with SSL on Localhost

Although most simple apps will operate just fine running on http://localhost:3000 (the default for Rails), it is often advantageous to run your local development on https, or using SSL/TLS. You may need to do this if you are developing an app that needs to be developed on https locally, for example, using advanced browser features like accessing the computer’s camera or other operations which Chrome forces you do on an https connection.

This guide will walk you through the steps to make that happen, and offer some useful added tips to make the process smooth.

1/ Decide Where Your Local Domain Will Be

This will be the made-up domain you will use for local development. Although it can be anything, I recommend it always ends with .localhost or .local . That way it’s easy to tell when you are doing development on your local machine.

Let’s assume the app I’m building is called “Abc”. For this and the rest of the examples, I will use abc.localhost as my preferred localhost domain. That means we’ll set up for development at abc.localhost, generate a self-signed key for abc.localhost, etc.

2/ Add your Domain to your /etc/hosts File

You will use your preferred shell editor (Vi or Emacs) to edit the /etc/hosts file.

127.0.0.1   abc.localhost

After editing /etc/hosts, you may already have the DNS request for that domain cached. You can clear the cached entry by running dscacheutil -flushcache on the command line.

Important: When editing your /etc/hosts file, never change the top of the file where it says:

127.0.0.1 localhost

You also should add your entries to the end of the file, not to the beginning of it, and do not put entries above this line for localhost (it is used by the system).

3/ Create The SSL Config

In your existing Rails app, make a folder at config/ssl/ (The config/ folder exists by default, the ssl/ folder does not.)

In this folder, add a file called openssl.config

You will need this file only one time, in the next step, but you can check it into your repository as an artifact (a saved thing) in your app. Be sure to replace “abc.localhost” with your domain in the 3 spots in your file where is used and all of the rest of the examples.

[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
CN = abc.localhost
[v3_req]
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = abc.localhost
DNS.2 = *.abc.localhost

4/ Generate the KEY and CRT Files

openssl req -x509 -sha256 -nodes -newkey rsa:2048 -days 365 -keyout config/ssl/abc.localhost.key -out config/ssl/abc.localhost.crt -config config/ssl/openssl.config

Here, we are using OpenSSL to generate a new key and CRT file. This key is generated with an RSA 2048 signature (this refers to the strength of the encryption used) and is valid for 365 days from the day you generate it (so you’ll need to regenerate it once a year). You are telling OpenSSL to write your new key directly into a file at config/ssl/abc.localhost.key and the CRT file into a file at config/ssl/abc.localhost.crt. You are telling OpenSSL to use the config file (already created in step 3) at config/ssl/openssl.config

(Note that the -out and -keyout flags tell OpenSSL where to write to the file whereas -config specifies which file OpenSSL should read from. In this example, they just happen to be the same folder but this is just because of the way I set it up.)

Confirm that you now have your new keys at config/ssl/abc.localhost.key and config/ssl/abc.localhost.crt

5/ Trust the Self-Signed Certificate In Your Keychain

Whereas Steps 1-4 can be done by just you or one developer, this step must be done but all developers on your team. For this reason, be sure to put this specific instruction into the README file of your app:

sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain config/ssl/abc.localhost.crt

Here you are adding the self-signed certificate you just made to your macOS keychain.

6/ Enable Invalid Certs in Chrome

In Chrome, browse to: chrome://flags/

Search for “insecure” and you should see the option to “Allow invalid certificates for resources loaded from localhost.” Enable that option and then scroll down to the “Restart” button at the bottom of this window.

Configure allowing of insecure SSL certificates in Chrome

7/ Force Your Rails App To Use SSL in Development

Add to your config/development.rb file:

config.force_ssl = true

This will force all connections to be SSL. If they come in as non-SSL connections, Rails will automatically redirect to the HTTPS website. Important: Chrome will cache the redirect, meaning if you test it before making this change or unmake this change, Chrome will cache the fact that it saw the redirect previously and will re-use that redirect even when it is no longer present.

The way around this is to test in an Incognito Window.

8/ Tell Rails to Accept the Special Host

Add to your config/environments/development.rb file:

config.hosts << "abc.localhost"

9/ Start Your Rails Server With the Self-Signed Cert

If you normally start your Rails server using rails server, now you must do this:

bin/rails s -b 'ssl://0.0.0.0:3000?key=config/ssl/abc.localhost.key&cert=config/ssl/abc.localhost.crt'

If you are using ./bin/dev (using JSBundling or Shakapacker) to start your server, that means you are using Foreman, a tool that starts several services at once.

In this case, go into your Procfile.dev file, and change the web line to this:

web: bin/rails s -b 'ssl://0.0.0.0:3000?key=config/ssl/abc.localhost.key&cert=config/ssl/abc.localhost.crt'

Now, start your Rails server as you normally would with ./bin/dev

(If you are not doing peer-to-peer connections, you can instead use the syntax ssl://abc.localhost:3000 in Procfile above)

If you use abc.localhost instead of 0.0.0.0, you will not be able to access it from another machine on your local network, which is necessary for peer-to-peer connections. See below for more discussion on peer-to-peer considerations.

10/ Go to https://abc.localhost:3000 in Your Browser

You should now see your website at https://abc.localhost:3000 in your browser with no errors. Confirm that the site is loading on an SSL connection by looking for the browser’s lock symbol.

TROUBLESHOOTING:

if you get:

 Puma compiled without SSL support (RuntimeError)

That means that you are on a version of Mac OS that does not ship with OpenSSL support. To fix:

brew install openssl
gem uninstall puma

If it asks you which version of Puma to uninstall, choose All Versions.

bundle install

Also, you may need to try this from this SO post:

ruby -rpuma -e "puts Puma.ssl?"
gem install puma
ruby -rpuma -e "puts Puma.ssl?"

Addendum: Peer-to-Peer Considerations

If you are going to do peer-to-peer development using your primary computer as a rails server and connecting to it from another machine on your local network, there are some additional considerations. You will need this for testing peer-to-peer connections. For these examples, I will call the computer running the rails server as “the host” machine and the other one as “the client.” In this example, my local router is configured at a network that looks like 192.168.1.x (the DHCP-enabled router uses 192.168.1.1 as its own address and can give up to 255 local IP addresses in this range.) My host machine‘s local IP address is 192.168.1.2 and my client machine is running at 192.168.1.3

1/ Confirm that the two peers can see each other

Some networks allow peer computers to “see” (or be able to communicate with). Some networks don’t. Whether yours does depends on your IT setup— your router. You should 1) Use ping from the client computer to ping the host machine. For example, ping 192.168.1.2

Confirm that your Firewall on the host machine is either not turned on or allows for port 3000 to receive incoming connections.

2/ Instead of .localhost TLD, use .local

This is because .localhost TLDs are particular in the operating system. You can and should use .local for both the host and the client machine. (If you already configured .localhost for just the host machine, you will need to re-do all of the steps above.)

In Step #2 (above), on the host machine, you will add to /etc/hosts entry pointing to 127.0.0.1 (“the loopback address”)

127.0.0.1         abc.local

On the client machine, you will add the local IP address of the host machine. Remember, in our example the host machine operates at a local address of 192.168.1.2.

So on the client machine (the other one), enter this in the /etc/hosts

192.168.1.2       abc.local

In Steps #3 & #4 above, you will create the self-signed certificate for a domain that ends with .local (for example, abc.local).

Steps #5 & #6 (above) must be done on both machines. Remember that to trust the certificate abc.local you’ll need to copy the file abc.local.crt to the client machine itself, either manually or by cloning your repo onto the client machine. Then run the add-trusted-cert command on the client machine.

sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain config/ssl/abc.local.crt

In steps #8, #9, and #10 above you will use the .local version of your self-signed cert domain.

3/ Start Your Rails Server at 0.0.0.0

Booting Rails+Puma has a prickly two-pronged mechanism that decides which requests will receive a response: 1) The hosts setting in Rails (configured in development.rb) which is explained here in the Rails guides, and 2) the Puma address to bind on, let’s call it the “bound address.” (Puma is the default development server.)

The bound address is important because if the request is stopped there by Puma, it won’t even be logged into the Rails logs.

Normally, in development, this defaults to localhost. That means that only localhost and 127.0.0.1 will be responded to. When using self-signed certificates, you must tell Rails to accept the special domain name (Step #8 above).

You will also need to start Rails at 0.0.0.0, which is given in the default instructions in Step #9 above. (If you tell Puma to use the bound address of abc.local, the peer machine won’t be able to access it because the request comes into 192.168.1.2 and Puma won’t respond.)

On the host machine, the operating system will route abc.localhost to 127.0.0.1. Both versions of the domain will be accepted by Rails.

On the client machine, the request gets translated to the local IP address of the host machine (192.168.1.2 in the example above), and the Puma will allow only requests that match the bound address. Since the IP system doesn’t allow this, the only way to bypass the Puma-bound address mechanism is to use 0.0.0.0 (not the local domain which you have configured your self-signed cert for). That’s because 0.0.0.0 specifically allows requests to come in on any domain.

The default instructions in Step #9 above instruct you to boot your Rails server at 0.0.0.0 anyway.