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. 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;
}
}
Don't forget to replace yourdomain.com
with your actual domain, then
sudo systemctl reload nginx
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
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}`)
})
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?
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 ( https://github.com/FiloSottile/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
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
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
Discussion (0)