DEV Community

loading...

Modern HTTPS configuration

alexeydc profile image Alexey Updated on ・5 min read

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 https://certbot.eff.org/ and LetsEncrypt https://letsencrypt.org/.

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 https://docs.nginx.com/nginx/admin-guide/web-server/serving-static-content/ ):

# 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:
   /etc/letsencrypt/live/yourdomain.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/yourdomain.com/privkey.pem
   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 https://certbot.eff.org/docs/using.html#where-are-my-certificates

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/yourdomain.com/privkey.pem
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; # https://stackoverflow.com/questions/51703109/nginx-the-ssl-directive-is-deprecated-use-the-listen-ssl
  server_name yourdomain.com;

  ssl_certificate           /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
  ssl_certificate_key       /etc/letsencrypt/live/yourdomain.com/privkey.pem;

  ssl_session_cache  builtin:1000  shared:SSL:10m;
  ssl_protocols  TLSv1 TLSv1.1 TLSv1.2;
  ssl_ciphers HIGH:!aNULL:!eNULL:!EXPORT:!CAMELLIA:!DES:!MD5:!PSK:!RC4;
  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 https://yourdomain.com;
    # if you want to use https for the server
    proxy_redirect      http://localhost:8080 https://yourdomain.com;
  }
}
Enter fullscreen mode Exit fullscreen mode

Don't forget to replace yourdomain.com 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/yourdomain.com/privkey.pem
SSL_CERTIFICATE_PATH = /etc/letsencrypt/live/yourdomain.com/fullchain.pem
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 yourdomain.com:PORT (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?

There are 2 reasonable ways: either use http, or set up self-issued certificates (of course, developing on a machine reachable by certbot's verifier is also always an option, but I personally prefer to be able to develop completely locally on any network I'm connected to).

I don't like using http, because I like development/test to be as similar to production as possible.

So what I do is issue self-signed dev certificates https://chovy.com/web-development/self-signed-certs-with-secure-websockets-in-node-js/

openssl req -newkey rsa:2048 -nodes -keyout key.pem -x509 -days 365 -out certificate.pem
# These can be stored wherever, as long as the
# app has access to them via the env vars above.
# I use dotenv, so my .env file has the path
Enter fullscreen mode Exit fullscreen mode

There's several ways to make self-signed certificates work with chrome:
1.) Explicitly allow our certificate https://www.pico.net/kb/how-do-you-get-chrome-to-accept-a-self-signed-certificate
2.) Allow all localhost https chrome://flags/#allow-insecure-localhost ( https://stackoverflow.com/a/31900210/4471481 )

I couldn't get (2) to work. Basically, if you double-click the certificate, it'll be added to Keychain on Mac. I had to restart Chrome to get it to recognize it.

Maintenance

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 https://certbot.eff.org/docs/using.html?highlight=renew#renewing-certificates

Discussion (0)

pic
Editor guide