#!/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