DEV Community


Posted on • Updated on

Modern HTTPS configuration

I've tried many ways of setting up HTTPS servers, and I've finally found a favorite method.

Instead of paying for production certificates, it's easy to verify your own certificates via cerbot and LetsEncrypt

The flow below is for Ubuntu, and involves using nginx to serve a file - as opposed to having the file served by your actual backend. I find this solution to be more elegant if you have full access to your server.

Installing cerbot and receiving a certificate

1. Install certbot

For ubuntu 20:

sudo snap install core; sudo snap refresh core
sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
Enter fullscreen mode Exit fullscreen mode

Earlier versions:

sudo add-apt-repository ppa:certbot/certbo
sudo apt-get update
sudo apt-get install certbot
Enter fullscreen mode Exit fullscreen mode

2. Run certbot

sudo certbot certonly --manual
Enter fullscreen mode Exit fullscreen mode

This will stop at a prompt; keep it open and follow the next steps.

3. Set up nginx to serve the right data for certbot

# Snap didn't have nginx when I was doing this setup, so:
sudo apt install nginx
sudo ufw allow 'Nginx HTTP'
Enter fullscreen mode Exit fullscreen mode

(with reference to ):

# By default nginx will serve files from /var/www/html
# Put the cert there by default, or see what works best for your setup:
sudo mkdir /var/www/html/.well-known
sudo mkdir /var/www/html/.well-known/acme-challenge
sudo vim /var/www/html/.well-known/acme-challenge/<filename from certbot>
<copy in certbot data>
sudo chmod a=r /var/www/html/.well-known/acme-challenge/<filename from certbot>

# We don't need to change anything with the above folder structure.
# Alternatively, we can change the config
sudo vim /etc/nginx/sites-enabled/default
# If you do change the config, reload nginx
sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

4. Finalizing verification

Go back to certbot; it should be prompting to hit Enter. Do that, and the verification should complete:

 - Congratulations! Your certificate and chain have been saved at:
   Your key file has been saved at:
   Your certificate will expire on 2021-06-07. To obtain a new or
   tweaked version of this certificate in the future, simply run
   certbot again. To non-interactively renew *all* of your
   certificates, run "certbot renew"
Enter fullscreen mode Exit fullscreen mode

Using the certificate

The newly created certificates will only be available to root

sudo chmod 0755 /etc/letsencrypt/{live,archive}
# In the doc above, this isn't mentioned as necessary, but I couldn't get access to the privkey w/o being explicit
sudo chmod 0755 /etc/letsencrypt/live/
Enter fullscreen mode Exit fullscreen mode

Now, you can either choose to use these certificates directly by your service, or let nginx deal with that layer.

Configuring nginx

To make a server available on HTTPS w/o a port suffix, it's necessary to run on port 443. That requires elevated privileges in linux, and it's not a good idea to run Node.js that way - though nginx is perfectly suited just for this.

A good way to set up port-less access is to configure port forwarding via nginx: from 443 to e.g. 8080 - you can connect from nginx to your service directly via HTTP w/o SSL. It's also possible to configure redirects from http (port 80), but in this config port 80 is only serving the certificate files:

# Open the nginx config
sudo vim /etc/nginx/sites-available/default
# Then, here is an example of a working config:

# This is just to serve the data that certbot wants, it's nginx's default config
server {
  listen 80 default_server;
  listen [::]:80 default_server;

  root /var/www/html;

  index index.html index.htm index.nginx-debian.html;

  server_name _;

  location / {
    # First attempt to serve request as file, then
    # as directory, then fall back to displaying a 404.
    try_files $uri $uri/ =404;

# Port forwarding - this is what we want to add
server {
  listen 443 ssl; #

  ssl_certificate           /etc/letsencrypt/live/;
  ssl_certificate_key       /etc/letsencrypt/live/;

  ssl_session_cache  builtin:1000  shared:SSL:10m;
  ssl_protocols  TLSv1 TLSv1.1 TLSv1.2;
  ssl_prefer_server_ciphers on;

  access_log            /var/log/nginx/yourdomain.access.log;

  location / {
    proxy_set_header        Host $host;
    proxy_set_header        X-Real-IP $remote_addr;
    proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header        X-Forwarded-Proto $scheme;

    # NOTE: This will also work if you use
    # proxy_pass          https://localhost:8443; 
    # This could be desirable to e.g. use https
    # for the app in all environments - that way
    # you can run in development w/o nginx on https.
    proxy_pass          http://localhost:8080;
    proxy_read_timeout  90;

    # Just make sure to also update this to
    # proxy_redirect      https://localhost:8443;
    # if you want to use https for the server
    proxy_redirect      http://localhost:8080;
Enter fullscreen mode Exit fullscreen mode

Don't forget to replace with your actual domain, then

sudo systemctl reload nginx
Enter fullscreen mode Exit fullscreen mode

Running without nginx

Give your project access to these environment variables

SSL_PRIVATE_KEY_PATH = /etc/letsencrypt/live/
SSL_CERTIFICATE_PATH = /etc/letsencrypt/live/
Enter fullscreen mode Exit fullscreen mode

NOTE: this style will also work with the nginx config, if you proxy_pass and proxy_redirect to an https address, as per the note in the nginx config above. Yes, you can use the same certificates for your app, and nginx will accept them, and the port forwarding will work correctly.

E.g. in Node.js you might load them like this:

const fs = require('fs')
const express = require('express')
const https = require('https')

const loadSsl = () => {
  const privateKey  = fs.readFileSync(process.env.SSL_PRIVATE_KEY_PATH, 'utf8')
  const certificate = fs.readFileSync(process.env.SSL_CERTIFICATE_PATH, 'utf8')
  return { key: privateKey, cert: certificate }

const express = express()
const server = https.createServer(loadSsl(), express)
server.listen(process.env.PORT, () => { // e.g. port 8443
  console.log(`Server live on port ${process.env.PORT}`)
Enter fullscreen mode Exit fullscreen mode

And now you can access your service on (follow the nginx setup above to get rid of the PORT suffix).

Running in dev

The above would work on the server with those certificates, but what's a good way to go about running in development?

It's common to just use HTTP, or issue self-signed certificates, and then accept self-signed certificates in various parts of your codebase in dev.

Instead, I prefer using a local Certificate Authority to issue the localhost certificates - it's the most seamless way to get your development environment to be maximally similar to production.

This can be done with the magical tool mkcert ( ), and it's easier than it sounds:

# Only do this once for all mkcert projects
brew install mkcert
brew install nss # for Firefox
mkcert -install
# Set up this repo with mkcert certificates
# I personally just keep my mkcert right in the folder of the repo.
# Don't forget to add the directory to .gitignore!
mkdir mkcert
cd mkcert
mkcert localhost
Enter fullscreen mode Exit fullscreen mode

Now in dev, just add these to your environment (assuming you have the HTTPS loading logic from the previous section):

SSL_PRIVATE_KEY_PATH = mkcert/localhost-key.pem
SSL_CERTIFICATE_PATH = mkcert/localhost.pem
Enter fullscreen mode Exit fullscreen mode


The issued certificates are good for 3 months. To get the time remaining, run:

sudo certbot certificates
Enter fullscreen mode Exit fullscreen mode

To re-issue fresh certificates manually, run:

sudo certbot --force-renewal
Enter fullscreen mode Exit fullscreen mode

This will renew all certificates from certbot (i.e. it's intended to support multiple certificates and services on one machine).

Certbot is built to be automated - so choose your own style, set up a crontab if you like. The general-purpose renew command is

sudo certbot renew
Enter fullscreen mode Exit fullscreen mode

For more information, see

Discussion (0)