Using SSL with Passenger in Development on macOS

This blog post will show you how to create, install, and trust a self signed certificate on your development web server, and setup your DNS to redirect all your development traffic to your development box.

Not on macOS? Check out our Library article for more configurations!

Why use SSL in development?

It is a good idea to have your development environment be as close to your production environment as possible, while remaining convenient. For example setting up SSL in your development environment helps you to notice and fix mixed content warnings so there are no surprises when you move to staging or production. It also doesn't train you to click through SSL warnings in your browser.

If you already have a production SSL certificate, using it in development could be achieved as simply as starting Passenger with:

sudo passenger start --ssl --environment development --ssl-certificate "/path/to/certificate.pem" --ssl-certificate-key "/path/to/key.pem" --port 443

Why use self-signed certificates?

The previous approach has some downsides however: Your production certificate and secret key are sitting on an actively used box, which makes them easier to steal. You need a production certificate before you are even done with your development work. You need to edit your hosts file every time you start a new site, and you need to edit your hosts file again any time you want to access the production website once it is published.

Instead of dealing with the aforementioned downsides, we will instead create and trust our own certificate authority, request and then sign a certificate for our development server, and setup a local DNS server to handle development web traffic.

Prepare the System

Before you get started we assume you have the following requisites: you are using the Bash shell, have admin rights to your computer, and are using Chrome as your browser.

We will be using homebrew to simplify installing the software packages that we need. Homebrew handles the compilation from source, or more often simply provides a binary, and keeps all of your installed packages in a single directory, making sure they are available on your path.

 # if you haven’t already installed homebrew, run this command and follow the prompts (the defaults are sane and what we’ll be assuming here)
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
# start here if homebrew is already installed
brew update
brew upgrade
brew install openssl passenger dnsmasq

Software versions used in this article:

To check the versions of the packages you have installed you can use the following commands:

Application Version Version Check
OpenSSL 1.0.2h openssl version
Passenger 5.0.29 passenger -v
Dnsmasq 2.76 dnsmasq -v
Chrome 51.0.2704.* /Applications/Chrome.app/Contents/MacOS/Google\ Chrome --version

OpenSSL Configuration

When setting up a development environment with SSL it is better to use a self-signed certificate, and leave your real certificate/key pair somewhere safe and encrypted, preferably on offline storage like a thumb drive in a safe place. In order to create a certificate that works and makes development convenient, you need to edit the openssl.cnf file.

This file can be located with ls `brew --prefix`/etc/openssl/openssl.cnf. If it isn't there, or the configuration file is from a much older version of openssl (homebrew doesn't update config files when you update packages) you can copy the default config cp `brew --prefix`/etc/openssl/openssl.cnf{.default,} to that path as a starting point, and edit from there.

Also be aware that macOS has a highly modified version of OpenSSL 0.9.8 bundled with the operating system, which won't work for our purposes; so be sure to double check your paths and environment variables.

The changes you need to make to your configuration file are as follows:

Uncomment these lines, they enable needed functionality:

# unique_subject = no # allows you to recreate the cert as needed, for example to add more domains
# copy_extensions = copy # allows you to have many domains
# req_extensions = v3_req # allows you to have many domains
# keyUsage = nonRepudiation, digitalSignature, keyEncipherment # makes cert work with modern browsers
# keyUsage = cRLSign, keyCertSign # makes cert work with modern browsers

Comment out these lines, they don't add anything useful and get in the way:

attributes                      = req_attributes
[ req_attributes ]
challengePassword               = A challenge password
challengePassword_min           = 4
challengePassword_max           = 20
unstructuredName                = An optional company name

Modify the following values as indicated:

default_days = 3650 # there's no reason to have to redo this every year, set to 10 years
default_md = sha256 # this is the default value in openssl 1.1.0, and it's needed for modern browsers

Modify the following values as desired:

countryName_default             = NL # two letter country code
stateOrProvinceName_default     = North Holland # name of province or state
localityName_default            = Amsterdam # name of city
0.organizationName_default      = ACME Inc # name of organization
organizationalUnitName_default  = Certificate Services # name of department
commonName_default              = example.dev # your main domain
emailAddress_default            = admin@example.dev # your email

