Skip to content

Free SSL Certificate from Let’s Encrypt for Nginx

See my previous post in the unmanaged VPS (virtual private server) series, Automate Remote Backup of WordPress Database, on how to create and schedule a Windows batch script to backup the WordPress database.

Update: Let’s Encrypt has made changes that have broken my pre-existing certificate renewal. First, the new certbot package has replaced the original letsencrypt package. Second, the certbot package does not recognize the pre-existing certificates in the “/etc/letsencrypt” directory (generated by the old letsencrypt package). If you have the old letsencrypt package, I recommend deleting it, the “~/.local” directory, and the “/etc/letsencrypt” directory before installing the new certbot package. I’ve updated the instructions below to use the new certbot package.

I’ve been meaning to enable HTTPS/SSL access to this WordPress site since I heard that Google had started giving ranking boosts to secure HTTPS/SSL websites; however, the thing stopping me was the expensive and yearly cost of a SSL certificate. (Unfortunately, self-signed SSL certificates wouldn’t work because browsers would throw security warnings when encountering them.) But now, there is a new certificate authority, Let’s Encrypt, which provides free SSL certificates.

The only catch is that the SSL certificates would expire in 90 days. But that’s okay because Let’s Encrypt provides a command line client which can create and renew the certificates. Some elbow grease and a weekly Cron job should automatically renew any expiring SSL certificates.

Note: StartSSL provides a free, non-commercial SSL certificate which can be manually renewed after a year. I learned about it at the same time as Let’s Encrypt, but decided to go with Let’s Encrypt because of the possibility of automation and no restrictions on usage.

Below are the steps I took to create and install the SSL certificates on the Nginx server running on my unmanaged DigitalOcean VPS.

Create SSL Certificate

Ubuntu didn’t have the certbot package, so we will need to build it from source. Secure shell into your VPS server and run these commands:

# Install Git version control (alternative to Subversion)
sudo apt-get install git

# Double-check that Git is installed by getting version
git --version

# Download the certbot client source code (to the home directory)
cd
git clone https://github.com/certbot/certbot

# Install dependencies, update client code, build it, and run it using sudo
cd certbot
./certbot-auto --help
# Input the sudo password if requested to

# Get a SSL certificate for mydomain.com
./certbot-auto certonly --webroot -w /var/www/wordpress -d mydomain.com -d www.mydomain.com
# Input your email address (for urgent notices and lost key recovery)

# Get another SSL certificate for mydomain2.com
./certbot-auto certonly --webroot -w /var/www/mydomain2 -d mydomain2.com -d www.mydomain2.com

Note: The Let’s Encrypt Ubuntu Nginx install instructions suggest using the wget command to get the latest available certbot version. I think “git clone” is a better method because it provides a more powerful way to update the certbot package, as we will see later.

Running the “certbot-auto” script will do the following:

  1. Install any missing dependencies including the GCC compiler and a ton of libraries.
  2. Update the certbot client source code.
  3. If necessary, build or update the certbot client, located at “~/.local/share/letsencrypt/bin/letsencrypt”. (The name switch from letsencrypt to certbot is not complete and thus a little confusing.)
  4. Run the certbot client using sudo; thus, you may be prompted to input the sudo password.

Note: If you want to speed it up by avoiding the extra update steps, you can just run the “sudo ~/.local/share/letsencrypt/bin/letsencrypt” command directly, instead of the “~/certbot/certbot-auto” script.

When running the “certbot-auto certonly –webroot” certificate generation option, the following (with some guesses on my part) occurs:

  1. The certbot client will create a challenge response file under the domain’s root directory (indicated by the “-w /var/www/wordpress” parameter); for example, “/var/www/wordpress/.well-known/acme-challenge/Y8a_KDalabGwur3bJaLfznDr5vYyJQChmQDbVxl-1ro”. (The sudo access is required to write to the domain’s root web directory.)
  2. The certbot client will then call the letsencrypt.org ACME server, passing in necessary credential request info such as the domain name (indicated by the “-d mydomain.com -d www.mydomain.com” parameters).
  3. The letsencrypt.org ACME server will attempt to get the challenge response file; for example, by browsing to “http://mydomain.com/.well-known/acme-challenge/Y8a_KDalabGwur3bJaLfznDr5vYyJQChmQDbVxl-1ro”. This verifies that the domain has valid DNS records and that you have control of the domain.
  4. The letsencrypt.org ACME server passes the generated SSL certificate back to the certbot client.
  5. The certbot client writes the SSL certificate to the “/etc/letsencrypt” directory, including the private key. If you can only backup one thing, it should be the contents of this directory.
  6. The certbot client deletes the contents of the “.well-known” directory; for example, leaving an empty “/var/www/wordpress/.well-known” directory once done. (The latest version appears to delete the “.well-known” directory. If the “.well-known” directory is still present, you can manually it.)

