Skip to main content

Valid HTTPS for local environments

Category

Despite how complicated X.509 can get, generating this type of certificate is actually fairly straightforward and can be completed in a matter of minutes.

Below is a local development version of the website you're looking at right now.

The web server here is configured with proper encryption, but a generic self-signed certificate.

Unsurprisingly, Chrome isn't too thrilled about that. After clicking past the warning page we can see a big red "Not Secure" badge in the address bar.

Sure, it's annoying to look at, but the real problem lies with a form on this page which is submitted insecurely over HTTP. Chrome would usually change the icon in the address bar to indicate this, but the invalid certificate warning takes precedence so no such behavior occurs.

Here's where a trusted certificate would come in handy.

While I'd love to have a single certificate that applies to all *.local domains, this isn't possible, so I'll be settling for *.dev.local instead. After some research online, I pieced together this minimal configuration file which I've saved to dev.cnf.

[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_ca

[req_distinguished_name]
commonName = Common Name (e.g. server FQDN or YOUR name)
commonName_default = *.dev.local

[v3_ca]
subjectAltName = @alt_names

[alt_names]
DNS.1 = *.dev.local

This is essentially the bare minimum to generate a certificate that Chrome and Firefox won't complain about. For more complicated use cases, a proper PKI with root and intermediate certificate authorities may be more appropriate - two excellent resources for this can be found here and here.

Now it's just a matter of creating the certificate and private key. These two files will then be copied to the web server. The certificate will also need to be imported locally, but more on that later.

This is being run from Ubuntu via WSL, but should work on any operating system with OpenSSL.

$ openssl req -batch -x509 -nodes -days 1095 -sha256 \
>     -newkey ec:<(openssl ecparam -name prime256v1) \
>     -keyout dev.key -out dev.crt -config dev.cnf
Generating an EC private key
writing new private key to 'dev.key'
-----

There should be a dev.crt and dev.key file in the same directory as dev.cnf now.

The dev.crt and dev.key files should be copied over to the web server, then the web server configuration should be updated accordingly.

# nginx
ssl_certificate /etc/nginx/ssl/dev.crt;
ssl_certificate_key /etc/nginx/ssl/dev.key;

Always be sure to test the updated configuration before reloading the webserver.

$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ sudo systemctl reload nginx

Finally, I imported the certificate into my local machine to grant it trust.

To do this on Windows:

  • Double-click the dev.crt certificate file and select 'Install Certificate'.
  • Set the Store Location to Local Machine and click Next.
  • Select 'Place all certificates in the following store' and click Browse.
  • Select 'Trusted Root Certification Authorities' and click OK.
  • Click Next, then Finish. You may also need to restart your browser.

Instructions differ slightly on Linux and Mac but follow the same basic principle.

There's also an extra step for Firefox since it doesn't use the system certificate store by default. Simply navigate to "about:config" and enable the "security.enterprise_roots.enabled" option.​

With the trusted certificate now in place, the page loads without any security prompts.

That insecure form I mentioned earlier is a lot easier to spot now. A similar warning in the address bar will also occur if content on the page is loaded over HTTP rather than HTTPS.

With the form taken care of, the page is now fully secure!

One thing to keep in mind is the certificate we generated only applies to *.dev.local but not, for example: test.project.dev.local. For this, you just need to add another entry into the 'alt_names' section of the OpenSSL configuration file - i.e. "DNS.2 = *.project.dev.local"