Matthias Andreas Benkard | b382b10 | 2021-01-02 15:32:21 +0100 | [diff] [blame] | 1 | #!/bin/bash |
| 2 | |
| 3 | # Return values / exit codes |
| 4 | # 0 = cert created successfully |
| 5 | # 1 = cert renewed successfully |
| 6 | # 2 = cert not due for renewal |
| 7 | # * = errors |
| 8 | |
| 9 | |
| 10 | source /srv/functions.sh |
| 11 | |
| 12 | CERT_DOMAINS=(${DOMAINS[@]}) |
| 13 | CERT_DOMAIN=${CERT_DOMAINS[0]} |
| 14 | ACME_BASE=/var/lib/acme |
| 15 | |
| 16 | TYPE=${1} |
| 17 | PREFIX="" |
| 18 | # only support rsa certificates for now |
| 19 | if [[ "${TYPE}" != "rsa" ]]; then |
| 20 | log_f "Unknown certificate type '${TYPE}' requested" |
| 21 | exit 5 |
| 22 | fi |
| 23 | DOMAINS_FILE=${ACME_BASE}/${CERT_DOMAIN}/domains |
| 24 | CERT=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}cert.pem |
| 25 | SHARED_KEY=${ACME_BASE}/acme/${PREFIX}key.pem # must already exist |
| 26 | KEY=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}key.pem |
| 27 | CSR=${ACME_BASE}/${CERT_DOMAIN}/${PREFIX}acme.csr |
| 28 | |
| 29 | if [[ -z ${CERT_DOMAINS[*]} ]]; then |
| 30 | log_f "Missing CERT_DOMAINS to obtain a certificate" |
| 31 | exit 3 |
| 32 | fi |
| 33 | |
| 34 | if [[ "${LE_STAGING}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then |
| 35 | if [[ ! -z "${DIRECTORY_URL}" ]]; then |
| 36 | log_f "Cannot use DIRECTORY_URL with LE_STAGING=y - ignoring DIRECTORY_URL" |
| 37 | fi |
| 38 | log_f "Using Let's Encrypt staging servers" |
| 39 | DIRECTORY_URL='--directory-url https://acme-staging-v02.api.letsencrypt.org/directory' |
| 40 | elif [[ ! -z "${DIRECTORY_URL}" ]]; then |
| 41 | log_f "Using custom directory URL ${DIRECTORY_URL}" |
| 42 | DIRECTORY_URL="--directory-url ${DIRECTORY_URL}" |
| 43 | fi |
| 44 | |
| 45 | if [[ -f ${DOMAINS_FILE} && "$(cat ${DOMAINS_FILE})" == "${CERT_DOMAINS[*]}" ]]; then |
| 46 | if [[ ! -f ${CERT} || ! -f "${KEY}" || -f "${ACME_BASE}/force_renew" ]]; then |
| 47 | log_f "Certificate ${CERT} doesn't exist yet or forced renewal - start obtaining" |
| 48 | # Certificate exists and did not change but could be due for renewal (30 days) |
| 49 | elif ! openssl x509 -checkend 2592000 -noout -in ${CERT} > /dev/null; then |
| 50 | log_f "Certificate ${CERT} is due for renewal (< 30 days) - start renewing" |
| 51 | else |
| 52 | log_f "Certificate ${CERT} validation done, neither changed nor due for renewal." |
| 53 | exit 2 |
| 54 | fi |
| 55 | else |
| 56 | log_f "Certificate ${CERT} missing or changed domains '${CERT_DOMAINS[*]}' - start obtaining" |
| 57 | fi |
| 58 | |
| 59 | |
| 60 | # Make backup |
| 61 | if [[ -f ${CERT} ]]; then |
| 62 | DATE=$(date +%Y-%m-%d_%H_%M_%S) |
| 63 | BACKUP_DIR=${ACME_BASE}/backups/${CERT_DOMAIN}/${PREFIX}${DATE} |
| 64 | log_f "Creating backups in ${BACKUP_DIR} ..." |
| 65 | mkdir -p ${BACKUP_DIR}/ |
| 66 | [[ -f ${DOMAINS_FILE} ]] && cp ${DOMAINS_FILE} ${BACKUP_DIR}/ |
| 67 | [[ -f ${CERT} ]] && cp ${CERT} ${BACKUP_DIR}/ |
| 68 | [[ -f ${KEY} ]] && cp ${KEY} ${BACKUP_DIR}/ |
| 69 | [[ -f ${CSR} ]] && cp ${CSR} ${BACKUP_DIR}/ |
| 70 | fi |
| 71 | |
| 72 | mkdir -p ${ACME_BASE}/${CERT_DOMAIN} |
| 73 | if [[ ! -f ${KEY} ]]; then |
| 74 | log_f "Copying shared private key for this certificate..." |
| 75 | cp ${SHARED_KEY} ${KEY} |
| 76 | chmod 600 ${KEY} |
| 77 | fi |
| 78 | |
| 79 | # Generating CSR |
| 80 | printf "[SAN]\nsubjectAltName=" > /tmp/_SAN |
| 81 | printf "DNS:%s," "${CERT_DOMAINS[@]}" >> /tmp/_SAN |
| 82 | sed -i '$s/,$//' /tmp/_SAN |
| 83 | openssl req -new -sha256 -key ${KEY} -subj "/" -reqexts SAN -config <(cat /etc/ssl/openssl.cnf /tmp/_SAN) > ${CSR} |
| 84 | |
| 85 | # acme-tiny writes info to stderr and ceritifcate to stdout |
| 86 | # The redirects will do the following: |
| 87 | # - redirect stdout to temp certificate file |
| 88 | # - redirect acme-tiny stderr to stdout (logs to variable ACME_RESPONSE) |
| 89 | # - tee stderr to get live output and log to dockerd |
| 90 | |
| 91 | log_f "Checking resolver..." |
| 92 | until dig letsencrypt.org +time=3 +tries=1 @unbound > /dev/null; do |
| 93 | sleep 2 |
| 94 | done |
| 95 | log_f "Resolver OK" |
| 96 | |
| 97 | ACME_RESPONSE=$(acme-tiny ${DIRECTORY_URL} \ |
| 98 | --account-key ${ACME_BASE}/acme/account.pem \ |
| 99 | --disable-check \ |
| 100 | --csr ${CSR} \ |
| 101 | --acme-dir /var/www/acme/ 2>&1 > /tmp/_cert.pem | tee /dev/fd/5; exit ${PIPESTATUS[0]}) |
| 102 | SUCCESS="$?" |
| 103 | ACME_RESPONSE_B64=$(echo "${ACME_RESPONSE}" | openssl enc -e -A -base64) |
| 104 | log_f "${ACME_RESPONSE_B64}" redis_only b64 |
| 105 | case "$SUCCESS" in |
| 106 | 0) # cert requested |
| 107 | log_f "Deploying certificate ${CERT}..." |
| 108 | # Deploy the new certificate and key |
| 109 | # Moving temp cert to {domain} folder |
| 110 | if verify_hash_match /tmp/_cert.pem ${KEY}; then |
| 111 | RETURN=0 # certificate created |
| 112 | if [[ -f ${CERT} ]]; then |
| 113 | RETURN=1 # certificate renewed |
| 114 | fi |
| 115 | mv -f /tmp/_cert.pem ${CERT} |
| 116 | echo -n ${CERT_DOMAINS[*]} > ${DOMAINS_FILE} |
| 117 | rm /var/www/acme/* 2> /dev/null |
| 118 | log_f "Certificate successfully obtained" |
| 119 | exit ${RETURN} |
| 120 | else |
| 121 | log_f "Certificate was successfully requested, but key and certificate have non-matching hashes, ignoring certificate" |
| 122 | exit 4 |
| 123 | fi |
| 124 | ;; |
| 125 | *) # non-zero is non-fun |
| 126 | log_f "Failed to obtain certificate ${CERT} for domains '${CERT_DOMAINS[*]}'" |
| 127 | redis-cli -h redis SET ACME_FAIL_TIME "$(date +%s)" |
| 128 | exit 100${SUCCESS} |
| 129 | ;; |
| 130 | esac |