I’ve been using Let’s Encrypt SSL certificates for well over 2 years now. Of course, Let’s Encrypt SSL certificates are short-lived so need to be renewed every 3 months or actually well before the 3-month expiry arrives. This is why we check daily for any certificates needing renewal and automatically renew them.

For obtaining and renewing certificates I have always used the certbot-auto client coupled with a very simple bash script run from cron every night. This has worked great (flawlessly) for all this time.

Sadly, some 2 weeks ago renewals failed one evening across 6 different servers all at the same time. This was caused by certbot-auto running bootstrap dependencies and updating itself before it continued with renewals.

Luckily I monitor my renewals daily and always get notified by email so seeing this across 6 servers was troubling indeed. I personally do not like certbot trying to upgrade components on my system and in this case it tried to do so and also failed for some unexplainable reason, leaving itself in a broken condition that required me to have to manually fix it.

I don’t like fixing things and I simply do not have things that break like this on any of my servers so for me it’s often a one strike and you’re out policy. My previous daily renewal script was a mere 5 lines of bash which has, as I mentioned, run flawlessly for 2 years now.

So I set off to write a much more solid, portable and fool proof way to run certbot renewals on any server using Mini(Conda), Bash and Cron.

This still uses certbot but NOT certbot-auto NOR the one recommended to be used or the one most people use. This uses the certbot and certbot plugins from the Python (pip) PyPi repositories.

My script creates a solid, self contained and stable Python environment within Conda which is self installing, self updating and literally unbreakable.

This bash script below can be run on pretty much any distribution on Linux, it should work just fine.

The script takes care of installing Conda, creating the Conda Python Environment, installing certbot, its dependencies and certbot plugins from PyPi and then performing the renewals.

This simple script is Copyright to me but you are free to use it. If you are going to use it commercially consider being kind enough to buy me some Ko-Fi.

Requirements:

  • any command line mailer (for sending emails) – script automatically finds a mailer binary.

Simply save the script (below) in your home folder as certbot-renewals.sh. (Script at bottom of this post)

Make the script executable

sudo chmod +x certbot-renewals.sh

Make sure your user has the ability to run sudo commands from CRON. To do this you need to specify the script path and name for your username inside visudo. BE VERY CAREFUL editing visudo, do not make typos and mess things up or you could break sudo. PLEASE I take not responsibility if you do not know what you are doing with visudo.

An example of the visudo entry on its own line.

myusername ALL=(ALL) NOPASSWD: /home/myusername/certbot-renewals.sh

Then add an entry to cron. This example below will run every night at 8pm

0 20 * * * sudo /home/myusername/certbot-renewals.sh

Test running the script manually once

sudo ./certbot-renewals.sh

Here it is finally, the script. Enjoy 😉 Make sure to change the email address, email subject, renewhook and specify any additional certbot DNS plugins you use. You don’t need to change anything else in the script but feel free to and if you do, please share your changes with me at the public GIST. The Raw script is available here.

#!/bin/bash
# -----------------------------------------------------------
# Mini(Conda) Environment for Failproof Certbot Renewals
# Created by Mitchell Krog: https://github.com/mitchellkrogza
# Copyright Mitchell Krog: https://github.com/mitchellkrogza
# GIST: https://gist.github.com/mitchellkrogza/547a850a34d022009e5d80e896684eac
# Last Updated: 2019-10-01 11:11:00 SAST
# -----------------------------------------------------------

# Save as certbot-renewals.sh 
# Execute with sudo | sudo ./certbot-renewals.sh
# Uninstall any other certbot installed through apt, yum or other method
# Run through cron and make sure your user is allowed to run sudo commands from cron (visudo)
# Uses any command line mailer on your syste, the script will verify a command line mailer exists.

# -------------
# User Settings
# -------------

recipient="me@myownemail.com"
emailsubject="CERTBOT Renewals - Server 1"
pythonversion="3.7.4"
environmentname="certbot-conda"
renewallogfile="/var/log/certbot-renew.log"
renewhook="nginx -s reload"

