Using SSL with Passenger in development

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

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

Table of contents

  1. Loading...

Prepare the System

Assumptions: you are using the Bash shell, have admin rights to your computer, and are using Firefox, Chrome, or Safari. Ensure that OpenSSL, Passenger, Dnsmasq, and optionally Nginx or Apache are installed.

Follow these instructions to install Passenger; you may have to adjust the settings to your desired config, for example using Passenger Enterprise or OSS.

Debian, Ubuntu
$ sudo apt-get update
$ sudo apt-get upgrade -y
$ sudo apt-get install -y openssl dnsmasq
$ sudo apt-get install -y apache2
$ sudo a2enmod ssl
Red Hat, CentOS, Fedora, Amazon Linux, Scientific Linux
$ sudo yum update -y
$ sudo yum install -y openssl dnsmasq openssl-perl
$ sudo yum install -y httpd24 mod24_ssl
macOS
$ sudo softwareupdate -i -a
$ brew update
$ brew upgrade
$ brew install openssl passenger dnsmasq
$ sed -E -i.bak -e 's|#(LoadModule ssl_.*)|\1|' /etc/apache2/httpd.conf

Software versions used in this article:

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

Application Version Linux Command Mac Command
OpenSSL 1.0.2h
$ openssl version
Passenger 5.0.29
$ passenger -v
Apache 2.4.18
$ httpd -v || apache2 -v
$ httpd -v
Dnsmasq 2.76
$ dnsmasq -v
Firefox 47.0.1
$ firefox -v
$ /Applications/Firefox.app/Contents/MacOS/firefox -v
Chrome 51.0.2704.*
$ google-chrome --version
$ /Applications/Chrome.app/Contents/MacOS/Google\ Chrome --version
Safari 9.1.1
N/A
$ echo `system_profiler -xml SPApplicationsDataType | xmllint --xpath \
"//dict[string='Safari']/key[.='version']/following-sibling::*[1]/text()" -`

OpenSSL Configuration

When setting up a development environment with SSL it is better practice 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.

On macOS this is located at /usr/local/etc/openssl/openssl.cnf or if it isn't there you can copy /usr/local/etc/openssl/openssl.cnf.default to that path as a starting point, and edit from there.

On linux it may be in one of the following locations: /usr/local/ssl/openssl.cnf /usr/lib/ssl/openssl.cnf /etc/ssl/openssl.cnf /etc/pki/tls/openssl.cnf

The changes you need to make are as follows:

uncomment these lines, they enable needed functionality:

# unique_subject = no # allows you to recreate the cert if needed
# 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 add nothing 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

and modify the following values as indicated:

default_days = 3650 # there's no reason to have to redo this every year, set to 10
default_md = sha256 # default in openssl 1.1.0, 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

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

Using a separate top level domain (TLD) for development allows you to access the production site at the proper url, while having easy access to your development sites. This guide will use .dev and 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, if you are developing a large number of micro services or sites that share a domain and have added a wildcard for the domain to the list of alt names in the certificate, then you can simply add more sites to your setup without changing your certificate and by using mass deployment you don't have to modify your web server or app server configs to spin up another site, simply create it in the correct directory.

A working sample is provided below

Replace occurrences of example.dev with your own domain:

