Setting up HTTPS locally can be tricky business. Even if you do manage to wrestle self-signed certificates into submission, you still end up with browser privacy errors. In this article, I’ll walk you through setting up self-signed certificates and show you a nice little trick to quiet browser privacy errors.
For at least a year now I’ve been running HTTPS in my local development environment. Last week I updated to Google Chrome 58 and something changed that nuked this setup. All of a sudden I was getting browser privacy errors again.
Unlike the privacy errors of the past, there was no longer any “Add Exception” option. I checked Firefox and it behaved the same. Safari still worked.
A search for ERR_CERT_COMMON_NAME_INVALID produced little results, but I eventually found the solution in the Chromium bug tracker. Turns out Chrome and Firefox have dropped support for commonName matching in certificates.
I managed to fix my setup using the suggestions in the Chromium comment (more on that later) but the whole ordeal made me realize that I hadn’t documented how to set up HTTPS locally without browser privacy errors. This article will serve as that document and I plan to update it as things change in the future.
Why HTTPS Locally?
Why not just use regular HTTP locally? Because if your production site is HTTPS-only and you’re developing locally on regular HTTP, your dev and production environments are not as similar as they could be. For example, my dev environment for this site (deliciousbrains.com) runs as an Ubuntu server in a VMware virtual machine (VM) on my Mac. The production site is an Ubuntu server running on Linode with an almost identical configuration.
You definitely want your dev environment to mirror production as closely as possible.
When it doesn’t, you invite more issues showing up in production that didn’t show up in dev. Running HTTP when your production site is HTTPS-only is definitely an unnecessary risk.
Creating a Self-Signed Certificate
Like enabling HTTPS on a production site, you first need a certificate. For a production site, you request one from a certificate authority like Let’s Encrypt, Comodo, etc. For a local dev environment, we can generate a self-signed certificate on the command line. It used to be as simple as this command:
openssl req -new -sha256 -newkey rsa:2048 -nodes \ -keyout dev.deliciousbrains.com.key -x509 -days 365 \ -out dev.deliciousbrains.com.crt
Running that command, you get asked a few questions:
Country Name (2 letter code) [AU]: State or Province Name (full name) [Some-State]: Locality Name (eg, city) : Organization Name (eg, company) [Internet Widgits Pty Ltd]: Organizational Unit Name (eg, section) : Common Name (e.g. server FQDN or YOUR name) :dev.deliciousbrains.com Email Address :
Most of these questions weren’t important to answer for a dev environment certificate. The answers would show up when looking at the certificate information, but it didn’t have any impact on whether the browser deemed the site to be secure or not. In fact, the only question that really needed an answer was Common Name (CN). The answer to that question determined which domain the certificate was valid for.
But now, the CN question is also superficial. As of Chrome 58 and Firefox 48 it is ignored when matching a domain name to a certificate. This is exactly why I started getting privacy errors when I updated to Chrome 58.
RFC 2818 describes two methods to match a domain name against a certificate – using the available names within the subjectAlternativeName extension, or, in the absence of a SAN extension, falling back to the commonName. The fallback to the commonName was deprecated in RFC 2818 (published in 2000), but support still remains in a number of TLS clients, often incorrectly. — chromestatus.com
Wow, deprecated since 2000. Definitely time to remove support.
So now the domain name must be defined in the Subject Alternative Name (SAN) section (i.e. extension) of the certificate:
Now when creating a self-signed certificate, we need to provide a configuration file to OpenSSL and define the SAN in that configuration file. Our command becomes:
openssl req -config dev.deliciousbrains.com.conf -new -sha256 -newkey rsa:2048 \ -nodes -keyout dev.deliciousbrains.com.key -x509 -days 365 \ -out dev.deliciousbrains.com.crt
The only change I made was replacing the
DNS.1 = example.com line with
DNS.1 = dev.deliciousbrains.com and removed the rest of the DNS lines underneath it. Here’s the full config with comments removed and formatting cleaned up:
[ req ] default_bits = 2048 default_keyfile = server-key.pem distinguished_name = subject req_extensions = req_ext x509_extensions = x509_ext string_mask = utf8only [ subject ] countryName = Country Name (2 letter code) countryName_default = US stateOrProvinceName = State or Province Name (full name) stateOrProvinceName_default = NY localityName = Locality Name (eg, city) localityName_default = New York organizationName = Organization Name (eg, company) organizationName_default = Example, LLC commonName = Common Name (e.g. server FQDN or YOUR name) commonName_default = Example Company emailAddress = Email Address emailAddress_default = email@example.com [ x509_ext ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid,issuer basicConstraints = CA:FALSE keyUsage = digitalSignature, keyEncipherment subjectAltName = @alternate_names nsComment = "OpenSSL Generated Certificate" [ req_ext ] subjectKeyIdentifier = hash basicConstraints = CA:FALSE keyUsage = digitalSignature, keyEncipherment subjectAltName = @alternate_names nsComment = "OpenSSL Generated Certificate" [ alternate_names ] DNS.1 = dev.deliciousbrains.com
If you’re using MAMP, you may be tempted to generate your self-signed certificates using the MAMP UI:
I tried this with MAMP 4.1.1 but unfortunately it doesn’t define a SAN and so you’ll get the ERR_CERT_COMMON_NAME_INVALID browser privacy error. Until they update MAMP to define a SAN, you’ll have to generate your certificates on the command line and add them to MAMP.
Installing the Certificate
Next you’ll need to install the certificate into Nginx, Apache, or whatever web server you’re using. I’m not going to cover that here as it really depends on your environment. In my case, because I’m using an Ubuntu server I just follow the instructions from our Install WordPress on Ubuntu series. If you’re using MAMP, you select the certificate and key files using the UI as shown in the screenshot above.
Once you’ve updated your web server’s config and restarted it (don’t forget to restart it), loading the site will still give you a browser privacy error:
You’ll notice that it’s now a different error: ERR_CERT_AUTHORITY_INVALID. The browser doesn’t trust the certificate because we self-signed it instead of getting it from a certificate authority. However, we can add the certificate to our macOS Keychain and indicate that the certificate should always be trusted.
Adding the Certificate to macOS Keychain
- In Chrome, open the dev site you’ve configured to use the certificate
- Press Cmd-Alt-I to open Developer Tools
- Click the Security tab
- Click the View certificate button
You should end up with a screen that looks like this:
Now, drag the little certificate icon into a folder in the Finder app.
A certificate file will be created in that folder. Double click on the file. If you have multiple keychains like I do, you might get a window like this:
Click “Add”. If you only have one keychain, your certificate might be added to your keychain without a prompt. Regardless if you have to accept a prompt or not, a Keychain Access window should show up. Search for your certificate:
Double click on it. A window will open with the certificate details. Expand the Trust section. Change the “When using this certificate:” select box to “Always Trust”.
Close the certificate window. It will ask you to enter your password (or scan your finger), do that. Now visit your dev site again.
To clean up, you can delete the certificate file from the folder you dragged it into since you’ve added it to the system’s keychain.
Are you currently working with HTTP-only locally? Will you be switching to HTTPS? Let us know in the comments.
Update: Some commenters pointed out that Laravel Valet makes this easier. Evan actually pointed this out when reviewing my article and I plan to give it a try in the future. If you’ve used Laravel Valet to do this, let us know in the comments.