# SELECT DNS PLUGINS TO INSTALL
# --------------------------------
# 1 = INSTALL / 0 = DO NOT INSTALL
# --------------------------------

cloudflare_dns=1
cloudxns_dns=0
digitalocean_dns=0
dnsimple_dns=0
dnsmadeeasy_dns=0
google_dns=0
linode_dns=0
luadns_dns=0
nsone_dns=0
ovh_dns=0
rfc2136_dns=1
route53_dns=0

# -----------------
# End User Settings
# -----------------

# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
# Do Not Modify Anything Below This Line
# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

# ------------------------
# Set Terminal Font Colors
# ------------------------

bold=$(tput bold)
red=$(tput setaf 1)
green=$(tput setaf 2)
yellow=$(tput setaf 3)
blue=$(tput setaf 4)
magenta=$(tput setaf 5)
cyan=$(tput setaf 6)
white=$(tput setaf 7)
defaultcolor=$(tput setaf default)

# -----------
# DNS Plugins
# -----------

cloudflare="certbot-dns-cloudflare"
cloudxns="certbot-dns-cloudxns"
digitalocean="certbot-dns-digitalocean"
dnsimple="certbot-dns-dnsimple"
dnsmadeeasy="certbot-dns-dnsmadeeasy"
google="certbot-dns-google"
linode="certbot-dns-linode"
luadns="certbot-dns-luadns"
nsone="certbot-dns-nsone"
ovh="certbot-dns-ovh"
rfc2136="certbot-dns-rfc2136"
route53="certbot-dns-route53"

# ------------------
# DECLARE CONDA PATH
# ------------------

export PATH="${HOME}/miniconda/bin:${PATH}"

# ---------------------------------------------------
# Check Mini(Conda) is Installed Otherwise Install It
# ---------------------------------------------------

checkforconda () {
if conda 2>&1 | grep -i 'command not found'; then
   echo "${bold}${red}CONDA NOT FOUND - ${bold}${green}Installing Mini(Conda)"
   export PATH="${HOME}/miniconda/bin:${PATH}"
   wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh
   bash miniconda.sh -b -p ${HOME}/miniconda
   hash -r
   conda config --set always_yes yes --set changeps1 no
   conda update -q conda
   sudo rm miniconda.sh
   echo "${bold}${green}CONDA INSTALLED - Continuing"
else
   echo "${bold}${green}CONDA FOUND - Continuing"
fi
}
checkforconda

# -----------------------------------------
# Find which command line mailer we can use
# -----------------------------------------

find_mail_binary() {
	local x= path= binary=$1 bin_paths='/bin /usr/bin /usr/local/bin /usr/sbin /usr/local/sbin /root/bin /root/.bin'

	for x in $bin_paths; do
		path="$x/$binary"

		if [ -x $path ]; then
			echo $path
			return
		fi
	done
}

# -------------------------------
# Set Conda Path and Update Conda
# -------------------------------

printf '\n\n%s\n\n' "${bold}${magenta}Updating Conda"
conda update -q conda

# -------------------------------------------------
# Make sure we always run the latest Python version
# -------------------------------------------------

conda update python

# -----------------------------------------------------------
# Check for Existing Environment otherwise Create Environment
# -----------------------------------------------------------

DIR="${HOME}/miniconda/envs/${environmentname}"
if [ -d "${DIR}" ]; then
	printf '\n%s\n%s\n\n' "${bold}${cyan}Environment ${DIR} Found" "Continuing with Renewals"
else
	printf '\n%s\n%s\n\n' "${bold}${red}Environment ${DIR} Not Found" "${bold}${yellow}Creating Environment"
    conda create -q -n ${environmentname} python="${pythonversion}"
fi

# --------------------
# Activate Environment
# --------------------

printf '\n%s\n\n' "${bold}${magenta}Activating Environment"
source activate ${environmentname}

# ---------------------------------
# Upgrade / Install Certbot and Pip
# ---------------------------------