Specify all your development domains

Using a separate top level domain (TLD) for development allows you to access the production version of the site at the proper url, while providing easy access to your development sites. This guide will use .dev though you could choose anything (some TLDs like .local can cause issues and should be avoided, while some TLDs are explicitly reserved for personal use such as .test, .example, .invalid, and .localhost). We will will go into more detail about how to set this up below in the DNS section.

While no browser accepts wildcard certificates for entire top level domains (no *.dev certificates unfortunately), if you are developing a large number of micro services or sites that share a domain (say example.dev) and have added a wildcard for the domain to the list of alt names in the certificate (*.example.dev), then you can simply add more sites to your setup without changing your certificate.

Add the following on a new line under keyUsage = nonRepudiation, digitalSignature, keyEncipherment:

subjectAltName = @alt_names
[alt_names]
DNS.1 = localhost
DNS.2 = example.dev
DNS.3 = example2.dev
DNS.4 = *.example1.dev
IP.1 = 127.0.0.1
IP.2 = ::1
# add as many more domains and IPs as you want

Add homebrew's openssl and CA.pl to your path:

PATH=`brew --prefix openssl`/bin:`brew --prefix`/etc/openssl/misc:$PATH

Create Your Certificate Authority

The changes you made to openssl.cnf will allow the certificates produced with your CA to be accepted by modern browsers. You can hit enter/return to accept the default values for each question you get asked by the script, with the exception of the passphrase which must be at least 4 characters long.

mkdir ~/certs
cd ~/certs
CA.pl -newca

Trust your new certificate authority root certificate:

The previous command will have created a new subdirectory called demoCA, you will need to install the certificate authority root certificate from the new demoCA dir into your system keychain to prevent Chrome from warning you about broken SSL.

sudo /usr/bin/security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain ~/certs/demoCA/cacert.pem

Create the new server certificate/key pair:

You can accept all the defaults, as you already customized them earlier. In the signing step you will be asked for the passphrase from the CA you created earlier, as well as confirmation that you want to sign the certificate.

cd ~/certs
CA.pl -newreq-nodes
CA.pl -sign

Configure Passenger Standalone

Be sure to set the paths to the certificate/key pair you created:

passenger start --ssl --daemonize --environment development --ssl-certificate ~/certs/newcert.pem --ssl-certificate-key ~/certs/newkey.pem --port 443

Configure DNS

Since you need the domain you visit in Chrome to match the SSL certificate, you still need to direct all your development traffic to localhost. One option is to edit your hosts file (located at /etc/hosts) and add entries to redirect each development domain back to your computer. That solution works*, but does not scale well. The next section will describe setting up Dnsmasq & configuring your own top level domain in order to save having to edit the hosts file repeatedly.

*Note some older versions of macOS actually ignored entries in the hosts file for new top level domains (for example: .dev stopped working once Google started responding to DNS queries for that TLD. Google owns .dev, but they have stated it is for internal use only, so you are unlikely to break anything if you use it, unless you work for Google), which makes using a DNS resolver like Dnsmasq all the more useful, because you avoid that bug.

echo 'address=/dev/127.0.0.1' >> `brew --prefix`/etc/dnsmasq.conf
sudo cp `brew --prefix dnsmasq`/homebrew.mxcl.dnsmasq.plist /Library/LaunchDaemons/
sudo launchctl load /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist
sudo mkdir -p /etc/resolver
sudo echo 'nameserver 127.0.0.1' > /etc/resolver/dev

Done

Now if you restart Passenger, SSL should be working and Chrome shouldn't complain when you visit your app over https.

To summarize, at this point we have managed to: create and trust our own certificate authority, request and then sign a certificate for our development server, and setup a local DNS server to handle development web traffic.

⌘+Z

In order to undo all of the above you can simply run the following commands:

sudo launchctl unload /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist
sudo /usr/bin/security remove-trusted-cert -d ~/certs/demoCA/cacert.pem
sudo rm -rf /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist /etc/resolver/dev ~/certs
brew uninstall dnsmasq
cp `brew --prefix`/etc/openssl/openssl.cnf{.default,}