Note: It is possible to create a multi-domain certificate containing more than one domain, but I recommend keeping it simple. Multi-domain certificates are bigger to download and may be confusing to the user when viewed.

Configure Nginx

The directory where the SSL certificates are located under, “/etc/letsencrypt/live”, require root user access, so we will need to copy the certificate files to a directory which Nginx can read.

# Copy out the SSL certificate files
sudo cp /etc/letsencrypt/live/mydomain.com/fullchain.pem /etc/nginx/ssl/mydomain-fullchain.pem
sudo cp /etc/letsencrypt/live/mydomain.com/privkey.pem /etc/nginx/ssl/mydomain-privkey.pem
sudo cp /etc/letsencrypt/live/mydomain2.com/fullchain.pem /etc/nginx/ssl/mydomain2-fullchain.pem
sudo cp /etc/letsencrypt/live/mydomain2.com/privkey.pem /etc/nginx/ssl/mydomain2-privkey.pem

# Double-check that the files exist and are readable by group and others
ls -l /etc/nginx/ssl

Because our website will behave the same under HTTPS as under HTTP, we will only need to make minimal changes to the existing HTTP server configuration.

Edit Nginx’s server block file for mydomain (“sudo nano /etc/nginx/sites-available/wordpress”) and add the “#Support both HTTP and HTTPS” block to the “server” section:

