Setting up a local https website without certificate warnings was a frustrating task for me, so creating this blog post as a cheatsheet for the future...

You can check out the video at the end of the blog post, where I go step by step for setting up a secure local domain. Only code change in this blog post is that I added support for localhost too.

https localhost

Let's setup a simple hello world

Docker seems like a good way to try out something fast and clean it afterwards, so let's run a hello world on port 4000.

 docker run --rm -it -p 4000:80 strm/helloworld-http

We're going to map "http://localhost:4000" to "https://example.local" domain.

/etc/hosts local domain mapping

Create certificates

For this example, same certificate will be used as a server cert and as a certificate authority.

This command covers localhost and example.local domain.

openssl req -x509 -out example.crt -keyout example.key -newkey rsa:2048 -nodes -sha256  -new -subj "/C=GB/CN=example.local" \
                  -addext "subjectAltName = DNS:example.local, DNS:localhost" \
                  -addext "certificatePolicies = 1.2.3.4" 

sudo chmod 644 example.crt
sudo chown root:root example.crt

sudo chmod 600 example.key
sudo chown root:ssl-cert example.key

sudo mv example.crt /etc/ssl/certs/example.crt
sudo mv example.key /etc/ssl/private/example.key

Pay attention to subjectAltNames, that's the key part.

Configure nginx

Created certificates will be referenced from nginx configuration.

Create a config in /etc/sites-available and add a symlink to /etc/sites-enabled.

upstream example {
    server 127.0.0.1:4000;
}

server {
    listen 443 ssl http2;
    server_name example.local;
    ssl_certificate      /etc/ssl/certs/example.crt;
    ssl_certificate_key  /etc/ssl/private/example.key;
    ssl_ciphers          HIGH:!aNULL:!MD5;
    ssl_protocols        TLSv1 TLSv1.1 TLSv1.2;
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    location / {
        proxy_set_header X-Real-IP $remote_addr;
        proxy_pass http://example;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }

}


server {
    listen 80;
    listen [::]:80;
    server_name  example.local;

    return 301 https://example.local$request_uri;
}
  • Configuration for port 80 just redirects every http://example.local request to https://example.local
  • upstream is a neat way to organize your urls/group of urls for load balancing. Not needed for this example, but looked cleaner.
  • Configuration for port 443 says to proxy all https://example.local request to port 4000. There's a HSTS header also, which in short instructs browser that https is preferred. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security

Add a Certificate Authority

Go to Chrome->Settings->Privacy and Security->Manage Certificates->Authorities->Find /etc/ssl/example.crt and click to add it.

Reload https://example.local, no more warnings!

If you still get a warning, try incognito mode or reloading Chrome.

Fix for Node self signed certificate error

Node will probably fail with reason: self signed certificate
at ClientRequest or DEPTH_ZERO_SELF_SIGNED_CERT.

To fix that, export either:

  • NODE_TLS_REJECT_UNAUTHORIZED=0 (you'll see ugly warnings in logs)
    or
  • NODE_EXTRA_CA_CERTS=/etc/ssl/certs/example.crt

For the next read you might wanna checkout how to setup full ssl encryption.