HOME			= .
RANDFILE		= $ENV::HOME/.rnd
oid_section		= new_oids
[ new_oids ]
tsa_policy1 = 1.2.3.4.1
tsa_policy2 = 1.2.3.4.5.6
tsa_policy3 = 1.2.3.4.5.7
[ ca ]
default_ca	= CA_default		# The default ca section
[ CA_default ]
dir		= ./demoCA		# Where everything is kept
certs		= $dir/certs		# Where the issued certs are kept
crl_dir		= $dir/crl		# Where the issued crl are kept
database	= $dir/index.txt	# database index file.
unique_subject	= no			# Set to 'no' to allow creation of
new_certs_dir	= $dir/newcerts		# default place for new certs.
certificate	= $dir/cacert.pem       # The CA certificate
serial		= $dir/serial           # The current serial number
crlnumber	= $dir/crlnumber	# the current crl number
crl		= $dir/crl.pem          # The current CRL
private_key	= $dir/private/cakey.pem# The private key
RANDFILE	= $dir/private/.rand	# private random number file
x509_extensions	= usr_cert		# The extensions to add to the cert
name_opt        = ca_default		# Subject Name options
cert_opt        = ca_default		# Certificate field options
copy_extensions = copy
default_days	= 3650			# how long to certify for
default_crl_days= 30			# how long before next CRL
default_md	= sha256		# which md to use.
preserve	= no			# keep passed DN ordering
policy		= policy_match
[ policy_match ]
countryName		= match
stateOrProvinceName	= match
organizationName	= match
organizationalUnitName	= optional
commonName		= supplied
emailAddress		= optional
[ policy_anything ]
countryName		= optional
stateOrProvinceName	= optional
localityName		= optional
organizationName	= optional
organizationalUnitName	= optional
commonName		= supplied
emailAddress		= optional
[ req ]
default_bits		= 4096
default_keyfile         = privkey.pem
distinguished_name	= req_distinguished_name
x509_extensions	= v3_ca	# The extensions to add to the self signed cert
string_mask = utf8only
req_extensions = v3_req # The extensions to add to a certificate request
[ req_distinguished_name ]
countryName			= Country Name (2 letter code)
countryName_default		= NL
countryName_min			= 2
countryName_max			= 2
stateOrProvinceName		= State or Province Name (full name)
stateOrProvinceName_default	= North Holland
localityName			= Locality Name (eg, city)
localityName_default            = Amsterdam
0.organizationName		= Organization Name (eg, company)
0.organizationName_default	= My Company
organizationalUnitName		= Organizational Unit Name (eg, section)
organizationalUnitName_default	= Certificate Services
commonName			= Common Name (e.g. server FQDN or YOUR name)
commonName_max			= 64
commonName_default              = www.example.dev
emailAddress			= Email Address
emailAddress_max		= 64
emailAddress_default            = info@example.dev
[ usr_cert ]
basicConstraints=CA:FALSE
keyUsage = nonRepudiation, digitalSignature, keyEncipherment
nsComment			= "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
[ v3_req ]
basicConstraints = CA:FALSE
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
[ v3_ca ]
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid:always,issuer
basicConstraints = CA:true
keyUsage = cRLSign, keyCertSign
[ crl_ext ]
authorityKeyIdentifier=keyid:always
[ proxy_cert_ext ]
basicConstraints=CA:FALSE
nsComment			= "OpenSSL Generated Certificate"
subjectKeyIdentifier=hash
authorityKeyIdentifier=keyid,issuer
proxyCertInfo=critical,language:id-ppl-anyLanguage,pathlen:3,policy:foo
[ tsa ]
default_tsa = tsa_config1	# the default TSA section
[ tsa_config1 ]
dir		= ./demoCA		# TSA root directory
serial		= $dir/tsaserial	# The current serial number (mandatory)
crypto_device	= builtin		# OpenSSL engine to use for signing
signer_cert	= $dir/tsacert.pem      # The TSA signing certificate
certs		= $dir/cacert.pem	# Certificate chain to include in reply
signer_key	= $dir/private/tsakey.pem # The TSA private key (optional)
default_policy	= tsa_policy1		# Policy if request did not specify it
other_policies	= tsa_policy2, tsa_policy3	# acceptable policies (optional)
digests		= md5, sha1		# Acceptable message digests (mandatory)
accuracy	= secs:1, millisecs:500, microsecs:100	# (optional)
clock_precision_digits  = 0	# number of digits after dot. (optional)
ordering		= yes	# Is ordering defined for timestamps?
tsa_name		= yes	# Must the TSA name be included in the reply?
ess_cert_id_chain	= no	# Must the ESS cert id chain be included?

Add CA.pl to your path:

