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
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.
$ 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
$ 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 version
$ passenger -v
$ httpd -v || apache2 -v
$ httpd -v
$ dnsmasq -v
$ firefox -v
$ /Applications/Firefox.app/Contents/MacOS/firefox -v
$ google-chrome --version
$ /Applications/Chrome.app/Contents/MacOS/Google\ Chrome --version
$ echo `system_profiler -xml SPApplicationsDataType | xmllint --xpath \ "//dict[string='Safari']/key[.='version']/following-sibling::*/text()" -`
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
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:
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 = email@example.com # 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 = 220.127.116.11.1 tsa_policy2 = 18.104.22.168.5.6 tsa_policy3 = 22.214.171.124.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 = firstname.lastname@example.org [ 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:
|Red Hat, CentOS, Fedora, Amazon Linux, Scientific Linux||
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:||
$ 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
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>
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)
$ 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
$ 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
Now if you restart your webserver, SSL should be working and your browser shouldn't complain when you visit your app over https.