Files
system-config/playbooks/roles/letsencrypt-acme-sh-install/files/driver.sh
Ian Wienand 864f39bfff letsencrypt-acme-sh-install: handle errors better in driver
Currently we discard the exit code of the acme.sh call and swallow any
possible errors.  Although they are logged, it means the Ansible calls
won't fail and you'll have to debug much later on why you didn't get a
certificate as expected.

Capture the failure of the call and log it better.  Note that when
skipping renewal due to current valid certificates acme.sh returns
"2".  After [1] acme.sh is returning "3" when it exits with a TXT
entry requiring validation; anything else is an error on the request
path.  Valid issues should be "0" and anything else will be an error.

While we here, make sure we always output the end stamp by putting it
in a exit trap.

[1] 2d4ea720eb

Change-Id: Ica63860f3221e99ca0a2aa2636d573fc134447bb
2022-08-05 08:18:55 +10:00

188 lines
7.3 KiB
Bash

#!/bin/bash
ACME_SH=${ACME_SH:-/opt/acme.sh/acme.sh}
CERT_HOME=${CERT_HOME:-/etc/letsencrypt-certs}
# Common CA setup by Zuul test infrastructure
OPENDEV_CA_HOME=${OPENDEV_CA_HOME:-/etc/opendev-ca}
CHALLENGE_ALIAS_DOMAIN=${CHALLENGE_ALIAS_DOMAIN:-acme.opendev.org.}
# Set to !0 to use letsencrypt staging rather than production requests
LETSENCRYPT_STAGING=${LETSENCRYPT_STAGING:-0}
LOG_FILE=${LOG_FILE:-/var/log/acme.sh/acme.sh.log}
SERVER=""
if [[ ${LETSENCRYPT_STAGING} != 0 ]]; then
# TODO acme.sh doesn't let us specify staging and also set the server.
# If --staging is passed then the built in default is used. Can/should
# we change this to --server letsencrypt_test?
SERVER="--staging"
#SERVER="--server letsencrypt_test"
else
SERVER="--server letsencrypt"
fi
# Ensure we don't write out files as world-readable
umask 027
function _exit {
echo "--- end --- $(date -u '+%Y-%m-%dT%k:%M:%S%z') ---" >> ${LOG_FILE}
}
trap _exit EXIT
echo -e "\n--- start --- ${1} --- $(date -u '+%Y-%m-%dT%k:%M:%S%z') ---" >> ${LOG_FILE}
if [[ ${1} == "issue" ]]; then
# Take output like:
# [Thu Feb 14 13:44:37 AEDT 2019] Domain: '_acme-challenge.test.opendev.org'
# [Thu Feb 14 13:44:37 AEDT 2019] TXT value: 'QjkChGcuqD7rl0jN8FNWkWNAISX1Zry_vE-9RxWF2pE'
#
# and turn it into:
#
# _acme-challenge.test.opendev.org:QjkChGcuqD7rl0jN8FNWkWNAISX1Zry_vE-9RxWF2pE
#
# Ansible then parses this back to a dict.
shift;
for arg in "$@"; do
$ACME_SH ${SERVER} \
--cert-home ${CERT_HOME} \
--no-color \
--yes-I-know-dns-manual-mode-enough-go-ahead-please \
--issue \
--dns \
--challenge-alias ${CHALLENGE_ALIAS_DOMAIN} \
$arg 2>&1 | tee -a ${LOG_FILE} | \
egrep 'Domain:|TXT value:' | cut -d"'" -f2 | paste -d':' - -
# shell magic ^ is
# - extract everything between ' '
# - stick every two lines together, separated by a :
_exit_code=${PIPESTATUS[0]}
if [[ ${_exit_code} == 2 ]]; then
echo "Valid and current certificate found" >> ${LOG_FILE}
exit 0
elif [[ ${_exit_code} == 3 ]]; then
echo "Certificate request issued" >> ${LOG_FILE}
else
echo "Unknown failure: ${_exit_code}" >> ${LOG_FILE}
exit ${_exit_code}
fi
done
elif [[ ${1} == "issue-selfsign" ]]; then
shift;
for arg in "$@"; do
# looks like
# "-d foo01.com -d foo.com " "-d bar01.com -d bar.com"
arr=(${arg})
len=${#arr[@]}
for (( i=0; i<$len; i++ )); do
if [[ $((i%2)) -eq 0 ]]; then
continue # this should be a "-d"
else
# The ACME protocol hashes "stuff" and the TXT record
# is ultimately a sha256 encoded into base64url
# (RFC8555); emulate that here.
base64url=$(echo -n ${arr[$i]}-${RANDOM} | \
openssl dgst -binary -sha256 | \
openssl base64 | sed 's/+/-/g; s,/,_,g; s/=//g')
echo "${arr[$i]}:${base64url}"
fi
done
done
elif [[ ${1} == "renew" ]]; then
shift;
for arg in "$@"; do
# NOTE(ianw) 2020-02-28 : we only set force here because of a
# bug/misfeature in acme.sh dns manual-mode where it does not
# notice that the renewal is required when we update domain
# names in a cert
# (https://github.com/acmesh-official/acme.sh/issues/2763).
# This is safe (i.e. will not explode our quota limits by
# constantly renewing) because Ansible only calls this path
# when TXT records have been installed for this certificate;
# i.e. we will never run this renewal unless it is actually
# required.
$ACME_SH ${SERVER} \
--cert-home ${CERT_HOME} \
--no-color \
--yes-I-know-dns-manual-mode-enough-go-ahead-please \
--force \
--renew \
$arg 2>&1 | tee -a ${LOG_FILE}
_exit_code=${PIPESTATUS[0]}
if [[ ${_exit_code} == 2 ]]; then
echo "Valid and current certificate found" >> ${LOG_FILE}
exit 0
elif [[ ${_exit_code} == 0 ]]; then
echo "Certificate renewed" >> ${LOG_FILE}
else
echo "Unknown failure: ${_exit_code}" >> ${LOG_FILE}
exit ${_exit_code}
fi
done
elif [[ ${1} == "selfsign" ]]; then
# For testing, simulate the key generation
shift;
for arg in "$@"; do
{
read -r -a domain_array <<< "$arg"
domain=${domain_array[1]}
mkdir -p ${CERT_HOME}/${domain}
cd ${CERT_HOME}/${domain}
echo "Creating certs in ${CERT_HOME}/${domain}"
# Create key for domain
openssl genrsa -out ${domain}.key 2048
# openssl makes this 0600; match the permissions in acme.sh
chmod 0640 ${domain}.key
# Create the certificate signing request
openssl req -new -sha256 \
-key ${domain}.key \
-subj "/C=US/ST=CA/O=OpenDev Infra/CN=${domain}" \
-out ${domain}.csr
# The argument is "-d domain -d alias -d alias" Thus when
# reading, odd numbered elements > 1 are the SAN names.
# Always add the first (which must exist)
len=${#domain_array[@]}
san="DNS:${domain}"
if [[ ${len} -gt 2 ]]; then
for (( i=3; i < ${len}; i=i+2 )); do
echo "Adding SAN : ${domain_array[$i]}"
san="${san},DNS:${domain_array[$i]}"
done
fi
# Issue the certificate signed by the OpenDev CA that Zuul
# has pre-installed.
# NOTE(ianw) :
# * CA has to be ".crt" for update-ca-certificates but
# we've used ".cer" for certificates everywhere else
# just to make things confusing.
# * I've seen some guides add the SAN names to the CSR
# but I found x509 here requires it explicitly anyway
# to actually get it in the resulting certificate?
# Seems to be multiple ways to skin the cat with all
# these arguments and quite some variations across
# openssl versions.
openssl x509 -req -days 30 -sha256 \
-in ${domain}.csr \
-CA ${OPENDEV_CA_HOME}/ca.crt -CAkey ${OPENDEV_CA_HOME}/ca.key \
-CAcreateserial \
-out ${domain}.cer \
-extensions SAN -extfile <(printf "[SAN]\nsubjectAltName=${san}")
# Copy CA certificate for apache SSLCertificateChainFile
cp ${OPENDEV_CA_HOME}/ca.crt ca.cer
chown root:letsencrypt ca.cer
chmod 0640 ca.cer
# Save the fullchain (some apps like gitea require)
cat ${domain}.cer > fullchain.cer
cat ca.cer >> fullchain.cer
chown root:letsencyrpt fullchain.cer
chmod 0640 fullchain.cer
} 2>&1 | tee -a ${LOG_FILE}
done
else
echo "Unknown driver arg: $1"
exit 1
fi