2017-10-19 11:05:54 +02:00
|
|
|
#!/bin/bash
|
2017-08-24 18:25:42 +02:00
|
|
|
#
|
|
|
|
# evoacme is a shell script to manage Let's Encrypt certificate with
|
|
|
|
# certbot tool but with a dedicated user (no-root) and from a csr
|
|
|
|
#
|
|
|
|
# Author: Victor Laborie <vlaborie@evolix.fr>
|
|
|
|
# Licence: AGPLv3
|
|
|
|
#
|
2016-12-14 15:49:34 +01:00
|
|
|
|
2017-09-21 03:29:55 +02:00
|
|
|
set -e
|
2017-10-12 18:19:09 +02:00
|
|
|
set -u
|
2017-09-21 03:29:55 +02:00
|
|
|
|
2017-09-11 14:18:20 +02:00
|
|
|
usage() {
|
2017-10-17 14:46:26 +02:00
|
|
|
cat <<EOT
|
|
|
|
Usage: ${PROGNAME} NAME
|
|
|
|
NAME must be correspond to :
|
|
|
|
- a CSR in ${CSR_DIR}/NAME.csr
|
|
|
|
- a KEY in ${SSL_KEY_DIR}/NAME.key
|
|
|
|
|
|
|
|
If env variable TEST=1, certbot is run in staging mode
|
|
|
|
If env variable DRY_RUN=1, certbot is run in dry-run mode
|
|
|
|
If env variable QUIET=1, no message is output
|
|
|
|
If env variable VERBOSE=1, debug messages are output
|
|
|
|
EOT
|
2017-09-11 14:18:20 +02:00
|
|
|
}
|
|
|
|
|
2017-10-17 14:46:26 +02:00
|
|
|
log() {
|
2017-10-18 00:43:33 +02:00
|
|
|
if [ "${QUIET}" != "1" ]; then
|
|
|
|
echo "${PROGNAME}: $1"
|
|
|
|
fi
|
2017-10-17 14:46:26 +02:00
|
|
|
}
|
2017-10-03 09:50:53 +02:00
|
|
|
debug() {
|
2017-10-18 00:43:33 +02:00
|
|
|
if [ "${VERBOSE}" = "1" ] && [ "${QUIET}" != "1" ]; then
|
|
|
|
>&2 echo "${PROGNAME}: $1"
|
|
|
|
fi
|
2017-10-03 09:50:53 +02:00
|
|
|
}
|
|
|
|
error() {
|
2017-10-17 14:46:26 +02:00
|
|
|
>&2 echo "${PROGNAME}: $1"
|
|
|
|
[ "$1" = "invalid argument(s)" ] && >&2 usage
|
2017-10-03 09:50:53 +02:00
|
|
|
exit 1
|
|
|
|
}
|
|
|
|
|
2017-10-12 18:19:53 +02:00
|
|
|
sed_cert_path_for_apache() {
|
2017-10-17 14:46:26 +02:00
|
|
|
local vhost=$1
|
|
|
|
local vhost_full_path="/etc/apache2/ssl/${vhost}.conf"
|
|
|
|
local cert_path=$2
|
2017-10-12 00:29:21 +02:00
|
|
|
|
2017-10-24 17:37:46 +02:00
|
|
|
[ ! -r "${vhost_full_path}" ] && return 0
|
2017-10-13 17:14:03 +02:00
|
|
|
|
2017-10-17 14:46:26 +02:00
|
|
|
local search="^SSLCertificateFile.*$"
|
|
|
|
local replace="SSLCertificateFile ${cert_path}"
|
2017-10-13 17:14:03 +02:00
|
|
|
|
2017-11-21 16:17:21 +01:00
|
|
|
if grep -qE "${search}" "${vhost_full_path}"; then
|
2017-10-13 17:14:03 +02:00
|
|
|
[ -w "${vhost_full_path}" ] || error "File ${vhost_full_path} is not writable"
|
|
|
|
|
2017-11-21 14:44:36 +01:00
|
|
|
sed -i "s~${search}~${replace}~" "${vhost_full_path}"
|
2017-10-13 17:14:03 +02:00
|
|
|
debug "Config in ${vhost_full_path} has been updated"
|
|
|
|
$(command -v apache2ctl) -t
|
|
|
|
fi
|
2017-08-24 18:25:42 +02:00
|
|
|
}
|
2017-10-12 18:19:53 +02:00
|
|
|
sed_cert_path_for_nginx() {
|
2017-10-17 14:46:26 +02:00
|
|
|
local vhost=$1
|
|
|
|
local vhost_full_path="/etc/nginx/ssl/${vhost}.conf"
|
|
|
|
local cert_path=$2
|
2017-10-12 00:29:21 +02:00
|
|
|
|
2017-10-24 17:37:46 +02:00
|
|
|
[ ! -r "${vhost_full_path}" ] && return 0
|
2017-10-13 17:14:03 +02:00
|
|
|
|
2017-10-17 14:46:26 +02:00
|
|
|
local search="^ssl_certificate[^_].*$"
|
|
|
|
local replace="ssl_certificate ${cert_path};"
|
2017-10-13 17:14:03 +02:00
|
|
|
|
2017-11-21 16:17:21 +01:00
|
|
|
if grep -qE "${search}" "${vhost_full_path}"; then
|
2017-10-13 17:14:03 +02:00
|
|
|
[ -w "${vhost_full_path}" ] || error "File ${vhost_full_path} is not writable"
|
|
|
|
|
|
|
|
sed -i "s~${search}~${replace}~" "${vhost_full_path}"
|
|
|
|
debug "Config in ${vhost_full_path} has been updated"
|
|
|
|
$(command -v nginx) -t
|
|
|
|
fi
|
2017-10-12 00:29:21 +02:00
|
|
|
}
|
|
|
|
x509_verify() {
|
2017-10-17 14:46:26 +02:00
|
|
|
local file="$1"
|
2017-10-13 12:28:44 +02:00
|
|
|
[ -r "$file" ] || error "File ${file} not found"
|
2017-10-17 14:46:26 +02:00
|
|
|
"${OPENSSL_BIN}" x509 -noout -modulus -in "$file" >/dev/null
|
2017-10-12 18:20:49 +02:00
|
|
|
}
|
2017-10-17 14:46:26 +02:00
|
|
|
x509_enddate() {
|
|
|
|
local file="$1"
|
2017-10-13 12:28:44 +02:00
|
|
|
[ -r "$file" ] || error "File ${file} not found"
|
2017-10-17 14:46:26 +02:00
|
|
|
"${OPENSSL_BIN}" x509 -noout -enddate -in "$file"
|
2017-10-12 00:29:21 +02:00
|
|
|
}
|
2017-10-17 14:46:26 +02:00
|
|
|
csr_verify() {
|
|
|
|
local file="$1"
|
2017-10-13 12:28:44 +02:00
|
|
|
[ -r "$file" ] || error "File ${file} not found"
|
2017-10-17 14:46:26 +02:00
|
|
|
"${OPENSSL_BIN}" req -noout -modulus -in "$file" >/dev/null
|
2017-08-24 18:25:42 +02:00
|
|
|
}
|
2016-12-14 15:49:34 +01:00
|
|
|
|
2017-08-24 18:25:42 +02:00
|
|
|
main() {
|
2017-10-17 14:46:26 +02:00
|
|
|
# check arguments
|
|
|
|
[ "$#" -eq 1 ] || error "invalid argument(s)"
|
2017-09-21 03:29:55 +02:00
|
|
|
|
2017-10-19 11:08:16 +02:00
|
|
|
[ "$1" = "-h" ] || [ "$1" = "--help" ] && usage && exit 0
|
2017-10-19 11:07:45 +02:00
|
|
|
|
|
|
|
mkdir -p "${ACME_DIR}"
|
2017-10-24 17:38:05 +02:00
|
|
|
chown acme: "${ACME_DIR}"
|
2017-10-13 12:46:40 +02:00
|
|
|
[ -w "${ACME_DIR}" ] || error "Directory ${ACME_DIR} is not writable"
|
2017-10-19 11:07:45 +02:00
|
|
|
|
|
|
|
[ -d "${CSR_DIR}" ] || error "Directory ${CSR_DIR} is not found"
|
|
|
|
|
|
|
|
mkdir -p "${CRT_DIR}"
|
2017-10-24 17:38:05 +02:00
|
|
|
chown acme: "${CRT_DIR}"
|
2017-10-13 12:46:40 +02:00
|
|
|
[ -w "${CRT_DIR}" ] || error "Directory ${CRT_DIR} is not writable"
|
2017-10-19 11:07:45 +02:00
|
|
|
|
|
|
|
mkdir -p "${LOG_DIR}"
|
2017-10-24 17:38:05 +02:00
|
|
|
chown acme: "${LOG_DIR}"
|
2017-10-13 12:46:40 +02:00
|
|
|
[ -w "${LOG_DIR}" ] || error "Directory ${LOG_DIR} is not writable"
|
|
|
|
|
2017-10-19 22:21:18 +02:00
|
|
|
mkdir -p "${HOOKS_DIR}"
|
2017-10-24 17:38:05 +02:00
|
|
|
chown acme: "${HOOKS_DIR}"
|
2017-10-19 22:21:18 +02:00
|
|
|
[ -d "${HOOKS_DIR}" ] || error "Directory ${HOOKS_DIR} is not found"
|
|
|
|
|
2017-10-17 14:46:26 +02:00
|
|
|
readonly VHOST=$(basename "$1" .conf)
|
2017-10-12 00:29:21 +02:00
|
|
|
|
|
|
|
# check for important programs
|
2017-10-17 14:46:26 +02:00
|
|
|
readonly OPENSSL_BIN=$(command -v openssl) || error "openssl command not installed"
|
|
|
|
readonly CERTBOT_BIN=$(command -v certbot) || error "certbot command not installed"
|
2017-10-12 00:29:21 +02:00
|
|
|
|
|
|
|
# double check for directories
|
2017-10-13 12:30:24 +02:00
|
|
|
[ -d "${ACME_DIR}" ] || error "${ACME_DIR} is not a directory"
|
|
|
|
[ -d "${CSR_DIR}" ] || error "${CSR_DIR} is not a directory"
|
|
|
|
[ -d "${LOG_DIR}" ] || error "${LOG_DIR} is not a directory"
|
2017-10-12 00:29:21 +02:00
|
|
|
|
|
|
|
#### CSR VALIDATION
|
2017-09-21 03:29:55 +02:00
|
|
|
|
|
|
|
# verify .csr file
|
2017-10-17 14:46:26 +02:00
|
|
|
readonly CSR_FILE="${CSR_DIR}/${VHOST}.csr"
|
2017-10-12 00:29:21 +02:00
|
|
|
debug "Using CSR file: ${CSR_FILE}"
|
2017-10-13 12:30:24 +02:00
|
|
|
[ -f "${CSR_FILE}" ] || error "${CSR_FILE} absent"
|
|
|
|
[ -r "${CSR_FILE}" ] || error "${CSR_FILE} is not readable"
|
2017-10-12 00:29:21 +02:00
|
|
|
|
2017-10-12 18:20:49 +02:00
|
|
|
csr_verify "${CSR_FILE}" || error "${CSR_FILE} is invalid"
|
2017-09-21 03:29:55 +02:00
|
|
|
|
|
|
|
# Hook for evoadmin-web in cluster mode : check master status
|
2017-10-17 14:46:26 +02:00
|
|
|
local evoadmin_state_file="/home/${VHOST}/state"
|
2017-10-13 12:28:44 +02:00
|
|
|
[ -r "${evoadmin_state_file}" ] \
|
2017-10-12 18:22:43 +02:00
|
|
|
&& grep -q "STATE=slave" "${evoadmin_state_file}" \
|
|
|
|
&& debug "We are slave of this evoadmin cluster. Quit!" \
|
|
|
|
&& exit 0
|
2017-09-11 14:18:20 +02:00
|
|
|
|
2017-10-12 00:29:21 +02:00
|
|
|
#### INIT OR RENEW?
|
|
|
|
|
2017-10-17 14:46:26 +02:00
|
|
|
readonly LIVE_DIR="${CRT_DIR}/${VHOST}/live"
|
|
|
|
readonly LIVE_CERT="${LIVE_DIR}/cert.crt"
|
|
|
|
readonly LIVE_FULLCHAIN="${LIVE_DIR}/fullchain.pem"
|
|
|
|
readonly LIVE_CHAIN="${LIVE_DIR}/chain.pem"
|
2017-10-12 00:29:21 +02:00
|
|
|
|
|
|
|
# If live symlink already exists, it's not our first time...
|
|
|
|
if [ -h "${LIVE_DIR}" ]; then
|
|
|
|
# we have a live symlink
|
|
|
|
# let's see if there is a cert to renew
|
|
|
|
x509_verify "${LIVE_CERT}" || error "${LIVE_CERT} is invalid"
|
|
|
|
|
|
|
|
# Verify if our certificate will expire
|
|
|
|
crt_end_date=$(x509_enddate "${LIVE_CERT}" | cut -d= -f2)
|
|
|
|
date_renew=$(date -ud "${crt_end_date} - ${SSL_MINDAY} days" +"%s")
|
|
|
|
date_today=$(date +'%s')
|
|
|
|
if [ "${date_today}" -lt "${date_renew}" ]; then
|
|
|
|
debug "Cert ${LIVE_CERT} expires at ${crt_end_date} => more than ${SSL_MINDAY} days: kthxbye."
|
|
|
|
exit 0
|
|
|
|
fi
|
2017-09-21 00:39:06 +02:00
|
|
|
fi
|
2017-09-21 03:29:55 +02:00
|
|
|
|
2017-10-12 00:29:21 +02:00
|
|
|
#### CERTIFICATE CREATION WITH CERTBOT
|
2017-09-21 03:29:55 +02:00
|
|
|
|
2017-10-17 14:46:26 +02:00
|
|
|
local iteration=$(date "+%Y%m%d%H%M%S")
|
|
|
|
[ -n "${iteration}" ] || error "invalid iteration (${iteration})"
|
2017-09-21 03:29:55 +02:00
|
|
|
|
2017-10-17 14:46:26 +02:00
|
|
|
readonly NEW_DIR="${CRT_DIR}/${VHOST}/${iteration}"
|
2017-09-21 03:29:55 +02:00
|
|
|
|
2017-10-12 00:29:21 +02:00
|
|
|
[ -d "${NEW_DIR}" ] && error "${NEW_DIR} directory already exists, remove it manually."
|
2017-10-13 12:08:47 +02:00
|
|
|
mkdir -p "${NEW_DIR}"
|
|
|
|
chmod -R 0700 "${CRT_DIR}"
|
|
|
|
chown -R acme: "${CRT_DIR}"
|
2017-10-12 00:29:21 +02:00
|
|
|
debug "New cert will be created in ${NEW_DIR}"
|
|
|
|
|
2017-10-17 14:46:26 +02:00
|
|
|
readonly NEW_CERT="${NEW_DIR}/cert.crt"
|
|
|
|
readonly NEW_FULLCHAIN="${NEW_DIR}/fullchain.pem"
|
|
|
|
readonly NEW_CHAIN="${NEW_DIR}/chain.pem"
|
2017-10-12 00:29:21 +02:00
|
|
|
|
2017-10-17 14:46:26 +02:00
|
|
|
local CERTBOT_MODE=""
|
2017-10-12 18:22:06 +02:00
|
|
|
[ "${TEST}" = "1" ] && CERTBOT_MODE="${CERTBOT_MODE} --test-cert"
|
2017-10-17 14:46:26 +02:00
|
|
|
[ "${QUIET}" = "1" ] && CERTBOT_MODE="${CERTBOT_MODE} --quiet"
|
2017-10-12 18:22:06 +02:00
|
|
|
[ "${DRY_RUN}" = "1" ] && CERTBOT_MODE="${CERTBOT_MODE} --dry-run"
|
2017-10-12 00:29:21 +02:00
|
|
|
|
2017-10-17 14:46:26 +02:00
|
|
|
local CERTBOT_REGISTRATION="--agree-tos"
|
2017-10-12 00:29:21 +02:00
|
|
|
if [ -n "${SSL_EMAIL}" ]; then
|
|
|
|
debug "Registering at certbot with ${SSL_EMAIL} as email"
|
|
|
|
CERTBOT_REGISTRATION="${CERTBOT_REGISTRATION} -m ${SSL_EMAIL}"
|
2017-09-21 03:29:55 +02:00
|
|
|
else
|
2017-10-12 00:29:21 +02:00
|
|
|
debug "Registering at certbot without email"
|
|
|
|
CERTBOT_REGISTRATION="${CERTBOT_REGISTRATION} --register-unsafely-without-email"
|
2017-09-21 00:39:06 +02:00
|
|
|
fi
|
2017-09-21 03:29:55 +02:00
|
|
|
|
2017-10-13 12:08:47 +02:00
|
|
|
# Permissions checks for acme user
|
|
|
|
sudo -u acme test -r "${CSR_FILE}" || error "File ${CSR_FILE} is not readable by user 'acme'"
|
2017-10-13 12:32:16 +02:00
|
|
|
sudo -u acme test -w "${NEW_DIR}" || error "Directory ${NEW_DIR} is not writable by user 'acme'"
|
2017-10-13 12:08:47 +02:00
|
|
|
|
2017-10-12 00:29:21 +02:00
|
|
|
# create a certificate with certbot
|
2017-10-12 18:22:43 +02:00
|
|
|
sudo -u acme \
|
2017-10-17 14:46:26 +02:00
|
|
|
"${CERTBOT_BIN}" \
|
2017-10-12 00:29:21 +02:00
|
|
|
certonly \
|
2017-10-12 18:22:43 +02:00
|
|
|
${CERTBOT_MODE} \
|
|
|
|
${CERTBOT_REGISTRATION} \
|
|
|
|
--non-interactive \
|
|
|
|
--webroot \
|
|
|
|
--csr "${CSR_FILE}" \
|
|
|
|
--webroot-path "${ACME_DIR}" \
|
|
|
|
--cert-path "${NEW_CERT}" \
|
|
|
|
--fullchain-path "${NEW_FULLCHAIN}" \
|
|
|
|
--chain-path "${NEW_CHAIN}" \
|
|
|
|
--logs-dir "$LOG_DIR" \
|
|
|
|
2>&1 \
|
|
|
|
| grep -v "certbot.crypto_util"
|
2017-09-21 03:29:55 +02:00
|
|
|
|
2017-10-13 17:13:14 +02:00
|
|
|
if [ "${DRY_RUN}" = "1" ]; then
|
2017-10-17 14:46:26 +02:00
|
|
|
debug "In dry-run mode, we stop here. Bye"
|
2017-10-13 17:13:14 +02:00
|
|
|
exit 0
|
|
|
|
fi
|
|
|
|
|
2017-09-21 03:29:55 +02:00
|
|
|
# verify if all is right
|
2017-10-13 12:30:34 +02:00
|
|
|
x509_verify "${NEW_CERT}" || error "${NEW_CERT} is invalid"
|
2017-10-12 00:29:21 +02:00
|
|
|
x509_verify "${NEW_FULLCHAIN}" || error "${NEW_FULLCHAIN} is invalid"
|
2017-10-13 12:30:34 +02:00
|
|
|
x509_verify "${NEW_CHAIN}" || error "${NEW_CHAIN} is invalid"
|
2017-10-12 00:29:21 +02:00
|
|
|
|
2017-10-17 14:46:26 +02:00
|
|
|
log "New certificate available at ${NEW_CERT}"
|
|
|
|
|
2017-10-12 00:29:21 +02:00
|
|
|
#### CERTIFICATE ACTIVATION
|
2017-09-21 03:29:55 +02:00
|
|
|
|
|
|
|
# link dance
|
2017-10-12 00:29:21 +02:00
|
|
|
if [ -h "${LIVE_DIR}" ]; then
|
|
|
|
rm "${LIVE_DIR}"
|
|
|
|
debug "Remove ${LIVE_DIR} link"
|
|
|
|
fi
|
|
|
|
ln -s "${NEW_DIR}" "${LIVE_DIR}"
|
|
|
|
debug "Link ${NEW_DIR} to ${LIVE_DIR}"
|
|
|
|
# verify final path
|
|
|
|
x509_verify "${LIVE_CERT}" || error "${LIVE_CERT} is invalid"
|
2017-09-21 03:29:55 +02:00
|
|
|
|
2017-10-19 22:21:18 +02:00
|
|
|
# update Apache
|
|
|
|
sed_cert_path_for_apache "${VHOST}" "${LIVE_FULLCHAIN}"
|
|
|
|
# update Nginx
|
|
|
|
sed_cert_path_for_nginx "${VHOST}" "${LIVE_FULLCHAIN}"
|
2017-10-12 00:29:21 +02:00
|
|
|
|
2017-10-19 23:23:51 +02:00
|
|
|
#### EXECUTE HOOKS
|
|
|
|
#
|
|
|
|
# executable scripts placed in ${HOOKS_DIR}
|
|
|
|
# are executed, unless their name ends with ".disabled"
|
|
|
|
|
|
|
|
export EVOACME_VHOST_NAME="${VHOST}"
|
|
|
|
export EVOACME_CERT="${LIVE_CERT}"
|
|
|
|
export EVOACME_CHAIN="${LIVE_CHAIN}"
|
|
|
|
export EVOACME_FULLCHAIN="${LIVE_FULLCHAIN}"
|
|
|
|
|
2017-10-20 10:15:12 +02:00
|
|
|
# search for files in hooks directory
|
|
|
|
for hook in $(find ${HOOKS_DIR} -type f); do
|
|
|
|
# keep only executables files, not containing a "."
|
2017-10-24 17:38:59 +02:00
|
|
|
if [ -x "${hook}" ] && (basename "${hook}" | grep -vqF "."); then
|
2017-10-19 23:23:51 +02:00
|
|
|
debug "Executing ${hook}"
|
|
|
|
${hook}
|
|
|
|
fi
|
|
|
|
done
|
2017-08-24 18:25:42 +02:00
|
|
|
}
|
|
|
|
|
2017-10-17 14:46:26 +02:00
|
|
|
readonly PROGNAME=$(basename "$0")
|
2017-10-18 00:42:15 +02:00
|
|
|
readonly PROGDIR=$(realpath -m $(dirname "$0"))
|
2017-10-17 14:46:26 +02:00
|
|
|
readonly ARGS=$@
|
|
|
|
|
|
|
|
readonly VERBOSE=${VERBOSE:-"0"}
|
|
|
|
readonly QUIET=${QUIET:-"0"}
|
|
|
|
readonly TEST=${TEST:-"0"}
|
|
|
|
readonly DRY_RUN=${DRY_RUN:-"0"}
|
|
|
|
|
|
|
|
# Read configuration file, if it exists
|
|
|
|
[ -r /etc/default/evoacme ] && . /etc/default/evoacme
|
|
|
|
|
|
|
|
# Default value for main variables
|
|
|
|
readonly SSL_KEY_DIR=${SSL_KEY_DIR:-"/etc/ssl/private"}
|
|
|
|
readonly ACME_DIR=${ACME_DIR:-"/var/lib/letsencrypt"}
|
|
|
|
readonly CSR_DIR=${CSR_DIR:-"/etc/ssl/requests"}
|
|
|
|
readonly CRT_DIR=${CRT_DIR:-"/etc/letsencrypt"}
|
|
|
|
readonly LOG_DIR=${LOG_DIR:-"/var/log/evoacme"}
|
2017-10-19 22:21:18 +02:00
|
|
|
readonly HOOKS_DIR=${HOOKS_DIR:-"${CRT_DIR}/hooks"}
|
2017-10-17 14:46:26 +02:00
|
|
|
readonly SSL_MINDAY=${SSL_MINDAY:-"30"}
|
|
|
|
readonly SSL_EMAIL=${SSL_EMAIL:-""}
|
|
|
|
|
|
|
|
main ${ARGS}
|