Automating SSL Certificates using Nginx & Letsencrypt - Without the Catch 22

There’s a ton of smart people out there who’ve written guides on setting up Nginx, and automating Letsencrypt —but none that setup automation and work 100% correctly out of the box. That’s the goal here, I’ll be documenting all the steps required to get your web application protected by an automatically renewing SSL certificate.

NOTE: The following commands will require root user permissions. You might want to run sudo su - first.

Installing Nginx

The first step is to install Nginx. If all you want to do is install the standard version, it should be available via your distro’s package manager.

# install Nginx on Ubuntu
apt-get update
apt-get install -y nginx

Install Letsencrypt.sh

The second step is to install a Letsencrypt client. The official client is a bit bloated and complicated to setup. I prefer to use the letsencrypt.sh client instead as its code is easier to understand, has few dependencies and its incredibly simple to automate.

# install letsencrypt.sh dependencies (most should already be installed)
apt-get install -y openssl curl sed grep mktemp git

# install letsencrypt.sh into /srv/letsencrypt
git clone https://github.com/lukas2511/letsencrypt.sh.git /srv/letsencrypt

Configure Letsencrypt

Letsencrypt.sh requires some configuration, but not much, the defaults work out of the box. That means that all you need to do is

  • create a domains.txt file with the url(s) of the site(s) you’re generating ssl certificates for
  • create a acme-challenges folder that can be accessed by Nginx.

Here’s how we can do that.

# First we need to make the client executable
chmod +x /srv/letsencrypt/letsencrypt.sh
# Then we need to create an ACME challenges folder and symlink it for Nginx to use
mkdir -p /srv/letsencrypt/.acme-challenges
mkdir -p /var/www/
ln -s /srv/letsencrypt/.acme-challenges /var/www/letsencrypt

Finally we need to specify the site(s) that will be protected by Letsencrypt ssl certificates.

echo "www.example.com" >> /srv/letsencrypt/domains.txt

Read more about the domains.txt file format here

Configure Nginx (without the Catch-22)

Up to now, the steps I’ve shown have been the same as almost any other Letsencrypt+Nginx guide you’ve seen online. However most of other guides will tell you to configure Nginx in a way that requires manual intervention.

A basic Letsencrypt Nginx configuration file looks like this:

# DONT USE THIS, IT WONT WORK.

# /etc/nginx/sites-enabled/example.conf
# HTTP server
server {
	listen      80;
	server_name www.example.com;
	location '/.well-known/acme-challenge' {
		default_type "text/plain";
		alias /var/www/letsencrypt;
	}
	location / {
		return 301 https://$server_name$request_uri;
	}
}
# HTTPS
server {
	listen       443;
	server_name  www.example.com;
	ssl                  on;
	ssl_certificate      /srv/letsencrypt/certs/www.example.com/fullchain.pem;
	ssl_certificate_key  /srv/letsencrypt/certs/www.example.com/privkey.pem;

	...
}

There’s a problem with this though. If you try starting up your Nginx server with this config, it’ll throw an error because the SSL certificate files don’t exist. And you can’t start the letencrypt.sh command to generate the SSL certificates without a working Nginx server to serve up the acme-challenge folder. Classic catch 22.

Here’s the solution: we’re going to break up the Nginx configuration into 2 separate configuration files, one for the HTTP endpoint with letsencrypt challenge files and one for the HTTPS endpoint serving the actual web application.

We’ll then place them both in the sites-available folder rather than the standard sites-enabled folder. By default, any configuration files in the sites-enabled folder are automatically parsed by Nginx when it’s restarted, however we want to control this process.

The HTTP Nginx configuration file will be located at: /etc/nginx/sites-available/http.example.conf and look like:

# HTTP server
server {
	listen      80;
	server_name www.example.com;
	location '/.well-known/acme-challenge' {
		default_type "text/plain";
		alias /var/www/letsencrypt;
	}
	location / {
		return 301 https://$server_name$request_uri;
	}
}

The HTTPS Nginx configuration file will be located at /etc/nginx/sites-available/https.example.conf and look like:

# HTTPS
server {
	listen       443;
	server_name  www.example.com;
	ssl                  on;
	ssl_certificate      /srv/letsencrypt/certs/www.example.com/fullchain.pem;
	ssl_certificate_key  /srv/letsencrypt/certs/www.example.com/privkey.pem;

	#Include actual web application configuration here.
}

Controlling Nginx

Before we do anything else, we’ll need to first stop the running Nginx service.

service nginx stop

Then we need to enable the HTTP endpoint by creating a symlink from the sites-available file to the sites-enabled folder, and starting the Nginx service

echo "Enable the http endpoint"
ln -s /etc/nginx/sites-available/http.example.conf /etc/nginx/sites-enabled/http.example.conf

echo "Starting nginx service..."
service nginx start

At this point we have a working HTTP endpoint which will correctly serve up any files in the acme-challenge folder. Lets generate some certificates.

echo "Generate Letsencrypt SSL certificates"
/srv/letsencrypt/letsencrypt.sh --cron

After the certificates are generated successfully by Letsencrypt.sh, we’ll have to enable our HTTPS endpoint, which is where all standard traffic is being redirected to.

echo "Enable the https endpoint"
ln -s /etc/nginx/sites-available/https.example.conf /etc/nginx/sites-enabled/https.example.conf

Finally, we need to tell Nginx update its configuration, as we’ve just added the HTTPS endpoint, but we want to do it without any downtime. Thankfully the Nginx developers have provided us a way to do that.

echo "Reload nginx service..."
service nginx reload

Now we have a working HTTPS enabled web application. The only thing left to do is automate the certificate renewal.

Downtime-Free Automatic Certificate Renewal

Automatically renewing your SSL certificate isn’t just a cool feature of Letsencrypt.sh, its actually almost a requirement. By default Letsencrypt certificates expire every 90 days, so renewing it manually would pretty annoying. Thankfully it only takes a single command to completely automate this process.

echo "Register Letsencrypt to run weekly"
echo "5 8 * * 7 root /srv/letsencrypt/letsencrypt.sh --cron && service nginx reload" > /etc/cron.d/letsencrypt.sh
chmod u+x  /etc/cron.d/letsencrypt.sh

That command will register a new cron task to run every week that will run the letsencrypt.sh command. If the letsencrypt.sh script detects that the certificate will expire within 30 days, the certificates will be renewed automatically, and the Nginx server will reload, without any downtime.

Fin

At this point you should have a working SSL protected web application, with automatic certificate renewal, at the cost of a handful of bash commands.

If you’re looking for an example of how this process can be used to automatically protect a website running inside a Docker container, look no further than my minimal letsencrypt-http01-docker-nginx-example repo.

If you would like to see a more real world use of Letsencrypt with Nginx and automation you should check my Gitmask repo.

Jason Kulatunga

Devops & Infrastructure guy @Gusto (ex-Adobe). I write about, and play with, all sorts of new tech. All opinions are my own.

San Francisco, CA blog.thesparktree.com

Subscribe to Sparktree

Get the latest posts delivered right to your inbox.

or subscribe via RSS with Feedly!