A long time ago, I set up an internal PKI so I could create my own TLS certificates to add to internal devices and HTTPS servers. I used this primarily for my EdgeOS router since that was the main device I would log into that would give me a warning about untrusted certificates.
That was all the way back in 2016. Since then, I’ve kind of kept on top of things, renewing the certificate when it expired a few years ago, but it had expired yet again so I was faced with that dreaded warning when connecting to my router. This time, though, creating a new cert using the old commands wasn’t good enough, because it was missing a Subject Alternate Name (SAN), and that prompted a slightly different warning with the same result: no trust in the browser.
What happened? Using a SAN instead of the Common Name for certificate validation has been required by browsers for a while (since 2017 in Chrome for example). This is because the Common Name is ambiguous, whereas the SAN can specify a domain, IP, or URI; if you want the gnarly details, see RFC2818 and RFC6125.
This wasn’t a problem in 2016 since browsers weren’t requiring this, but it’s been an issue for a while now. Unfortunately, it’s a bit harder to create a certificate-signing request (CSR) and sign a certificate in such a way that a SAN is included. There are ways you can do this using OpenSSL configuration and extension files, but I was looking for one-liners like I could do before.
Eventually, thanks to some searching, I was able to find a way to add a DNS SAN to a certificate and still make it a one-liner for the most part. To start by generating the CSR, I ran the following command (replaced FQDN with the FQDN of the target server):
openssl req -new -sha256 -subj "/C=US/ST=Minnesota/L=Minneapolis/O=MyOrg/CN=FQDN" -addext "subjectAltName = DNS:FQDN" -key private.key -out req.csr
That generated a CSR with the right extension, as you can see when looking at the text dump via OpenSSL:
Attributes:
Requested Extensions:
X509v3 Subject Alternative Name:
DNS:FQDN
I thought that was it and signing it the old way would be all that I’d need, but nope, it didn’t include the SAN (a fact that a user calls out explicitly in the page where I found this. So I needed to find out how to sign it with the extension as well. After a bit more digging, I found that this would do the trick
openssl x509 -sha256 -req -days 1096 -in req.csr -CA ia.crt -CAkey ia.key -extensions SAN -extfile <(cat /etc/ssl/openssl.cnf <(printf "\n[SAN]\nsubjectAltName=DNS:FQDN")) -set_serial 4 -out cert.pem
This essentially creates the proper configuration on the fly, and as a result the certificate that is generated has the proper SAN. After adding to the router and restarting the lighttpd daemon, we are trusted again!
If there was a need to add another SAN such as an IP address, the same method could be used.
That’s it!