TJKeller.xyz SSL Certificates with NGINX and Certbot - The Correct Way
March 27, 2023
Tags: Nginx Openbsd Web Hosting

“What’s wrong with Certbot’s builtin NGINX plugin?”

The NGINX plugin for Certbot is not ideal for many reasons:

Using the plugin this way also generally contributes to a lack of understanding on the user’s end. In regards to how Certbot functions as well as how to configure SSL in NGINX.

Many of these criticisms also apply to the other Certbot plugins; they all over-complicate and obscure the actual mechanisms of certificate validation/retrieval.

Certbot also, by default, runs in an interactive mode which makes it a nightmare for those who would rather automate the process. This solution addresses that problem as well, although you could run it in unattended mode with a few flags using either method.

The correct method – Saying no to plugins

Certbot’s certonly mode is the solution. It allows for creating certificates manually without accessing your NGINX configuration.

To use it with NGINX, you will need to do the following:

  1. Setup a directory for and configure NGINX to serve Certbot’s acme-challenge files on port 80. Certbot must have read/write access to this directory, while NGINX only needs read access.
  2. Configure NGINX to serve the Let’s Encrypt certificates generated by certbot.
  3. Run certbot certonly to obtain certificates, and renew with certbot renew.

Tutorial

1. Completing ACME challenges

1.1. Background

ACME (Automatic Certificate Management Environment) is the protocol that Let’s Encrypt uses for issuing, managing, and renewing certificates. Basically, it allows them to verify that you own whatever domain you’re trying to get a certificate for. A challenge must be successfully completed before obtaining or renewing a certificate for each domain name (or subdomain) on the certificate. Certbot will run the ACME client automatically, and more or less manage the whole process.

There are several different ACME authentication methods. By default Certbot uses the HTTP-01 challenge, which I will also demonstrate as it is the most straightforward method. You can read more the different challenge types here: Let’s Encrypt Challenge Types.

You can use the DNS-01 challenge if you need a wildcard certificate (eg. *.example.com), but the renewal process more complicated. I may write about and link to it in the future if I ever decide to self-host a name server.

The HTTP-01 method simply involves serving a temporary token file at a specific route on your server. This route will look something like http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>. The route to this token is obfuscated by default. A token will be generated for each domain name on the certificate. The ACME challenge is completed once the ACME server (Let’s Encrypt) is able to verify the all tokens, and Certbot will delete the token files.

1.2. Basic Procedure in NGINX

Create a directory for the ACME tokens (called the ‘webroot’). /var/www/certs is what Certbot uses by default.

Then serve this directory on port 80 in your NGINX config.

You can serve the ACME tokens using this directive on all of your web servers:

1
2
3
4
5
6
# listen 80;

location /.well-known/acme-challenge/ {
	root /var/www/certs;  # Path to your webroot directory
	default_type text/plain;
}

To simplify this, save that directive as acme-challenge in your /etc/nginx, and include it like this:

1
2
3
4
5
6
7
8
server {
	listen 80 default_server;
	#listen [::]:80 default_server;  # ipv6
	include acme-challenge;
	location / {
		return 301 https://$host$request_uri;  # Try to use https
	}
}

This is how I configure my ACME challenges in NGINX. All other routes only listen on 443, so it is simpler to handle the ACME challenges on default_server block and try to redirect all other http requests to https. Although, you could also include acme-challenge in all of your server blocks that require a certificate (or even in your ssl-params file, created in the next step) if you wanted.

2. Configuring SSL in NGINX

2.1. NGINX’s Certificate Catch 22

Before configuring SSL in NGINX, certificates must be obtained. Without a certificate, NGINX will disallow the use of port 443 entirely. Even worse, if an invalid certificate or certificate path is configured, NGINX will crash outright reporting an ‘invalid configuration’.

This leaves two options:

  1. Obtain valid certificates before configuring SSL in NGINX.
  2. First create fake (self-signed) certificates to make NGINX happy, then obtain the real certificates.

Option A is simpler, but option B has the benefit of allowing you to obtain new certificates without modifying your NGINX configuration. That may seem trivial until you want to create a setup that is deployed with code eg. docker compose or Ansible.

Since I want to encourage you to use option B, I will proceed with configuring NGINX prior to obtaining the certificates.

2.2. NGINX SSL Parameters

Create a new file in /etc/nginx called ssl-params containing:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
listen 443 ssl;

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

# Default options in certbot
ssl_session_cache shared:le_nginx_SSL:10m;
ssl_session_timeout 1440m;
ssl_session_tickets off;

ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;

ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";

ssl_certificate and ssl_certificate_key will be the path to your fullchain.pem and privkey.pem respectively.

include ssl-params can then be used in all your secured server blocks as such:

1
2
3
4
5
6
7
server {
	server_name tjkeller.xyz www.tjkeller.xyz;
	include ssl-params;

	root /var/www/sites/tjkeller/public;
	index index.html;
}

2.3. Create Self-Signed Certificates

As mentioned above, NGINX will get very upset with you if either your ssl_certificate or ssl_certificate_key is invalid or does not exist.

To remedy this, you can use the following script to create a self-signed certificate keypair to be replaced by the valid keys once they are obtained:

1
2
3
4
5
6
#!/bin/sh

site=example.com
cdir=/etc/letsencrypt/live/$site
! [ -d $cdir ] && mkdir -p $cdir
openssl req -x509 -subj "/CN=$site" -nodes -days 36500 -newkey rsa:2048 -keyout $cdir/privkey.pem -out $cdir/fullchain.pem

NGINX will happily serve these, so now you can proceed to obtaining the Let’s Encrypt certificates.

3. Obtain The Certificates – For Real This Time

3.1. Initial Obtain

Start your NGINX server and ensure that you can access each domain properly.

Then, run the following certbot certonly command: (I put it in script form so that you can save it)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#!/bin/sh

domains="-d tjkeller.xyz -d www.tjkeller.xyz"  # -d [domain] for each domain and sub-domain
email="tjk@tjkeller.xyz"
certname="tjkeller.xyz"  # Saved as /etc/letsencrypt/live/[certname]. Uses first domain if not set

# Delete existing certificate if it is self-signed
if ! openssl x509 -in /etc/letsencrypt/live/$certname/fullchain.pem -noout -issuer -subject | grep "O = Let's Encrypt"; then
	echo "Deleting old self-signed certificate"
	rm -rf /etc/letsencrypt/live/$certname
fi

# Create certificate
certbot certonly --cert-name "$certname" -n -v --agree-tos --webroot -w /var/www/certs --email "$email" --expand $domains

This will create your certificates in an unattended mode, and overwrite the self-signed certificates with the new ones.

NGINX should automatically start serving the new certificates, but you may need to restart NGINX if the old certificates were cached.

3.2. Renewing Existing Certificates

Create the following as a cronjob:

0 0 * * * certbot renew

This will renew your existing certificate every 24 hours, ensuring that it will not expire. Certbot will only replace your certificate when the renew command is ran within a two week or so window of expiration.

3.3. Expanding Existing Certificates

Existing certificates can be expanded to include more domains if desired. However, it is often easier to simply overwrite the old certificate with the new one using the certbot certonly command you used to create your certificates initially.