One of the best ways to reduce complexity (read: stress) in web development is to minimize the differences between your development and production environments. After being frustrated by attempts to unify the approach to SSL on my local machine and in production, I searched for a workflow that would make the protocol invisible to me between all environments.
Most workflows make the following compromises:
-
Use HTTPS in production but HTTP locally. This is annoying because it makes the environments inconsistent, and the protocol choices leak up into the stack. For example, your web application needs to understand the underlying protocol when using the
secure
flag for cookies. If you don't get this right, your HTTP development server won't be able to read the cookies it writes, or worse, your HTTPS production server could pass sensitive cookies over an insecure connection. -
Use production SSL certificates locally. This is annoying because SSL credentials shouldn't be passed around lightly, and ideally should only exist on blessed machines. Plus, even with a wildcard certificate, coming up with a scheme to put development hosts and production hosts under the same namespace has some weird edge cases (if the production app is served off of
myproject.com
, where is the development app served from?) -
Give up on HTTPS entirely. This is annoying because it's like, 2013 already, amirite.
So here's my approach for a modern HTTPS workflow, in four steps:
- Resolve a top-level domain for all development work,
- Create a wildcard SSL certificate for each project,
- Avoid HTTPS warnings by telling OS X to trust the certificate, and
- Bask in easy HTTPS.
Let's get started.
Install Homebrew if it's not already installed
ruby -e "$(curl -fsSL https://raw.github.com/mxcl/homebrew/go)"
The most common way to get a given host to resolve to your local machine is to manually edit your /private/etc/hosts
file, which is annoyingly O(n2), since you need to add an entry for each subdomain of each project you're working on.
With a little more work up front, we can streamline our development workflow by resolving a single top-level domain (herein as TLD) to our development box and never touch the hosts file again.
One tool that can help us do this is Dnsmasq, a lightweight DNS forwarder. Here's how we'll install it:
brew install dnsmasq
mkdir -pv $(brew --prefix)/etc
sudo cp -v $(brew --prefix dnsmasq)/homebrew.mxcl.dnsmasq.plist /Library/LaunchDaemons
sudo launchctl load -w /Library/LaunchDaemons/homebrew.mxcl.dnsmasq.plist
sudo mkdir -pv /etc/resolver
Once it's installed and running, we need to choose what TLD we'd like to resolve to our local box. OS X reserves the .local
TLD, and many developers use .dev
, but I like the idea of using names as TLDs, to make it easier to reason about my local environment within the context of other developers on the same project. So I'm going to have Dnsmasq locally resolve all hosts ending in my OS X user id (also known as a short name, and what you get when you type whoami
in the terminal). Since my user id is jed
, that means everything from apple.jed
to zebras.jed
to apple.zebras.jed
will resolve to 127.0.0.1
.
All of the following shell scripts assume you're cool with using your user id as your top-level domain, but feel to change them accordingly by replacing $(whoami)
with your chosen TLD.
echo "address=/.$(whoami)/127.0.0.1" | sudo tee -a $(brew --prefix)/etc/dnsmasq.conf
echo "nameserver 127.0.0.1" | sudo tee /etc/resolver/$(whoami)
Now we need to make sure that worked, by spinning up a server and hitting it.
cd /Applications
sleep 1 && open "http://some.domain.$(whoami):9520" &
python -m SimpleHTTPServer 9520
This should open a new browser that shows the contents of your Applications folder, like this:
The browser might tell us that the DNS lookup failed, like this:
In this case, we'll need to restart the machine to make sure the setup above has taken effect.
Now that we know our DNS works, we need to create an SSL certificate for our development environment. Since (for good reason) browsers won't trust certificates that cover an entire TLD, we need to create a wildcard certificate for each domain we want to use. Also, since we want to cover both any subdomain (*.yourproject.tld) and the domain apex (yourproject.tld), we'll need to use Subject Alternative Name extension.
First, let's create a new directory named for our project and cd
into it.
mkdir ~/Desktop/myproject && cd $_
Next, let's create a temporary configuration file, and feed it into openssl
to create our certificate.
cat > openssl.cnf <<-EOF
[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
CN = *.${PWD##*/}.$(whoami)
[v3_req]
keyUsage = keyEncipherment, dataEncipherment
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = *.${PWD##*/}.$(whoami)
DNS.2 = ${PWD##*/}.$(whoami)
EOF
openssl req \
-new \
-newkey rsa:2048 \
-sha1 \
-days 3650 \
-nodes \
-x509 \
-keyout ssl.key \
-out ssl.crt \
-config openssl.cnf
rm openssl.cnf
Now we have two files in our project directory: ssl.key
, the private key used to sign the certificate, and ssl.crt
, the certificate itself.
At this point we have almost everything we need to fire up a local HTTPS server, but there's one problem.
Any content we try to serve over HTTPS from this domain gets IMMA-LET-YOU-FINISHed by a scary message like the one above, warning us that the presented certificate is not trusted. This message differs by browser, and you might be tempted to go ahead and ignore it, but that's not a good habit to get into, and is likely to lead to development complexity down the road.
There's a better way; we can tell OS X to trust the certificate we just created so that we don't have to see this screen ever again, by adding the certificate to our keychain.
Since our certificate is self-signed, we'll always get a warning when using it for our HTTPS site. We need to use Keychain Access to tell OS X to enhance its calm for this domain.
- Open Keychain Access.
open /Applications/Utilities/Keychain\ Access.app
-
From the Keychains list on the left, select System.
-
Click the [+] button under the list of certificates.
-
Choose the
ssl.crt
file created in the previous step, make sure System is selected for Destination Keychain, and click Open. -
When prompted, enter your password and click Modify Keychain.
-
Click Don't Trust.
-
Select the newly imported certificate, which should appear at the bottom of the certificate list, and click the [i] button.
-
In the popup window, click the ▶ button to the left of Trust, and select Always Trust for When using this certificate:.
-
Close the popup window.
-
When prompted, enter your password again and click Update Settings.
-
Close Keychain Access.
Now that OS X knows that our self-signed certificate is legit, let's spin up an HTTPS server to make sure it all works. You can use Apache or Nginx or whatever you like, but here we'll use nodejs:
sleep 1 && open "https://myproject.$(whoami):8443" &
sleep 1 && open "https://subdomain.myproject.$(whoami):8443" &
node <<-EOF
var https = require("https")
var fs = require("fs")
var options = {
key: fs.readFileSync("ssl.key"),
cert: fs.readFileSync("ssl.crt")
}
var server = https.createServer(options, function(req, res) {
res.writeHead(200, {"Content-Type": "text/plain"})
res.end("It worked!\n")
})
server.listen(8443, console.log)
EOF
As you can see, the correct content is being served by both the apex domain and subdomain.
Note that this will satisfy Chrome and Safari, but since Firefox doesn't inherit the same keychain from OS X, it will tell you that the certificate is untrusted. In this case, click I Understand the Risks, then Add Exception..., and then Confirm Security Exception_ when prompted.
This guide is a snapshot of my thoughts on how to simply the HTTPS workflow for web development, but if you have any feedback or want to share some tips about your HTTPS workflow, drop me a line at @jedschmidt or leave a comment below.