Introduction
When we develop a website, we usually connect to our development server via HTTP with a special port number like 3000, 5000, or 8888, e.g. http://localhost:3000
. Everything works fine and you are happy 😃 One day, you need to connect the dev server via HTTPS. You follow some instructions online but no matter what you've tried, you always get the Not Secure page in Chrome:
Have you run into this situation? In this article, I will show you how to solve this problem to generate valid certificates that can work in Chrome for the local host.
Please aware that the method in this article does not work for Firefox
Generate a certificate for localhost
The solution is to generate a certificate that includes localhost as the domain name and put this information in the certificate's field recognized by modern browsers, so when the browsers check the self-signed certificate and the current URL, it will know it's a valid certificate. It makes sense, right? We will use openssl
to generate such a certificate. I use a Macbook, so the example I show here is under macOS
Install openssl
Use Homebrew or other way more suitable for you to install openssl
if you don't have openssl
installed in your system yet.
brew install openssl
# or
brew upgrade openssl
Generate a certificate by openssl
To generate the certificate we want, execute the command below:
openssl req \
-newkey rsa:2048 \
-x509 \
-nodes \
-keyout localhost.key \
-new \
-out localhost.crt \
-subj /CN='Localhost self-signed certificate' \
-addext "subjectAltName=DNS:localhost" \
-sha256 \
-days 365
You will get
- a certificate,
localhost.crt
- a private key,
localhost.key
You need these two files to start a HTTPS server. Done!
It's super simple, eh? but it's definitely not straightforward unless you are already familiar with SSL certificates. To be honest, openssl
's commands always feel like black magic spells. In the past, I just grabbed pieces from different answers on the internet and tried to do what I want. This time, I do want to understand. Let me (and ChatGPT😆) explain to you what this command actually mean.
-
openssl req
: tell openssl to generate CSR. Although it's the main parameter but it's not important here. I'll explain more below. -
-newkey rsa:2048
: use RSA to generate private key. If you don't know what's RSA, you only need to know that is the reason we have a private key -
-x509
: instead of generating a CSR, generate a X.509 certificate. This mysterious X.509 is just the a standard for the certificate used by HTTPS. You can think it like a JSON with specific keys. -
-nodes
: don't use passphrase to encrypt the private key -
-keyout localhost.key
: what's the private key's filename. We call itlocalhost.key
here -
-new
: straightforward, we want a new certificate -
-out localhost.crt
: we want the output certificate's filename to belocalhost.crt
-
-subj /CN='Localhost self-signed certificate'
: Set Subject's value. Subject is a field in X.509. CN is Common Name, it can be anything like "My first self-signed certificate" -
-addext "subjectAltName=DNS:localhost"
: We want to add an extension field, Subject Alternative Name, and put DNS:localhost. This is the most important line. Basically, you can ignore all other parameters on this list. -
-sha256
: use SHA-256 to sign the certificate -
-days 365
: The validity is 365 days
Trust this certificate in your OS
Because our self-signed certificate is not issued by a trusted CA(Certificate Authority) like GoDaddy, modern browsers will be skeptical and won't trust it. Therefore, we need to make our computer "trust" our own certificate manually:
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain localhost.crt
This command registers your new certificate in the system and also trusts it. When your OS trusts it, your browser will trust it accordingly.
Test
We can use the newly generated certificate and private key to run any web server that supports HTTPS. It doesn't matter which web server you want to use. Since Deno2 is the hottest stuff recently and is also super simple, we can use Deno2 to demo.
You can install Deno2 by following their website's instructions, https://deno.com/. After installing Deno2, first we can make an empty folder. Second, we can put the localhost.crt
and localhost.key
into that folder. Then create a file in the folder, main.ts
like below to run a HTTPS web server using the certificate and the private key at port 8000:
// main.ts
Deno.serve({
port: 8000,
cert: Deno.readTextFileSync("./localhost.crt"),
key: Deno.readTextFileSync("./localhost.key"),
}, (_req) => {
return new Response("Hello, World!");
});
We can run the dev server by executing
deno main.ts
Open Chrome and link to https://localhost:8000
, you should see a nice Hello World without any Not Secure warning. Viola!
Generate a certificate for an arbitrary domain
This method can not only be used for localhost
. We can also generate a certificate for an arbitrary domain pointing to localhost. Let's say we want to test abc.dev
on our machine.
Add the domain in /etc/hosts
First, we need to add a new entry in /etc/hosts to make it point to 127.0.0.1, our local:
# executing `sudo vim /etc/hosts` in the terminal
127.0.0.1 abc.dev
Follow the same steps as localhost's example
I hope you get out of vim successfully. Now we only need to do the same things as we did for localhost
, the only difference is that we will add abc.dev
into subjectAltName
- Generate the certificate
openssl req \
-newkey rsa:2048 \
-x509 \
-nodes \
-keyout abc.dev.key \
-new \
-out abc.dev.crt \
-subj /CN='Localhost self-signed certificate' \
-addext "subjectAltName=DNS:localhost,DNS:abc.dev" \
-sha256 \
-days 365
- trust the newly generated certificate
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain abc.dev.crt
- remember to change the
main.ts
so it uses abc.dev.crt and .key
Deno.serve({
port: 8000,
cert: Deno.readTextFileSync("./abc.dev.crt"),
key: Deno.readTextFileSync("./abc.dev.key"),
}, (_req) => {
return new Response("Hello, World!");
});
Test
Voila!
You'll find that https://localhost:8000 also works! It's because we also add DNS:localhost
in subjectAltName
How to remove the certificate from the keychain?(MacOS)
We have now learned how to make and trust self-signed certificates. I guess we will have lots of trusted certificates in the system. I guess most of the time, we just want to test some things for development. After we remove the certificates for the testing. We can also remove their records of being trusted from the system. To do that in MacOS, we can open Keychain Access app, find the certificates we trusted and delete them.
MacOS Sequoia removes Keychain Access app from the launch pad, we can still find it at
/System/Library/CoreServices/Applications/Keychain Access.app
What is subjectAltName(SAN)?
Here comes a big question: why it works? SAN is just an extra field inside X.509 certificate.To dig deeper, we need to expand the certificate to see what's inside. We can use the command:
openssl x509 -in localhost.crt -text -noout
It will print out the content of the certificate. You can see it's a YAML-like, nested structure:
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
68:5b:f9:5b:72:d2:1f:cd:fd:1b:f9:43:bb:b9:66:cc:53:ab:14:ca
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=Localhost self-signed certificate
Validity
Not Before: Oct 29 03:45:30 2024 GMT
Not After : Oct 29 03:45:30 2025 GMT
Subject: CN=Localhost self-signed certificate
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:a6:84:2a:57:5c:c9:a1:69:60:66:6b:43:07:a3:
4c:56:68:43:eb:10:29:65:ba:08:d8:75:e8:47:d1:
7e:57:16:95:0d:e1:73:3b:69:3f:37:6f:e2:8e:c9:
82:74:75:46:e4:16:0f:83:b3:f2:13:77:45:e1:3f:
6f:1d:78:36:1b:56:04:65:b8:79:97:c5:15:30:7b:
28:59:7a:db:c0:7e:1c:32:f3:03:26:f9:5e:4d:ea:
db:76:3d:98:ec:12:ae:6b:10:b6:54:5c:68:88:47:
a2:b8:2c:2d:f8:c2:ba:7b:ca:14:a3:a6:e0:ec:49:
13:08:ed:bc:67:4f:ef:e7:ab:24:22:65:a8:60:45:
74:87:d3:f7:ea:3d:a2:05:e3:46:07:6c:32:2c:48:
4d:bd:e0:78:6f:05:dc:7e:8c:b3:4e:1a:e8:c9:0a:
cd:3b:2f:cd:9f:2d:a7:7d:4c:76:d8:64:de:7f:9b:
06:6c:de:10:cc:44:40:e3:c3:3e:41:85:f0:ca:96:
0d:7a:ab:94:26:c9:e3:cb:9a:ca:ab:52:86:c2:28:
03:7e:7a:7b:43:29:d2:aa:35:d0:69:1b:37:8f:cb:
45:6a:c2:20:37:b3:6e:05:02:43:0b:d5:dd:1d:27:
08:51:02:87:85:e3:75:93:03:86:b7:43:8e:b3:15:
a0:81
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
2F:90:ED:0C:14:D2:64:11:F8:0C:54:29:EF:EC:55:A7:3B:B1:30:7E
X509v3 Authority Key Identifier:
2F:90:ED:0C:14:D2:64:11:F8:0C:54:29:EF:EC:55:A7:3B:B1:30:7E
X509v3 Basic Constraints: critical
CA:TRUE
X509v3 Subject Alternative Name:
DNS:localhost, DNS:abc.dev
Signature Algorithm: sha256WithRSAEncryption
Signature Value:
11:05:1f:f5:80:aa:6d:42:c1:02:9d:a3:3d:c4:0d:29:d3:5b:
2b:d8:eb:91:7b:56:cf:de:1d:96:76:85:93:91:fa:95:df:86:
d4:68:a3:6b:3b:dd:92:61:69:dc:05:73:51:5d:f0:2a:7c:a4:
42:5a:dc:cf:3e:d0:92:19:7a:38:02:b1:20:9d:ce:c8:58:a2:
3a:45:24:c6:d7:f2:bf:72:5a:bb:cf:49:9e:96:49:02:08:15:
dd:da:5e:e4:01:40:92:e8:b9:86:7c:da:b7:1b:dc:bc:95:c9:
6c:38:5c:b7:4c:9d:df:c2:11:49:8e:db:72:41:9f:a6:bd:8e:
aa:4f:b1:8f:79:c4:ea:eb:03:f1:92:0c:20:3c:f1:d5:8e:9c:
87:b5:23:66:e9:60:de:10:b9:a6:3b:29:af:f0:bd:22:1f:ec:
21:c1:10:b9:0b:63:94:9c:c3:2f:87:09:4e:56:0e:24:ae:5d:
53:54:04:9a:38:10:cb:99:1e:ee:63:47:e3:d3:8b:4b:bd:af:
f3:76:7f:73:98:fb:71:87:9b:d0:a1:94:d4:4c:7e:04:95:6d:
83:57:54:93:da:81:44:51:c0:ec:2f:48:24:17:8b:58:65:58:
08:5c:2b:13:cd:3c:85:b8:2a:2e:06:37:d5:35:90:fb:7f:14:
da:11:d2:b2
There is a field called Subject and a field called X509v3 Subject Alternative Name
#...
Subject: CN=Localhost self-signed certificate
#...
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:localhost, DNS:abc.dev
Subject's CN(Common Name) used to be the domain name like CN=abc.dev
. However, it can have only one value. It results in that one certificate can only have one domain name bound on it. Therefore, Subject Alternative Name is invented as an extension field to let one certificate support multiple domain names.
The browser used to check only the Subject field in the past, but it somehow it checks only the SAN now. That's why it cannot work if we just look up some instructions online because they often only embed the domain names in the Subject field, and that way has been outdated.
Conclusion
I think the mechanism of HTTPS is complex for many people. That's why it feels hard to understand. Maybe it's not that bad because it's for security. The fewer people understand it the better. However, it will always be a pain in the ass if developers like us do not have a chance to learn about it.
This solution works well for Safari, Chrome or Chromium browsers. However, it does not work for Firefox, which will return MOZILLA_PKIX_ERROR_SELF_SIGNED_CERT warning. To make a self-signed certificate work for Firefox, we may need to create our own CA to sign the certificate. I may write another article showing how to do that one day 😀
Top comments (0)