#!/usr/bin/env bash # This script uses acme.sh to issue and deploy SSL certificates from Let's Encrypt for a list of domains # using the webroot or dns methods # # See README.md for more details # # Copyright 2020 Bryan Roessler unset SITES_DIR USEREMAIL DOMAIN_FILES DOMAIN_GROUPS DEPLOY_CMD_PREFIX ISSUE_CMD_PREFIX DEBUG GROUP DEBUG="true" # quote this line to stop DEBUG mode and issue certificates for real, or use --force in user options METHOD="dns" # set the default method CONF="$HOME/.acme.sh/account.conf" ACME_SH="$HOME/.acme.sh/acme.sh" parse_input() { local input declare -g USEREMAIL declare -ag DOMAIN_FILES if input=$(getopt -o +m:e:fgs:d -l method:,email:,force,group-by-file,sites-dir:,debug -- "$@"); then eval set -- "$input" while true; do case "$1" in --method|-m) shift METHOD="${1,,}" ;; --force|-f) unset DEBUG ;; --group-by-file|-g) GROUP="true" ;; --sites-dir|-s) shift SITES_DIR="$1" ;; --debug|-d) DEBUG="true" ;; --) shift break ;; esac shift done else echo "Incorrect options provided" exit 1 fi # Load domain files from remaining arguments if [[ $# -lt 1 ]]; then [[ -v SITES_DIR && -d "$SITES_DIR" ]] && return 0 if [[ -f "domains.txt" ]]; then echo "You have not supplied any domain files, using domains.txt by default" DOMAIN_FILES=("domains.txt") else echo "You must specify a domain list or use domains.txt by default" exit 1 fi else DOMAIN_FILES=("$@") fi } interactive_dns() { if [[ -f "$CONF" ]] && grep -q "CPANELDNS_AUTH_PASSWORD" "$CONF"; then echo "cPanel credentials already present, skipping configuration..." echo "To rerun the configuration, first run 'rm $CONF'" else read -rp 'Enter your cPanel username: ' CPANELDNS_AUTH_ID echo export CPANELDNS_AUTH_ID read -rp 'Enter your cPanel password: ' CPANELDNS_AUTH_PASSWORD echo export CPANELDNS_AUTH_PASSWORD read -rp 'Enter your cPanel address and port number (example: "https://www.example.com:2083/"): ' CPANELDNS_API echo export CPANELDNS_API fi } get_acme() { curl https://get.acme.sh | sh # shellcheck disable=SC1090 source "$HOME/.bashrc" "$ACME_SH" --upgrade --auto-upgrade [[ "$METHOD" == "dns" ]] && \ curl -o "$HOME/.acme.sh/dnsapi/dns_cpaneldns.sh" https://raw.githubusercontent.com/cryobry/dns_cpaneldns/master/dns_cpaneldns.sh } update_email() { if [[ ! -v USEREMAIL ]]; then if [[ -f "$CONF" ]] && line=$(grep -q "ACCOUNT_EMAIL" "$CONF"); then echo "Reusing existing contact e-mail: ${line#ACCOUNT_EMAIL=}" return 0 fi read -rp 'Enter your contact e-mail (in case of renewal failures): ' USEREMAIL fi "$ACME_SH" --update-account --accountemail "${USEREMAIL}" } command_prefixes() { declare -ag ISSUE_CMD_PREFIX DEPLOY_CMD_PREFIX ISSUE_CMD_PREFIX=("$ACME_SH" "--issue") [[ "$METHOD" == "dns" ]] && ISSUE_CMD_PREFIX=("${ISSUE_CMD_PREFIX[@]}" "--dns" "dns_cpaneldns") [[ -v DEBUG ]] && ISSUE_CMD_PREFIX=("${ISSUE_CMD_PREFIX[@]}" "--staging") || ISSUE_CMD_PREFIX=("${ISSUE_CMD_PREFIX[@]}" "--force") DEPLOY_CMD_PREFIX=("$ACME_SH" "--deploy" "--deploy-hook" "cpanel_uapi") } get_webroot() { local webroot if ! webroot=$(uapi DomainInfo single_domain_data domain="$1" | grep documentroot); then echo "UAPI call failed" >&2 fi if [[ ! -v webroot || "$webroot" == "" ]]; then if [[ -v DEBUG ]]; then webroot="/tmp" # set missing webroot in DEBUG mode for testing else echo "Could not find $1's webroot" >&2 exit 1 fi fi echo "$webroot" } # Either create a single array of all domains (DOMAINS) to issue one-by-one or create an array of array names to issue for a single webroot load_domains() { local domain_file declare -ag DOMAIN_GROUPS=() if [[ -v SITES_DIR ]]; then for domain_file in "$SITES_DIR"/*; do DOMAIN_GROUPS+=("$(<"$domain_file")") done fi for domain_file in "${DOMAIN_FILES[@]}"; do # Load list of domains as space-delimited strings in elements of the DOMAINS array # We can keep these separate or combine them later DOMAIN_GROUPS+=("$(<"$domain_file")") done } issue_and_deploy_certs() { local group_root domain_root domain domain_group for domain_group in "${DOMAIN_GROUPS[@]}"; do local -a issue_cmd=("${ISSUE_CMD_PREFIX[@]}") local -a deploy_cmd=("${DEPLOY_CMD_PREFIX[@]}") local i="set" # Issue certificates for domain in $domain_group; do # we want to split on whitespace [[ "$domain" == "" ]] && continue if [[ -v GROUP ]]; then if [[ "$METHOD" == "webroot" && -v i ]]; then group_root=$(get_webroot "$domain") issue_cmd+=("-w" "$group_root") unset i fi # Append domains to issue command that we will call after the loop issue_cmd+=("-d" "$domain" "-d" "www.$domain") # Issue certificate for single domain else local -a issue_cmd=("${ISSUE_CMD_PREFIX[@]}") domain_root=$(get_webroot "$domain") issue_cmd+=("-d" "$domain" "-d" "www.$domain") [[ "$METHOD" == "webroot" ]] && issue_cmd+=("-w" "$domain_root") echo "Running:" "${issue_cmd[@]}" if ! "${issue_cmd[@]}"; then echo "Failed to issue certificate for domain: $domain" err=1 fi fi done # Issue certificate for group of domains if [[ -v GROUP ]]; then echo "Running:" "${issue_cmd[@]}" if ! "${issue_cmd[@]}"; then echo "Failed to issue certificate for domain group: $domain_group" err=1 fi fi # Deploy certificates one domain at a time for domain in $domain_group; do deploy_cmd=("${DEPLOY_CMD_PREFIX[@]}" "-d" "$domain") # I think we only need to deploy to the domain, not subdomains (e.g. www.) echo "Running:" "${deploy_cmd[@]}" if ! "${deploy_cmd[@]}"; then echo "Failed to deploy certificate for $domain" err=1 fi done done } main() { parse_input "$@" get_acme update_email command_prefixes load_domains [[ "$METHOD" == "dns" ]] && interactive_dns sanity_check issue_and_deploy_certs } main "$@" exit "${err:-0}"