Debian, Ubuntu
PATH=$PATH:/usr/lib/ssl/misc
Red Hat, CentOS, Fedora, Amazon Linux, Scientific Linux
PATH=$PATH:/etc/pki/tls/misc
macOS
PATH=$PATH:/usr/local/etc/openssl/misc

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 hold 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’ll need to install the certificate authority root certificate from the new demoCA dir into your system keychain (and/or Firefox) to prevent your browser from warning you about broken SSL.

macOS with Safari or Chrome:
$ sudo /usr/bin/security add-trusted-cert -d -r trustRoot -k \
/Library/Keychains/System.keychain ~/certs/demoCA/cacert.pem
Debian, Ubuntu with Chrome:
$ sudo cp ~/certs/demoCA/cacert.pem /usr/local/share/ca-certificates/myCA.crt
$ sudo dpkg-reconfigure ca-certificates
Fedora 19+, RHEL/CentOS 7+ Chrome:
$ sudo cp ~/certs/demoCA/cacert.pem /etc/pki/ca-trust/source/anchors/
$ sudo update-ca-trust
$ sudo update-ca-trust enable # RHEL/CentOS 6 only since: RHEA-2013-1596
Any OS with Firefox:
open preferences
click advanced tab
click certificates tab
click “view certificates”
click “import…”
pick ~/certs/demoCA/cacert.pem file
In the Downloading Certificate window check "Trust this CA to identify web sites."
click "OK"
Any *nix OS with Curl: Pass the --cacert flag to curl when you use it to query your website.
$ curl --cacert ~/certs/demoCA/cacert.pem 'https://example.dev'

create the new server certificate/key pair:

You can accept all the defaults, as you already customized them earlier.

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

Configure Passenger + Apache

Replace example.dev with your domain, and set the paths to your app and the certificate/key pair you created:

<VirtualHost *:80>
    Redirect permanent / https://www.example.dev/
    ServerName example.dev
    ServerAlias www.example.dev
</VirtualHost>

<VirtualHost *:443>
    ServerName example.dev
    ServerAlias www.example.dev
    SSLCertificateFile "/etc/apache2/server.crt"
    SSLCertificateKeyFile "/etc/apache2/server.key"
    PassengerAppRoot "/path/to/app"
    DocumentRoot "/path/to/app/public"
    ErrorLog "/var/log/apache2/example.dev-error_log"
    CustomLog "/var/log/apache2/example.dev-access_log" common
    <Directory "/path/to/app/public">
        Options Indexes FollowSymLinks
        AllowOverride All
        Require all granted
    </Directory>
    PassengerEnv development
    SSLEngine on
    SSLProtocol all -SSLv2 -SSLv3
    SSLHonorCipherOrder on
    SSLCipherSuite HIGH
    SSLCompression off # not all versions of Apache+OpenSSL allow this option
    Header always edit Set-Cookie ^(.*)$ $1;HttpOnly;Secure
    Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
</VirtualHost>

Configure DNS

Since you need the domain you visit in your browser to match the SSL certificate, you still need to direct all your development traffic back 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 doesn't scale well. Next I'll 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 top level domain. Google owns .dev, but they've stated it's for internal use only, so you're 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 workaround that bug.

In Ubuntu 12.10+ use NetworkManager's Dnsmasq:
$ sudo mkdir -p /etc/NetworkManager/dnsmasq.d
$ sudo echo 'address=/dev/127.0.0.1' > /etc/NetworkManager/dnsmasq.d/dev-TLD
$ sudo service network-manager restart # (Ubuntu 12.10)
$ sudo service dnsmasq restart # (Ubuntu > 13.04)
Other Linux:
$ sudo echo 'address=/dev/127.0.0.1' > /etc/dnsmasq.d/dev-TLD
$ prepend domain-name-servers 127.0.0.1
$ sudo service NetworkManager restart
$ sudo service dnsmasq start
macOS:
$ 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 your webserver, SSL should be working and your browser shouldn't complain when you visit your app over https.