26bd32cb1c
This is a follow-on to Ica63860f3221e99ca0a2aa2636d573fc134447bb to make what's happening with the various exit points clearer. Also sneak in an explaination of the weird arg input for clarity. Change-Id: Ib059f1de465430d6e6f674b6649817105b7ef9a0
198 lines
7.8 KiB
Bash
198 lines
7.8 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
|
|
# acme.sh really wants to talk to your SAAS DNS API for
|
|
# you to setup the challenge-reponse and then issue the
|
|
# cert; the "dns manual mode" requires the odd flags and
|
|
# also returns a separate error code when issuing a cert.
|
|
# For our purposes, this is a success.
|
|
echo "Certificate request issued" >> ${LOG_FILE}
|
|
exit 0
|
|
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}
|
|
exit 0
|
|
else
|
|
echo "Unknown failure: ${_exit_code}" >> ${LOG_FILE}
|
|
exit ${_exit_code}
|
|
fi
|
|
done
|
|
elif [[ ${1} == "selfsign" ]]; then
|
|
# For testing, simulate the key generation
|
|
# Note as above "arg" is a compound argument where each
|
|
# request is a space-separated separate string, e.g.
|
|
# "-d foo.com -d foo1.com" "-d bar.com -d bar1.com"
|
|
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
|