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. Prepare the System
  2. OpenSSL Configuration
  3. Create Your Certificate Authority
  4. Configure Passenger + Nginx
  5. Configure DNS
  6. Done

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
Copy
$ sudo apt-get update $ sudo apt-get upgrade -y $ sudo apt-get install -y openssl dnsmasq $ sudo apt-get install -y nginx
Red Hat, CentOS, Fedora, Amazon Linux, Scientific Linux
Copy
$ sudo yum update -y $ sudo yum install -y openssl dnsmasq openssl-perl $ sudo yum install -y nginx
macOS
Copy
$ sudo softwareupdate -i -a $ brew update $ brew upgrade $ brew install openssl passenger dnsmasq $ brew install nginx

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
Copy
$ openssl version
Passenger 5.0.29
Copy
$ passenger -v
Nginx 1.10.1
Copy
$ nginx -v
Dnsmasq 2.76
Copy
$ dnsmasq -v
Firefox 47.0.1
Copy
$ firefox -v
Copy
$ /Applications/Firefox.app/Contents/MacOS/firefox -v
Chrome 51.0.2704.*
Copy
$ google-chrome --version
Copy
$ /Applications/Chrome.app/Contents/MacOS/Google\ Chrome --version
Safari 9.1.1
N/A
Copy
$ 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:

Copy
# 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:

Copy
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:

Copy
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:

Copy
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:

Copy
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:

Copy
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
Copy
PATH=$PATH:/usr/lib/ssl/misc
Red Hat, CentOS, Fedora, Amazon Linux, Scientific Linux
Copy
PATH=$PATH:/etc/pki/tls/misc
macOS
Copy
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.

Copy
$ 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:
Copy
$ sudo /usr/bin/security add-trusted-cert -d -r trustRoot -k \ /Library/Keychains/System.keychain ~/certs/demoCA/cacert.pem
Debian, Ubuntu with Chrome:
Copy
$ sudo cp ~/certs/demoCA/cacert.pem /usr/local/share/ca-certificates/myCA.crt $ sudo dpkg-reconfigure ca-certificates
Fedora 19+, RHEL/CentOS 7+ Chrome:
Copy
$ 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.
Copy
$ 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.

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

Configure Passenger + Nginx

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

Generate the Diffie-Hellman parameters file using this command:

Copy
$ openssl dhparam -out /path/to/dhparam.pem 2048
Copy
server { listen 80 default_server; listen [::]:80 default_server; return 301 https://$host$request_uri; } server { listen 443 ssl; listen [::]:443 ssl; root /var/www/example.dev/public; passenger_app_root /var/www/example.dev; server_name example.dev; ssl_certificate /usr/local/etc/nginx/ssl/server.crt; ssl_certificate_key /usr/local/etc/nginx/ssl/server.key; ssl_dhparam /usr/local/etc/nginx/ssl/dhparam.pem; ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; ssl_session_tickets off; ssl_prefer_server_ciphers on; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH; add_header Strict-Transport-Security max-age=31536000; passenger_enabled on; passenger_app_env development; }

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:
Copy
$ 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:
Copy
$ 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:
Copy
$ 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.