Skip to content

Instantly share code, notes, and snippets.

@ksubileau
Last active May 21, 2016 15:29
Show Gist options
  • Save ksubileau/e4568738117ce1afbe57ab456ac2223c to your computer and use it in GitHub Desktop.
Save ksubileau/e4568738117ce1afbe57ab456ac2223c to your computer and use it in GitHub Desktop.
Yet another script to renew Let's Encrypt certificates using acme-tiny.
#!/bin/bash
# Renew Let's Encrypt SSL certificates using acme-tiny
# Version 1.1
#
# Copyright (C) 2016 Kévin Subileau <www.kevinsubileau.fr>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, version 3 of the License only.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# See <http://www.gnu.org/licenses/> to receive a full-text-copy of
# the GNU General Public License.
#########################################
# DEFAULT CONFIGURATION #
# Use le-renew.conf to customize it #
#########################################
# Let's encrypt base directory
LETSENCRYPT_HOME="/etc/letsencrypt/"
# The remaining life on our certificate in days below which we should renew.
RENEW=30
# Path to the log file
LOGFILE='/var/log/letsencrypt/letsencrypt.log'
# Path to python binary
PYTHON="python"
# CA certificates store
SSL_CERT_FILE=""
# Permissions to assign on generated certificate files
CERTS_PERM="644"
# Enable or disable Apache automatic reloading after issuing a new certificate
RELOAD_APACHE=true
# Path to an optional script to run after issuing a new certificate
# Leave empty if none
POST_RENEW_SCRIPT=""
# Default echo and log levels
# From 0 (quiet) to 5 (debug)
ECHO_LEVEL=0
LOG_LEVEL=4
# Enable or disable colored messages
ENABLE_COLORS=true
# Load config file if it's readable
CONF_FILE="$( dirname "${BASH_SOURCE[0]}" )/le-renew.conf"
if [ -r "$CONF_FILE" ]; then
. $CONF_FILE
fi
###############################################################################
# Output a colorized message
###############################################################################
colorize () {
BLUE="\033[34;01m"
GREEN="\033[32;01m"
YELLOW="\033[33;01m"
PURPLE="\033[35;01m"
RED="\033[31;01m"
OFF="\033[0m"
CYAN="\033[36;01m"
COLORS=($BLUE $GREEN $YELLOW $RED $PURPLE $CYAN)
if $ENABLE_COLORS; then
local typestr=`echo "$@" | sed 's/\(^[^:]*\).*$/\1/'`
[ "$typestr" == "DEBUG" ] && type=0
[ "$typestr" == "INFO" ] && type=1
[ "$typestr" == "WARNING" ] && type=2
[ "$typestr" == "ERROR" ] && type=3
[ "$typestr" == "FATAL" ] && type=4
[ "$typestr" == "HALT" ] && type=5
color=${COLORS[$type]}
endcolor=$OFF
echo -e "$color$@$endcolor"
else
echo -e "$@"
fi
}
###############################################################################
# Print a message to screen and log file depending on the echo and log levels
#
# First variable passed is the error level, all others are printed
###############################################################################
printmsg() {
[ ${#@} -gt 1 ] || return
type=$1
shift
if [ $type == 100 ]; then
typestr=`echo "$@" | sed 's/\(^[^:]*\).*$/\1/'`
[ "$typestr" == "DEBUG" ] && type=0
[ "$typestr" == "INFO" ] && type=1
[ "$typestr" == "WARNING" ] && type=2
[ "$typestr" == "ERROR" ] && type=3
[ "$typestr" == "FATAL" ] && type=4
[ "$typestr" == "HALT" ] && type=5
typestr=""
else
types=(DEBUG INFO WARNING ERROR FATAL HALT)
typestr="${types[$type]}: "
fi
print=$[4-type]
if [ $print -lt $ECHO_LEVEL ]; then
colorize "$typestr$@" >&2
fi
if [ $print -lt $LOG_LEVEL ]; then
logmsg "$typestr$@"
fi
}
###############################################################################
# Write a message in the log file
###############################################################################
logmsg() {
[ ! -f "$LOGFILE" ] && touch "$LOGFILE"
if [ -w "$LOGFILE" ]; then
echo -e `LC_ALL=C date "+%b %d %H:%M:%S"` "$@" >> "$LOGFILE"
fi
}
###############################################################################
# Logging shortcuts
###############################################################################
passthru() { printmsg 100 "$@"; }
debug() { printmsg 0 "$@"; }
info() { printmsg 1 "$@"; }
warning() { printmsg 2 "$@"; }
error() { printmsg 3 "$@"; }
fatal() { printmsg 4 "$@"; exit 2; }
halt() { printmsg 5 "$@"; exit 2; }
###############################################################################
# Get the expiration date of a certificate
# Arguments :
# - $1 : Path to the certificate
# - $2 (optional) : Format to use to output the expiration date
# (default is timestamp)
###############################################################################
get_expiration_date() {
if [[ -z "$1" ]]; then
fatal "No certificate file given to get_expiration_date function."
fi
if [[ ! -r "$1" ]]; then
fatal "Certificate file does not exists or is not readable."
fi
dateformat=${2:-"%s"}
# Get the expiration date of our certificate.
enddate=$(openssl x509 -in $1 -noout -enddate 2>/dev/null)
ret=$?
if [ "$ret" -ne 0 ]; then
exit $ret
fi
# Trim the unecessary text at the start of the string.
enddate=${enddate:9}
# Convert the expiration date to seconds since epoch.
LC_ALL=C date --date="$enddate" "+$dateformat"
}
###############################################################################
# Reload Apache web server using sudo.
###############################################################################
reload_apache() {
sudo apache2ctl graceful &> /dev/null
}
###############################################################################
# Renew certificate if necessary (main entry point).
###############################################################################
renew() {
# Default settings
FORCE=false
# Remove trailing slash if any
LETSENCRYPT_HOME=${LETSENCRYPT_HOME%/}
# Log process start. This log line can't be disabled
logmsg "INFO: Let's Encrypt certificate renewal process started."
# Read parameters
while [ $# -gt 0 ]; do
if [ "$1" == "--force" ] || [ "$1" == "-f" ]; then
FORCE=true
elif [ "$1" == "--verbose" ] || [ "$1" == "-v" ]; then
ECHO_LEVEL=4
elif [ "$1" == "--debug" ] || [ "$1" == "-d" ]; then
ECHO_LEVEL=5
LOG_LEVEL=5
fi
shift
done
# Check prerequisites
if [ ! -d "$LETSENCRYPT_HOME" ]; then
fatal "Base directory '$LETSENCRYPT_HOME' not found."
fi
for dir in "$LETSENCRYPT_HOME/"{bin,certs,certs/archive,private,challenges}; do
if [ ! -d "$dir" ]; then
fatal "Required directory '$dir' not found."
fi
done
for file in "$LETSENCRYPT_HOME/"{bin/acme_tiny.py,private/account.key,private/domain.csr}; do
if [ ! -f "$file" ]; then
fatal "Required file '$file' not found."
fi
done
# Get the current date as seconds since epoch.
NOW=$(date +%s)
if $FORCE; then
info "Forcing certificate renewal."
lifetime=0
elif [ -f "$LETSENCRYPT_HOME/certs/live/signed.crt" ]; then
# Get the expiration date of the current certificate.
expiration_timestamp=$(get_expiration_date "$LETSENCRYPT_HOME/certs/live/signed.crt")
if [ "$?" -ne 0 ]; then
fatal "Unable to get expiration date of certificate $LETSENCRYPT_HOME/certs/live/signed.crt."
fi
# Calculate the time left until the certificate expires.
lifetime=$(( $expiration_timestamp - $NOW ))
debug "The current certificate will expire on $(date -d @$expiration_timestamp +'%b %d %H:%M:%S')."
else
warning "Current certificate not found, issuing a new certificate."
lifetime=0
fi
renew_sec=$(( $RENEW * 86400 ))
# If the certificate has less life remaining than we want.
if [ "$lifetime" -lt "$renew_sec" ]; then
NEW_CERT_PATH="$LETSENCRYPT_HOME/certs/archive/$(date --utc +'%FT%TZ')"
if [ -e "$NEW_CERT_PATH" ]; then
fatal "Cannot create directory '$NEW_CERT_PATH' : file exists."
fi
mkdir --mode=775 $NEW_CERT_PATH
if [ "$?" -ne 0 ]; then
fatal "Cannot create directory '$NEW_CERT_PATH'."
fi
debug "Downloading the intermediate certificate."
if [ -n "$SSL_CERT_FILE" -a -f "$SSL_CERT_FILE" ]; then
export SSL_CERT_FILE=$SSL_CERT_FILE
fi
wget -q -O - "https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem" > "$NEW_CERT_PATH/intermediate.pem"
if [ "$?" -ne 0 ]; then
fatal "Failed to retrieve intermediate certificate."
fi
if [ "$(head -n 1 "$NEW_CERT_PATH/intermediate.pem")" != "-----BEGIN CERTIFICATE-----" ]; then
fatal "Invalid certificate '$NEW_CERT_PATH/intermediate.pem'"
fi
debug "Requesting certificate renewal."
$PYTHON "$LETSENCRYPT_HOME/bin/acme_tiny.py" --account-key "$LETSENCRYPT_HOME/private/account.key" --csr "$LETSENCRYPT_HOME/private/domain.csr" --acme-dir "$LETSENCRYPT_HOME/challenges" > "$NEW_CERT_PATH/signed.crt"
if [ "$?" -ne 0 ]; then
fatal "Failed to renew certificate."
fi
if [ "$(head -n 1 "$NEW_CERT_PATH/signed.crt")" != "-----BEGIN CERTIFICATE-----" ]; then
fatal "Invalid certificate '$NEW_CERT_PATH/signed.crt'"
fi
debug "Building the chained certificate."
cat "$NEW_CERT_PATH/signed.crt" "$NEW_CERT_PATH/intermediate.pem" > "$NEW_CERT_PATH/chained.pem"
debug "Deploying the new certificate."
chmod $CERTS_PERM "$NEW_CERT_PATH/"{chained.pem,intermediate.pem,signed.crt}
# Remove previous link
if [ -L "$LETSENCRYPT_HOME/certs/live" ]; then
rm "$LETSENCRYPT_HOME/certs/live"
fi
# Create the link to the new cerificate directory
ln -s "$NEW_CERT_PATH" "$LETSENCRYPT_HOME/certs/live"
# Reload Apache to apply the new certificate
if $RELOAD_APACHE; then
info "Reloading Apache."
reload_apache
fi
# Executing post-renew script
if [ -x "$POST_RENEW_SCRIPT" ]; then
info "Executing post-renew script: $POST_RENEW_SCRIPT."
. $POST_RENEW_SCRIPT
fi
expiration_date=$(get_expiration_date "$LETSENCRYPT_HOME/certs/live/signed.crt" '%b %d %H:%M:%S')
if [ "$?" -ne 0 ]; then
error "Unable to get expiration date of certificate $NEW_CERT_PATH/signed.crt."
fi
info "Certificate renewed successfully."
info "The new certificate will expire on $expiration_date."
else
info "The certificate is up to date, renewal skipped ($(( $lifetime / 86400 )) days left)."
fi
}
# Call the main entry point
renew "$@"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment