Hello all,first of all I would like to thank people behind Let's Encrypt for their tremendous work. Great job!I am running server on Debian Jessie (please note that following script is not Jessie-specific and should run on any Debian). I run multiple websites there (Nginx) and I wanted to completely automate certificate renewal. This post has nothing to do with Nginx, though. I know that Let's Encrypt provides some Nginx module for automatic Nginx configuration, but I don't like the idea that some third-party scripts fiddle with my precious config files. I set up all paths to SSL certificates and keys for every server manually. It has to be done only once, anyway. Once the certificate is obtained for the first time, paths to the most recent certificate and its key always remain constant (/etc/letsencrypt/live/xxx.domain.tld/cert.pem and privkey.pem), so the only thing I need to to is to renew my certificates when necessary, nothing else - Nginx configs do not need to be changed any more.

So, here I present a bash script, which checks all present certificates, determines whether they are expiring soon and renews them, if needed. If renewal failed, an alert email will be sent. On my server, I use "webroot" authentication method, so all my websites are capable of serving URIs like http://xxx.domain.tld/.well-known/acme-challenge/xxxxxxxxxx. For this purpose I have created a separate directory and a "location" directive which has root there (in my Nginx setup), see here.

Anyway, here is the script. Feel free to modify it as you want. To be able to use it, you will have to set correct paths to used files and properly configure /etc/letsencrypt/cli.ini.EMAIL_ALERT_BODY_FILE variable contains path to the simple text file, which contains body of the alert email.Feel free to leave your comments - especially if you notice any bugs.

UPDATE 05 DEC 2015 v1.1: This is an updated version 1.1. The problem with the v1.0 (which I posted here on 04 DEC 2015) was that it didn't work well with certificates issued for multiple domains. In case you had a certificate entry in your /etc/letsencrypt/live/www.domain.tld, which contained cert for both www.domain.tld and domain.tld, script would have sent a renewal request for www.domain.tld only, which would have created a new certificate (valid only for domain.tld). Current version of the script takes domain list from /etc/letsencrypt/renewal/*.conf files. You can run this script either with --renew-all option to renew all certs automatically, or with [cert_name], which means that only the certificate located in /etc/letsencrypt/live/[cert_name] will be updated. The domain list for the update will be taken from /etc/letsencrypt/renewal/[cert_name].conf file, which must exist.UPDATE 08 DEC 2015 v1.2: Typo corrections, updated documentation and small code improvements.UPDATE 25 DEC 2015 v1.3: Removed superfluous "sync" from CMD_SRV_RESTART. Thanks to allo and TCM from Let's Encrypt forums.

