system-config/playbooks/roles/letsencrypt-acme-sh-install/files/driver.sh

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