printf '\n%s\n\n' "${bold}${magenta}Upgrading PIP"
pip install --upgrade pip
printf '\n%s\n\n' "${bold}${magenta}Installing / Upgrading Certbot"
pip install certbot --upgrade

# ---------------------------
# Install Certbot DNS Plugins
# ---------------------------

if [ ${cloudflare_dns} -eq 1 ]
then
   echo "${bold}${magenta}Installing Certbot Cloudflare DNS Plugin"
   pip install ${cloudflare} --upgrade
else
:
fi

if [ ${cloudxns_dns} -eq 1 ]
then
   echo "${bold}${magenta}Installing Certbot CloudXNS DNS Plugin"
   pip install ${cloudxns} --upgrade
else
:
fi

if [ ${digitalocean_dns} -eq 1 ]
then
   echo "${bold}${magenta}Installing Certbot DigitalOcean DNS Plugin"
   pip install ${digitalocean} --upgrade
else
:
fi

if [ ${dnsimple_dns} -eq 1 ]
then
   echo "${bold}${magenta}Installing Certbot DNSSimple DNS Plugin"
   pip install ${dnsimple} --upgrade
else
:
fi

if [ ${dnsmadeeasy_dns} -eq 1 ]
then
   echo "${bold}${magenta}Installing Certbot DNS Made Easy DNS Plugin"
   pip install ${dnsmadeeasy} --upgrade
else
:
fi

if [ ${google_dns} -eq 1 ]
then
   echo "${bold}${magenta}Installing Certbot Google DNS Plugin"
   pip install ${google} --upgrade
else
:
fi

if [ ${linode_dns} -eq 1 ]
then
   echo "${bold}${magenta}Installing Certbot Linode DNS Plugin"
   pip install ${linode} --upgrade
else
:
fi

if [ ${luadns_dns} -eq 1 ]
then
   echo "${bold}${magenta}Installing Certbot LUA DNS Plugin"
   pip install ${luadns} --upgrade
else
:
fi

if [ ${nsone_dns} -eq 1 ]
then
   echo "${bold}${magenta}Installing Certbot NSOne DNS Plugin"
   pip install ${nsone} --upgrade
else
:
fi

if [ ${ovh_dns} -eq 1 ]
then
   echo "${bold}${magenta}Installing Certbot OVH DNS Plugin"
   pip install ${ovh} --upgrade
else
:
fi

if [ ${rfc2136_dns} -eq 1 ]
then
   echo "${bold}${magenta}Installing Certbot RFC2136 BIND DNS Plugin"
   pip install ${rfc2136} --upgrade
else
:
fi

if [ ${route53_dns} -eq 1 ]
then
   echo "${bold}${magenta}Installing Certbot Route53 DNS Plugin"
   pip install ${route53} --upgrade
else
:
fi

# ------------------------------------
# Show Python and Certbot Version Info
# ------------------------------------

python -VV
certbot --version

# ------------------------------------------------------
# Capture Plugin Info and Append this to the renewal log
# ------------------------------------------------------

versionoutput=$(certbot plugins)

# ----------------------------------------------------------------------------
# Run our Certbot Renew Command and Send the Output to our variable "cboutput"
# ----------------------------------------------------------------------------

certbotoutput=$(certbot renew --renew-hook "${renewhook}")

# --------------------------------------------------------------------------
# If The Command Returned Output Then Send a Mail and Append to our Log File
# --------------------------------------------------------------------------

if [[ ! -z "${certbotoutput}" ]]; then
    if [ -n $(find_mail_binary mail) ]; then
        echo "${certbotoutput} ${versionoutput}" | mail -s "${emailsubject}" ${recipient}
        echo "${certbotoutput}" >> ${renewallogfile}
    else
        echo "${bold}${red}WARN: missing mail command"
    fi
    else
        echo "${bold}${yellow}INFO: no output was received from certbot"
fi


# ----------------------
# Deactivate Environment
# ----------------------

conda deactivate

# ---------------
# Exit with Error
# ---------------

exit ${?}

Leave a Reply

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

This site uses Akismet to reduce spam. Learn how your comment data is processed.