Modern HTTPS configuration
Tags: nginx, tutorial, node, webdev
Published: 2021-03-10 01:34:58

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
```

Earlier versions:
```
sudo add-apt-repository ppa:certbot/certbo
sudo apt-get update
sudo apt-get install certbot
```

## 2. Run certbot
```
sudo certbot certonly --manual
```

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'
```

(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
```

## 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"
```

# 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
```

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.

I usually create a file in `sites-available`, then symlink it inside of `sites-enabled`:

```bash
sudo vim /etc/nginx/sites-available/yourdomain.com
```

Let's say you're running your application server on port 8080 (you can use HTTP with your application, since nginx will handle the HTTPS handshake/encryption):
```nginx
server {
  listen 443 ssl http2;
  server_name yourdomain.com www.yourdomain.com;
    ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem; # managed by Certbot

  include ssl_config;
  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;

    # Fix the "It appears that your reverse proxy set up is broken" error.
    proxy_pass          http://localhost:8080;
    proxy_read_timeout  90;

    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $host;
  }

}

server {
    if ($host = yourdomain.com) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


  listen 80;
  server_name yourdomain.com www.yourdomain.com;
  return 301 https://yourdomain.com$request_uri;
}
```
Don't forget to replace `yourdomain.com` with your actual domain.

After the config is in `sites-available`, it should be symlinked into `sites-enabled`:
```bash
sudo ln -s /etc/nginx/sites-available/your-domain.com  /etc/nginx/sites-enabled/your-domain.com
```

If you just have 1 site, you could use `sudo vim /etc/nginx/sites-available/default` - just append the config above to that file.

Once the config is set up,
```bash
sudo systemctl reload nginx
```

# HTTPS in local development
Usually, you can rely on HTTP in local development - since browsers special-case behavior for `http://localhost`, and don't require the same degree of security as with remote hosts.

There are [some rare situations](https://web.dev/when-to-use-local-https/) where HTTPS locally is useful - e.g.
some OAuth providers that you may need to redirect to your site from may disallow `http` redirect URLS. 

In those cases, mkcert can be very handy ( https://github.com/FiloSottile/mkcert ):
```
# 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
```
After that you'll need to get your application server to load those certificates and set up HTTPS - I'll leave that out of scope for this article.

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

```
sudo certbot certificates
```

To re-issue fresh certificates manually, run:
```
sudo certbot --force-renewal
```

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
```

For more information, see https://certbot.eff.org/docs/using.html?highlight=renew#renewing-certificates