#!/bin/sh
###############################################################################
# This script checks one or all existing Let's Encrypt SSL certificate(s) for
# expiration, renews it/them if necessary and optionally restarts webserver
# (configurable option, see below).
# This script does NOT mess with you Nginx/Apache config files, they remain untouched
# (there is no need to modify them, since symlinks in /etc/letsencrypt/live directories
# always point to the most recent certificate).
#
# Let's Encrypt certificates are valid for 3 monts, so we should
# renew them approx. every 2 months to avoid near-expiry-date problems.
#
# USAGE:
# -h or --help : Help message
# --renew-all : Check all LE certificates and renew them if necessary.
# Before you run it with this option, check configuration
# block below to be sure that you have correct paths,
# expiration limit and email settings.
# CERT_NAME : Check specific certificate and renew it if necessary.
# Script will search for a renewal configuration file
# in /etc/letsencrypt/renewal/CERT_NAME.conf and extract
# list of domains that are included in the certificate.
# Before you run this command, check that these files exist:
# /etc/letsencrypt/renewal/CERT_NAME.conf
# /etc/letsencrypt/live/CERT_NAME/cert.pem
# If these files do not exist an error message will be shown.
#
# In case at least one certificate was renewed during script execution,
# a $CMD_SRV_RESTART command will be executed (useful for reloading webserver,
# or any other services that use LE certificates (see configuration block).
# If renewal of a certificate fails, an alert email will be sent.
#
# LE client logs all errors automatically to this file:
# /var/log/letsencrypt/letsencrypt.log
#
# Return codes: if used with --renew-all, then script always returns 0,
# because we can't determine whether renewal of individual certificates caused
# any errors. If used without --renew-all option, returns 0 if certificated
# does not need to be renewed or if certificate was successfully renewed.
# Returns 1 on errors (certificate file not found, renewal failed).
#
# Source loosely based on:
# http://eblog.damia.net/2015/12/03/lets-encrypt-automation-on-debian/
###############################################################################
#
# Update history:
#
# 03.12.2015 v1.0
# The very first version. Does not support multiple-domain certificates.
#
# 05.12.2015 v1.1
# Support for multiple-domain certificates. If used as script_name <CERT_NAME>,
# takes domain list from /etc/letsencrypt/renewal/CERT_NAME.conf file
#
# 08.12.2015 v1.2
# No bug fixing, typo corrections and small code improvements.
#
# 25.12.2015 v1.3
# Removed superfluous "sync" from CMD_SRV_RESTART. Thanks to allo and TCM from Let's Encrypt forums.
###############################################################################
SCRIPT_DESCRIPTION="Check and renew (if necessary) SSL certificate(s) from Let's Encrypt"
SCRIPT_AUTHOR="Acetylator"
SCRIPT_VERSION="1.3"
SCRIPT_DATE="25.12.2015"
SCRIPT_NAME=$(basename "$0")
###############################################################################
# This line MUST be present in all scripts executed by cron!
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# Before we start, set $CERT_NAME variable from the first parameter
CERT_NAME="$1"
###############################################################################
# CONFIGURATION START
###############################################################################
# Remaining days to expire before try renew.
# Let's Encrypt certificates expire in 90 days (on 12/2015).
# Currently, Let's Encrypt recommends renewal after 60 days.
# In future, this value may become even smaller.
# We renew our certificate when it expires in 30 days,
# which gives us plenty of time to react in case something goes wrong.
DAYS_REMAINING=30;
# Originating email, which will be used in FROM: field for email alerts.
# In case you use name, use this format: "johndoe@domain.tld (John Doe)".
EMAIL_ALERT_ADDRESS_FROM="server@domain.tld (Test Server)"
# Destination email, to which an alert email will be sent,
# in case that certificate renewal fails.
EMAIL_ALERT_ADDRESS_TO="admin@domain.tld"
# Subject of alert email. We can use $CERT_NAME variable, it is already set.
EMAIL_ALERT_SUBJ="WARNING: Let's Encrypt SSL certificate renewal for ${CERT_NAME} failed!"
# Full path to template file, containing email body.
# There is NO variable evaluation/substitution, the file will be sent as-is.
EMAIL_ALERT_BODY_FILE="/path/to/email_alert_body.txt"
# This command restarts or reloads our webserver, and
# eventually all other servers and services that depend on
# our certificates (for example, mail server, etc.)
# There is no need to use absolute paths (we have set $PATH variable at the beginning).
# To use multiple commands, use "cmd1; cmd2; cmd 3"
# There is no need to restart following services, it is sufficient to reload them.
CMD_SRV_RESTART="service nginx reload; service postfix reload; service dovecot reload"
# -----------------------------------------------------------------------------
# CONSTANTS
# Check that all paths are correct.
# Other than that, you don't need to change anything here.
# -----------------------------------------------------------------------------
# Path to letsencrypt-auto script.
# On our server, it is located in /opt/letsencrypt/letsencrypt-auto
LEBIN="/opt/letsencrypt/letsencrypt-auto"
# Config file that is Let's Encrypt should use.
# Specify it explicitly to avoid any possible confusion
LECFG="/etc/letsencrypt/cli.ini"
# Live directory, which contains .pem certificates.
# Typically it is /etc/letsencrypt/live. Do not use trailing slash.
LELIVE="/etc/letsencrypt/live"
# Renewal directory, which contains renewal configs for all certificates.
# Typically it is /etc/letsencrypt/renewal. Do not use trailing slash.
LERENEWAL="/etc/letsencrypt/renewal"
# Name of the option to check all certificates and renew them,
# if necessary.
OPT_RENEW_ALL="--renew-all"
# This option is used when we renewing all certificates and we don't
# want to restart our webserver after each renewal.
# If this option is used, no initial info will be displayed.
# This option is used only internally and is not intended to
# be used by a human user.
OPT_NO_SRV_RESTART="--no-srv-restart"
###############################################################################
# CONFIGURATION END
# Do not edit beyond this line.
###############################################################################
# -----------------------------------------------------------------------------
# This function examines .pem certificate file (passed as parameter $1) and
# returns in how many days will the certificate expire.
# Result value is placed in $DAYS_EXP global variable.
get_days_exp() {
local d1=$(date -d "`openssl x509 -in $1 -text -noout|grep "Not After"|cut -c 25-`" +%s)
local d2=$(date -d "now" +%s)
# Return result in global variable
DAYS_EXP=$(echo \( $d1 - $d2 \) / 86400 |bc)
}
# -----------------------------------------------------------------------------
# Show help if there are are no arguments specified or help is explicitly requested
if [ $# -eq 0 ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
echo "${SCRIPT_DESCRIPTION}"
echo "Author: ${SCRIPT_AUTHOR} Version: ${SCRIPT_VERSION} Last modified: ${SCRIPT_DATE}"
echo "Usage: ${SCRIPT_NAME} [CERT_NAME|--renew-all] [-h]"
echo "CERT_NAME must be an existing .conf file in ${LERENEWAL}/ directory."
echo "For example, using domain.tld as CERT_NAME will use ${LERENEWAL}/domain.tld.conf file to get domain list."
echo "Also, ${LELIVE}/live/CERT_NAME/cert.pem certificated must exist."
echo "In case of errors, check /var/log/letsencrypt/letsencrypt.log"
# If there are no parameters, display error message.
if [ $# -eq 0 ]; then
echo ""
echo "ERROR: Certificate not specified. To check and renew all certificates, use ${OPT_RENEW_ALL} option."
fi
exit 1;
fi;
# -----------------------------------------------------------------------------
# Are we called with parameter --renew-all? In this case
# call our script recursively with each certificate found in $LELIVE
# as $1 parameter.
if [ "$1" = "${OPT_RENEW_ALL}" ]; then
echo "INFO: All certificates will be now checked and renewed, if necessary."
# Set $NEED_SRV_RESTART to 0 as initial value.
NEED_SRV_RESTART=0
# Check and renew every individual certificate, running ourselves recursively.
# Server will be restarted only after we renew all certificates.
# We can use $CERT_NAME here, despite we have already defined it.
# Since we use $OPT_RENEW_ALL, we are not going to use previously set value.
for CERT_NAME in $(ls -1 "${LELIVE}"); do
$0 "${CERT_NAME}" ${OPT_NO_SRV_RESTART}
# Check last code. If it is 1, it means that certificate was renewed.
# In this case we set $NEED_SRV_RESTART to 1.
if [ "$?" -eq 1 ]; then NEED_SRV_RESTART=1; fi
done
# After we are finished, restart server, if needed
if [ "$NEED_SRV_RESTART" -eq 1 ]; then
eval $CMD_SRV_RESTART
fi
# All done, exit now. Here, we always use code 0, because we can't
# tell whether there were any errors during execution.
exit 0;
fi;
# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------
# This block checks individual certificate and renews it, if necessary
#
# Exit codes are used as following:
# * If we use $OPT_NO_SRV_RESTART, it means that we were recursively called
# from above (see $0 ....) and we use exit code to tell caller script whether
# certificate has changed (e.g. was renewed).
# - Code 1 indicates that certificate was renewed and server(s) must be restarted.
# - Code 0 indicates that no renewal was done (any reason - file not found,
# renewal not necessary, renewal failure).
# * If we do NOT use $OPT_NO_SRV_RESTART option, it is vice versa:
# - Code 0 indicates success (certificate was renewed or renewal was not needed),
# - Code 1 indicates failure (certificate file not found or renewal failed).
#
# -----------------------------------------------------------------------------
# OK, certificate is specified in the parameter,
# so set necessary variables first. $CERT_NAME is already set now.
#
# This is certificate file, located in /live directory
# We use it to check when does the certificate expire.
CERT_FILE="${LELIVE}/${CERT_NAME}/cert.pem"
# This is renewal configuration file, containing list of all domains for current $CERT_NAME
CERT_CONF="${LERENEWAL}/${CERT_NAME}.conf"
# This is not very interesting info, so let's disable it.
#echo "INFO: Processing certificate for $1, located in ${CERT_FILE}"
# Check whether certificate file (.pem) exists
if [ ! -f ${CERT_FILE} ]; then
echo "ERROR: Certificate file ${CERT_FILE} not found."
# Set exit code, see comments above
if [ "$2" = "${OPT_NO_SRV_RESTART}" ]; then exit 0; else exit 1; fi
fi
# Check whether renewal configuration file (.conf) exists
if [ ! -f ${CERT_CONF} ]; then
echo "ERROR: Renewal configuration file ${CERT_CONF} not found."
# Set exit code, see comments above
if [ "$2" = "${OPT_NO_SRV_RESTART}" ]; then exit 0; else exit 1; fi
fi
# Determine in how many days will the certificate expire.
# Result will be placed in $DAYS_EXP
get_days_exp "${CERT_FILE}"
echo -n "INFO: Certificate for ${CERT_NAME} will expire in ${DAYS_EXP} days. "
# Save $DAYS_EXP value for later use
OLD_DAYS_EXP=$DAYS_EXP
# Check if we need to renew it
if [ "$DAYS_EXP" -gt "$DAYS_REMAINING" ]; then
echo "Renewal is not necessary."
# Set exit code, see comments above
if [ "$2" = "${OPT_NO_SRV_RESTART}" ]; then
exit 0
else
exit 1
fi
else
echo "Certificate is nearing expiry date! Trying to renew..."
echo ""
# Now we need to get domain list from the $CERT_CONF file.
# It is very important that we use the same domain list, for which the original
# certificate (located in .pem file) was issued. LE saves this list into $CERT_CONF file.
# If we do not use the same domain list, LE will issue a new certificate,
# which might create an entry like domain.tld-0001 in your configuration,
# which will mess up our certificates!
# We need to read this file and get this domain list, so we can supply it to LE client.
# There is a line "domains = xxxxx" there, for example:
# domains = domain.tld, www.domain.tld
# OR
# domains = domain.tld,
#
# Note trailing comma! We have to read this value from the file, check whether
# there is a trailing comma there and remove it, if found.
#
# Read "domains = xxxx" value from the file into $DOMAINS
DOMAINS=$(grep --only-matching --perl-regex "(?<=domains \= ).*" "${CERT_CONF}")
# Determine last character
last_char=$(echo "${DOMAINS}" | awk '{print substr($0,length,1)}')
# If last character is comma, then delete it from $DOMAINS
if [ "${last_char}" = "," ]; then
DOMAINS=$(echo "${DOMAINS}" |awk '{print substr($0, 1, length-1)}')
fi
# Now $DOMAINS contains list of domains that we are going to supply to LE client.
# OK, we have prepared everything. Now try to renew certificates for $DOMAINS via LE Client.
${LEBIN} certonly --renew-by-default --config "${LECFG}" --domains "${DOMAINS}"
# After renewal, try to determine when does the new certificate expire.
# If renewal went OK, new value of $DAYS_EXP should be greater than $OLD_DAYS_EXP.
get_days_exp "${CERT_FILE}"
# Is $DAYS_EXP now less than or equal to $OLD_DAYS_EXP? If not, then renewal has failed.
# If renewal went OK, then $DAYS_EXP must be greater than $OLD_DAYS_EXP.
if [ "$DAYS_EXP" -le "$OLD_DAYS_EXP" ]; then
echo "ERROR: Certificate renewal failed. An e-mail alert was sent to ${EMAIL_ALERT_ADDRESS_TO}."
# Send alert email
cat "${EMAIL_ALERT_BODY_FILE}" | mail -aFrom:"${EMAIL_ALERT_ADDRESS_FROM}" -s "${EMAIL_ALERT_SUBJ}" ${EMAIL_ALERT_ADDRESS_TO}
# Set exit code, see comments above
if [ "$2" = "${OPT_NO_SRV_RESTART}" ]; then exit 0; else exit 1; fi;
else
echo "SUCCESS: Certificate was successfully renewed."
# After successful renewal, restart server,
# but only if option $OPT_NO_SRV_RESTART is not present
# This option is normally set if we were executed first with --renew-all parameter.
if [ "$2" = "${OPT_NO_SRV_RESTART}" ]; then
# If $OPT_NO_SRV_RESTART is present, then exit with code 1.
# This will indicate calling script that certificate was renewed and
# server(s) must be restarted
exit 1
else
echo ""
echo "INFO: Restarting server."
echo ""
eval $CMD_SRV_RESTART
# We can return 0 now, because $OPT_NO_SRV_RESTART is not present, so in this case
# 0 means success.
exit 0;
fi;
fi;
fi

Are you sure, you need the sync command? Even when something relevant is not yet written to disk, your servers will read it from cache (that's the point of caches ). Usually you only need to sync yourself, if you plan to pull the power cord in a few seconds.

allo and TCM, thank you for your responses. I have to admit that I have just blindly copied "sync" from some other source found somewhere. I wasn't sure whether it was needed at all, but I left it because I thought that it would not harm, not actually knowing too much about sync. So I remove it now. Thank you again.

###############################################################################
# CONFIGURATION FILE FOR LET'S ENCRYPT CLIENT #
# We use it to automatically and regularly generate new SSL certificates #
###############################################################################
#
# Sources:
# https://letsencrypt.readthedocs.org/en/latest/using.html#configuration-file
# https://github.com/letsencrypt/letsencrypt/blob/master/examples/cli.ini
# https://github.com/letsencrypt/letsencrypt/blob/master/examples/dev-cli.ini
#
# Help for letsencrypt client: ./letsencrypt -h all
#
# LAST CHECK/UPDATE: 12/2015
#
###############################################################################
# RSA key size. MUST be >= 2048.
# Possible values: 2048, 3072, 4096.
# Info on decision which size to use:
# http://www.keylength.com
# http://danielpocock.com/rsa-key-sizes-2048-or-4096-bits
# https://en.wikipedia.org/wiki/Key_size#Asymmetric_algorithm_key_lengths
#
# Currently, 2048 bits are fully sufficient.
# Status: OK
rsa-key-size = 2048
# Select which certificate server to use. Possible values:
# https://acme-v01.api.letsencrypt.org/directory <-- Production server (use this one)
# https://acme-staging.api.letsencrypt.org/directory <-- Staging (testing) server (do NOT use this one)
#
# You can see server list here: https://letsencrypt.status.io/
# Normally, we always use production server.
# We CAN, however, use staging server for testing purposes, but issued certificates
# will NOT be trusted - so it is really just for testing purposes.
# Status: CONST
server = https://acme-v01.api.letsencrypt.org/directory
# Email used for registration and recovery contact.
# This email is only used when creating an account with the ACME server, which is one
# of the first things the client does. Once you've had a successful run with letsencrypt and
# this account has been created, the --email flag has no effect.
# More info here: https://community.letsencrypt.org/t/clarify-the-email-flag-requirements/2603
# This email is used in the event of key loss or account compromise.
# It is also used for receiving notice about impending
# expiration of revocation of your certificates.
# In future it should be possible to change email, see here:
# https://community.letsencrypt.org/t/clarify-the-email-flag-requirements/2603/7
# Status: CHECK/MAYBE_OK
email = admin@domain.tld
# Uncomment to use a text interface instead of ncurses
# I don't like ncurses. Let's use hardcore text mode instead :-)
# Status: OK
text = True
# Uncomment to use the standalone authenticator on port 443
# We do NOT use standalone authenticator. We use webroot authenticator instead.
# Leave those line commented.
# authenticator = standalone
# standalone-supported-challenges = tls-sni-01
# Uncomment to use the webroot authenticator. Replace webroot-path with the
# path to the public_html / webroot folder being served by your web server.
# We use ONLY webroot authenticator, and we use special directory for it.
# In Nginx config, we redirect /.well-known/acme-challenge to this directory.
# To make it work, just create empty directory here:
# /var/www/specialfiles/letsencrypt/.well-known/acme-challenge/
# And make a location section in you Nginx config which will point to this directory.
authenticator = webroot
webroot-path = /var/www/specialfiles/letsencrypt
###############################################################################
# Additional parameters from here:
# https://github.com/letsencrypt/letsencrypt/blob/master/examples/dev-cli.ini
###############################################################################
# List of domains for which the certificate will be generated.
# We specify domains in the command line, so leave this empty
#domains = example.com
# Used for automation. Agree to the Let's Encrypt Subscriber Agreement.
# Default: False
# Set it to true to allow automated certificate generation/retrieval.
# Status: OK
agree-tos = True

Thanks. I was puzzled why it was needed, as I didn't have one when the certificates where created, and I "think" (it was a month ago now) once I had entered my details re email address etc it didn't ask again as I created more certificates

I am using cli.ini to reduce number of parameters that I pass to letsencrypt script, so I have put there parameters which are constant and should never change. If I understand this correctly, using cli.ini is optional.

Nice! Little remark concerning restarting webserver - first, there is no need to call "service nginx restart". "service nginx reload" is fully sufficient. Second (this is the reason why I use generic CMD_SRV_RESTART with a possibility of multiple commands in my script) - there maybe other services that depend on the SSL certificate which should be reloaded (for example, in my script, it is Postfix and Dovecot). My approach is not very elegant, though, since I don't check return code of the restart command. Normally, it should not be a problem, however it would be better to check it to make the whole operation completely fail-safe.

thanks for the feedback. When I did try reload on my web server and did check which certificate my browser was getting immediately afterwards, it did indicate it was using the old certificate. I do suspect that the cause leis in the nature of the implementation of "reload" with nginx (and apache is the same to my knowledge). Issuing a restart it kills all old worker tasks/threads/processes that are not in use any more and replaces them with a new instance running the new configuration. In my case I already had the page loaded in my browser prior to reloading. So I did have a web server thread/... running for my session and it I was served using the old configuration by just reloading the browser window. I should check this out and comment in my documentation.

Yoru are perfectly right that my script is only handling one service properly by design. To inplement it the right way, you would need to link each certificate to one or more restart commands. I did not have the need for that, yet. But patches are welcome, as always. I could think of "hiding" this information in the certificate config file as some "magic comments" like

Maybe it's just me, but I think the newer version of the lets encrypt client (I have 0.5.0) is doing something to the .conf files in /etc/letsencrypt/renewal/ so that the domains = domain.tld, sub.domain.tld line is no longer there for this script to read.

The line used to be there, but after updating the client (as part of a certificate renewal) the line is gone and a subsequent try to run the script results in the renewal failing, due to an empty list of domains.

If I use the lets encrypt client directly to renew a certificate (where it just renews everything), it does create certificates with all domains in them without me specifying the domain list, so I guess it checks the existing certificates and uses the Subject Alternative Name for the renewal.

So what I have done is to change the line in the script that gets the domain list: