Creating a Certificate Authority in 2020 for Your Soho

I have a couple of systems at home which provide web services, like my Intel NUC and my Synology NAS, and I have been wanting for a while to move all of them to a proper https only environment.

But my biggest hurdle for doing that, has been the enormous pain in managing certificates in a way that makes everybody - the servers, the browsers, the local http clients happy. From my previous attempts, there was always the browsers which annoyed me to no end, and I ended up getting by using improperly made self-signed certificates and accepting all the invalid certificate warnings that my browser threw up.

So this Friday night, I spent my late night hours trying to get at the bottom of it all, and several frustrating hours later, finally made everybody happy.

Now, remember how I mentioned 2020 in my post title? The reason is that the world of PKI infrastructure is not static, and is changing way faster than people realize.

At this point, most SOHO people have probably stopped paying for certificates and are using the wonderful automated certificate system provided for free by Lets Encrypt. But while that utopia is astonishingly real, it mostly exists outside in the real Internet world. Behind the firewall, where your private services lie, the verification systems of Lets Encrypt can’t reach easily. Besides, you would probably not need to use a real-world domain inside your firewall, with entries most likely to be using private IP addresses, which Letsencrypt cannot really verify.

So the only option is to run your own certificate chain inside your walls. That means setting up a root CA, and an intermediate CA (optional but recommended) etc.

The best guide that I found

I read a lot of different guides over time, and even this weekend, but the one which seemed the most useful was OpenSSL Certificate Authority by Jamie Nguyen. The guide provides step by step instructions for creating the root CA certs, the intermediate CA certs, and finally the server certs that you needed in the first place. It even has a section on the revocation of certificates using CRL and OCSP.

While the generation of root and intermediate certificates themselves were pretty much as described, there were several problems with the final certificates which were generated - they were rejected by all three browsers I was testing on - Chrome, Firefox, and Safari. And this was after I had added the root CA certs to the Mac Keychain and Firefox certificate store.

And this is where the 2020 part of my post comes in. Jamie’s guide from 2015 has been succeeded by a bunch of changes in recent years.

The “Common Name” field in an SSL certificate is no longer supported

Chrome 58 and later specifically started enforcing this for the last couple of years. As this article from Hashedout blog explains:

Many people don’t know that the “Common Name” field of an SSL certificate, which contains the domain name the certificate is valid for, was actually phased-out via RFC nearly two decades ago. Instead, the SAN (Subject Alternative Name) field is the proper place to list the domain(s).

However, this has been ignored and for many years the Common Name field was exclusively used. Chrome is finally fed up with the field that refuses to die. In Chrome 58, the Common Name field is now ignored entirely.

This means certificates that were exclusively using that field to indicate the valid domain name are no longer supported. Publicly-trusted SSL certificates have been supporting both fields for years, ensuring maximum compatibility with all software – so you have nothing to worry about if your certificate came from a trusted CA.

This change will only affect private PKIs and other software that have not been following spec. If you notice any sites returning the error NET::ERR_CERT_COMMON_NAME_INVALID, it’s likely due to the certificate not using SANs properly.

This additional blog post mentioned in the article has some good historical background on the topic.

Starting September 1 2020, major browsers will not support SSL/TLS certificates with validity longer than 398 days

At first, Apple, and then Google Chrome team have announced that they would only support certificates with 1 year validity (+~30 days grace period). That is 75% of the browser market.

Just to clarify, it seems only leaf certificates are impacted here - I created intermediate certificates with 5 years validity (the root one was 20) and didn’t see a problem yet. I will find out more after September 1st, I guess.

Working around these new certificate constraints

So I had two things to fix - reduce my leaf certificate validity to 1 year, and add subjectAltName field to the certificates.

The first part was easy. Creating leaf certificates is a hassle when you have to create multiple ones. So I had, in any case, created a script to make it easier. I just had to reduce the validity period there.

But the second one was a puzzle. I tried a bunch of hacks all around the Internet to add the field in the Certificate Signing Request (CSR), but no matter what I do, it won’t appear in the final certificate. So I read around a bit more.

For one, this is a bad practice, as this Stackoverflow comment mentions. CAs will not copy all attributes to the final cert for pretty reasonable security reasons.

Secondly, it appears that openssl itself has two behaviors regarding copying this attributes. The default behavior is not to copy anything in the ca subcommand. If you do want this, adding the parameter -copy_extensions will copy everything.

So the consensus was to add the fields explicitly in the ca command, and, from the point of correctness, but not usefulness, add it to the CSR as well.

Adding the field itself to the commands is much of a chore - you have to add sections to the configs dynamically (in case you want to avoid creating a config file for every certificate you create), and all the hacks seemed tedious. I finally found what I felt was the most elegant option - using environmental variables. (SO comment)

So in the [server_cert] section in the openssl.cnf config file, I added this line:

subjectAltName=${ENV::SAN}

And in the [req] section, I added a line req_extensions = san_env and created a section at the bottom of the config with this:

[ san_env ]
subjectAltName=${ENV::SAN}

Now all I needed to do was to make sure in the script, I added this environment variable before invoking the openssl commands. And now, I just need a single invocation of my script per certificate and it will generate the certificate and the keys for me.

The final certificate creation script is here in this gist.

Adding trusted certificates to the browsers

The best way to add the trusted certificates to all my home devices was to just add the root CA certificate in all the right places. For the Mac, this meant adding it to the system keychain - both Safari and Chrome use this location. For Firefox, the way to add it seemed to be in its preferences. Notice, that I am not adding the intermediate certificate here. That is because I am sending it from the servers (next section).

Adding the certificate to nginx

There are plenty of documentation on how to do this. The important thing to remember here is that if you have only added the root CA certificate in the browser (which you should, and then hide the root CA private key somewhere safe), then you need to add the intermediate certificate to the server certificate in nginx.

As the nginx doc mentions:

Some browsers may complain about a certificate signed by a well-known certificate authority, while other browsers may accept the certificate without issues. This occurs because the issuing authority has signed the server certificate using an intermediate certificate that is not present in the certificate base of well-known trusted certificate authorities which is distributed with a particular browser. In this case the authority provides a bundle of chained certificates which should be concatenated to the signed server certificate. The server certificate must appear before the chained certificates in the combined file:

$ cat www.example.com.crt bundle.crt > www.example.com.chained.crt

The resulting file should be used in the ssl_certificate directive.

tech
Open Every Link in a Web Page In a New Tab Automating MySQL GTID Migration With Ansible