server {
        #listen   80; ## listen for ipv4; this line is default and implied
        #listen   [::]:80 default ipv6only=on; ## listen for ipv6

        #Support both HTTP and HTTPS
        listen 80;
        listen 443 ssl;
        ssl_certificate /etc/nginx/ssl/mydomain-fullchain.pem;
        ssl_certificate_key /etc/nginx/ssl/mydomain-privkey.pem;

        root /var/www/wordpress;
        #index index.php index.html index.htm;
        index index.php;

        # Make site accessible from http://localhost/
        #server_name localhost;
        server_name mydomain.com www.mydomain.com;

If you wish to have this server block be the default to serve (for both HTTP and HTTPS) when your server is accessed by IP address, change the “listen” directives to the following:

server {
        #Support both HTTP and HTTPS
        listen 80 default_server; # default_server replaces older default
        listen 443 ssl default_server;
}

Note: Make sure that only one of your server block files is set to be the default for IP address access. Also, when you browse to the IP address using HTTPS, you will still get a security warning because the IP address won’t match the domain name.

Repeat the above modifications for any other domain’s server block file.

Note: If you want HTTPS to behave differently than HTTP, leave the HTTP server section alone, uncomment the “# HTTPS server” section (at bottom of the server block file) and make your updates there.

Once you are done updating the server block files, tell Nginx to reload its configuration:

# Reload Nginx service
sudo service nginx reload

# If Nginx throws an error, look at the error log for clues
sudo tail /var/log/nginx/error.log

To test, browse to your server using the HTTPS protocol; for example, “https://mydomain.com/”.

Renew SSL Certificate

To renew all your SSL certificates, run this command 30 days before the expiration date:

~/certbot/certbot-auto renew

Note: If you run it earlier than 30 days before the expiration date, no action will be taken.

Cron Job To Renew

To automate the SSL certificate renewal, we will use a Cron job that runs weekly under the root user. Running the job weekly is sufficient to guarantee a certificate renewal within the 30 days before expiration window.

First, create a script by running “nano ~/certbot_cron.sh” and inputting the content below (make sure to replace “mynewuser” with your actual username):

#!/bin/bash

# Run this script from root user's crontab

# Log file
LOGFILE="/tmp/certbot-renew.log"

# Print the current time
echo $(date)

# Try to renew certificates and capture the output
#/home/mynewuser/certbot/certbot-auto renew --no-self-upgrade > $LOGFILE 2>&1
/home/mynewuser/certbot/certbot-auto renew > $LOGFILE 2>&1

# Check if any certs were renewed
if grep -q 'The following certs have been renewed:' $LOGFILE; then
  # Copy SSL certs for Nginx usage
  cp /etc/letsencrypt/live/mydomain.com/fullchain.pem /etc/nginx/ssl/mydomain-fullchain.pem
  cp /etc/letsencrypt/live/mydomain.com/privkey.pem /etc/nginx/ssl/mydomain-privkey.pem
  cp /etc/letsencrypt/live/mydomain2.com/fullchain.pem /etc/nginx/ssl/mydomain2-fullchain.pem
  cp /etc/letsencrypt/live/mydomain2.com/privkey.pem /etc/nginx/ssl/mydomain2-privkey.pem

  # Reload Nginx configuration
  service nginx reload
fi

Notes about the script:

  • To test the script, run this command:
    sudo sh ~/certbot_cron.sh

    If none of your certificates are renewed, you won’t get a “Reloading nginx configuration” message (outputted by the “service nginx reload” command).

  • The “–no-self-upgrade” argument flag can be passed to certbot to prevent certbot from upgrading itself. At first, because we will be running the script under the root user, I hesitated to allow certbot to update with root permissions automatically. Avoiding an update seemed more secure and definitely faster to execute. However, without the update, by the time the three months expired, the certbot is hopelessly outdated and won’t successfully renew certificates. So, I had to allow certbot to automatic self-upgrade with root privileges to avoid having to do manual updates.
  • To simulate certificate renewals, use the “–dry-run” argument flag to simulate a successful renewal. Change the “certbot-auto” command in the script to the following:
    /home/mynewuser/certbot/certbox-auto renew --dry-run > $LOGFILE

    When you re-run the “certbot_cron.sh”, you will get the “Reloading nginx configuration” message. Don’t forget to remove this change from the script once you are done testing.

  • The script copies out all the SSL certificates, instead of checking for and only copying certificates which have been modified. I don’t think the effort to do the latter is worth it.

Update: The latest certbot script appears to have bug where it says that the certificates have not been renewed, even though they may have been. As a result, I suggest commenting out the check and forcing the copying of certificates every time. (Strangely, running certbot with the –dry-run flag does output the “certs have been renewed” message.).

...
# Check if any certs were renewed
#if grep -q 'The following certs have been renewed:' $LOGFILE; then
  # Copy SSL certs for Nginx usage
  ...
#fi

Add the script to the root user’s Crontab (Cron table) by running these commands:

# Edit the root user's crontab
sudo crontab -e
  # Insert this line at the end of the file
  @weekly sh /home/mynewuser/certbot_cron.sh > /tmp/certbot-cron.log 2>&1

# List content of root user's Crontab
sudo crontab -l

# Find out when @weekly will run; look for cron.weekly entry
cat /etc/crontab

Note: Instead of “@weekly”, you may wish to set a specific time that works best for your situation. Refer to these Cron examples for info on how to set the time format.

If you want to test the Cron job, do the following:

# Delete the script's output log file
sudo rm /tmp/certbot-renew.log

# Change "@weekly" to "@hourly" in the Crontab
sudo crontab -e
  # Edit this line at the end of the file
  @hourly sh /home/mynewuser/certbot_cron.sh > /tmp/certbot-cron.log 2>&1

# Wait more than an hour

# Check if output log files were generated
cat /tmp/certbot-renew.log
cat /tmp/certbot-cron.log

# Change "@hourly" back to "@weekly" in the Crontab
sudo crontab -e
  # Edit this line at the end of the file
  @weekly sh /home/mynewuser/certbot_cron.sh > /tmp/certbot-cron.log 2>&1

Update Certbot

If you have chosen to disable the certbot self update in the cron script (using the “–no-self-upgrade” argument flag), I recommend manually running the “certbot-auto” command (without any arguments) once a month to make sure that certbot is up-to-date.

If you find that the “certbot-auto” command is unable to self-update or doing the self-update doesn’t solve an issue, you can try to update using the “git pull” command.

# Update the certbot source code using git
cd certbot
git pull

# See status of certbot source code and version
git status

Strangely, the “git pull” command may tell me that the “certbot-auto” file has local changes. I had to discard the local changes so that git could update “certbot-auto” to the latest version. If you get this issue, do the following:

# Update returns error about local changes to certbot-auto file below:
git pull
#error: Your local changes to the following files would be overwritten by merge:
#        certbot-auto
#Please, commit your changes or stash them before you can merge.
#Aborting

# Discard changes to certbot-auto by checking out the latest version of the file
git checkout -- certbot-auto

# This update should go successfully through without aborting.
git pull

# See status of certbot source code and version
git status
#On branch master
#Your branch is up-to-date with 'origin/master'.
#
#nothing to commit, working directory clean

Backup SSL Certs & Keys

Note: Re-issuing the SSL certificates (because of the switch from the letsencrypt to the certbot package) proved to be painless and fast. Thus, I’ve realized that backing up the “/etc/letsencrypt” directory is not necessary. If something goes wrong, just re-issue the SSL certificates.

In Automate Remote Backup of WordPress Database, we created a Windows batch file to download MySQL dump files and other files from the server. Let us add an additional command to that Windows batch script to download a zip archive of the “/etc/letsencrypt” directory.

Originally, I added an ssh command to the Windows batch file to zip up the “/etc/letsencrypt” directory. Unfortunately, accessing that directory requires sudo privileges which causes the script to prompt for the sudo password. I looked at two solutions to running sudo over SSH without interruption. The first involved just echo’ing the sudo password (in plaintext) to the ssh command. The second involved updating the sudoers file to allow running a particular file without requiring the password. I didn’t actually test the two solutions, but they didn’t look secure so I decided go with a very simple solution: run the zip command in the “~/certbot_cron.sh” script.

First, edit the Cron script (“nano ~/certbot_cron.sh”) and add the tar zip command after reloading the Nginx server:

# Check if any certs were renewed
if grep -q 'The following certs have been renewed:' $LOGFILE; then
  ...

  # Reload Nginx configuration
  service nginx reload

  # Zip up the /etc/letsencrypt directory
  tar -zcvf /tmp/letsencrypt.tar.gz /etc/letsencrypt
fi

Note: We are using the tar command, instead of gzip, because gzip doesn’t handle the symbolic links under the “/etc/letsencrypt/live” directory correctly.

There is a security issue because “/tmp/letsencrypt.tar.gz” is readable by others; if this is a concern, you can adjust access permissions by adding the following commands to the “~/letsencrypt_cron.sh” script:

  ...

  # Zip up the /etc/letsencrypt directory
  tar -zcvf /tmp/letsencrypt.tar.gz /etc/letsencrypt

  # Change owner and restrict access to owner
  chown mynewuser /tmp/letsencrypt.tar.gz
  chmod 600 /tmp/letsencrypt.tar.gz
fi

Second, edit the Windows batch script file “C:\home\myuser\backups\backup_wordpress.bat” and add the following to the end:

REM Download the /etc/letsencrypt tar file

mkdir \home\myuser\backups\letsencrypt
cd \home\myuser\backups\letsencrypt
rsync.exe -vrt --progress -e "ssh -p 3333 -l mynewuser -v" mydomain.com:/tmp/letsencrypt.tar.gz %date:~10,4%.%date:~4,2%.%date:~7,2%-letsencrypt.tar.gz

And we are done with the backup. In the future, if you ever need to restore the contents of “/etc/letsencrypt”, upload the tar archive to the server’s “tmp” directory and run the following on the server:

# Unzip the tar file
cd /tmp
tar -xvzf letsencrypt.tar.gz
# Will uncompress everything to /tmp/etc/letsencrypt

# Copy the contents to its original location
sudo cp -r /tmp/etc/letsencrypt /etc/

Redirect HTTPS to HTTP

If you have a domain which you don’t wish to provide HTTPS access to (i.e., go through the trouble of creating a SSL certificate for), you can configure Nginx to redirect HTTPS requests to HTTP. Uncomment the “# HTTPS server” section in the domain’s server block file and add a redirect statement:

# HTTPS server
#
server {
        listen 443 ssl;
        server_name monieer.com www.monieer.com;

        #To redirect, use return instead of rewrite (no longer recommended):
        #rewrite ^(.*) http://$host$request_uri;
        return 302 http://$host$request_uri;
        #Use 302 for temporary redirect vs 301 for permanent redirect.
}

Because we did not set the “ssl_certificate” and ssl_certificate_Key” values in the server_block above, Nginx will use the default_server’s SSL certificate instead. Unfortunately, the browser will show a security warning because the domain name in the default_server’s SSL certificate won’t match the requested domain name. If the user agrees to proceed, the redirection to non-secure HTTP access would correctly take place.

Info above derived from:

Leave a Reply

Your email address will not be published. Required fields are marked *