DEV Community

Cover image for How to generate self-signed certificates for localhost
Kevin Luo
Kevin Luo

Posted on

How to generate self-signed certificates for localhost

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:

Chrome's warning saying

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 it localhost.key here
  • -new: straightforward, we want a new certificate
  • -out localhost.crt: we want the output certificate's filename to be localhost.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
Enter fullscreen mode Exit fullscreen mode

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!");
});
Enter fullscreen mode Exit fullscreen mode

We can run the dev server by executing

deno main.ts
Enter fullscreen mode Exit fullscreen mode

Open Chrome and link to https://localhost:8000, you should see a nice Hello World without any Not Secure warning. Viola!

Successful Hello World wit URL localhost:8000 in the browser

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
Enter fullscreen mode Exit fullscreen mode

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

  1. 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
Enter fullscreen mode Exit fullscreen mode
  1. trust the newly generated certificate
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain abc.dev.crt
Enter fullscreen mode Exit fullscreen mode
  1. 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!");
});
Enter fullscreen mode Exit fullscreen mode

Test

Voila!

Successful Hello World wit URL abc.dev:8000 in the browser

You'll find that https://localhost:8000 also works! It's because we also add DNS:localhost in subjectAltName

Successful Hello World wit URL localhost:8000 in the browser

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.

Keychain Access app in macOS

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)