-
-
Save ksubileau/e4568738117ce1afbe57ab456ac2223c to your computer and use it in GitHub Desktop.
Yet another script to renew Let's Encrypt certificates using acme-tiny.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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