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
188 lines
7.3 KiB
Bash
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
|