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.
If you already have a production SSL certificate, this could be achieved as simply as starting passenger with:
$ passenger start --ssl --environment development --ssl-certificate /path/to/certificate.pem --ssl-certificate-key /path/to/key.pem --port 443
However that approach has some downsides, 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 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.
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
- 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 |
Red Hat, CentOS, Fedora, Amazon Linux, Scientific Linux |
$ sudo yum update -y $ sudo yum install -y openssl dnsmasq openssl-perl |
macOS |
$ sudo softwareupdate -i -a $ 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 | Linux Command | Mac Command |
---|---|---|---|
OpenSSL | 1.0.2h |
$ openssl version
|
|
Passenger | 5.0.29 |
$ passenger -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 |
|
Red Hat, CentOS, Fedora, Amazon Linux, Scientific Linux |
|
macOS |
|
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 Standalone
Replace example.dev
with your domain, and set the paths to your app and 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 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.