#!/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 method # See README for more details # # Copyright 2020 Bryan Roessler # # USAGE # ./acme-cpanel-webroot.sh [OPTIONS] [FILES...] # # EXAMPLES # TESTING: ./acme-cpanel-webroot.sh --debug -e me@gmail.com multisites/flatwhitedesign.pw multisites/greengingermultisite.website # PRODUCTION: ./acme-cpanel-webroot.sh --force -e me@gmail.com multisites/flatwhitedesign.pw multisites/greengingermultisite.website # # TESTING: ./acme-cpanel-webroot.sh --debug -s multisites # PRODUCTION: ./acme-cpanel-webroot.sh --force -s multisites # # FILES is a list of files containing first-level DOMAIN names (see domains.txt) on newlines # Certificates will automatically be issued and deployed for DOMAIN and www.DOMAIN using the webroot method # # NOTE: The webroot method does NOT support wildcard domains, Let's Encrypt requires wildcard domains to # use DNS challenges, which the CPANEL uapi does not support (use dns_cpaneldns plugin instead) 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 parse_input() { local input declare -g USEREMAIL declare -ag DOMAIN_FILES if input=$(getopt -o +e:fks:d -l email:,force,keep-grouping,sites-dir:,debug -- "$@"); then eval set -- "$input" while true; do case "$1" in --email|-e) shift USEREMAIL="$1" ;; --force|-f) unset DEBUG ;; --keep-grouping|-k) 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 } get_acme() { curl https://get.acme.sh | sh source "$HOME/.bashrc" "$HOME/.acme.sh/acme.sh" --upgrade --auto-upgrade } update_email() { [[ -v USEREMAIL ]] && "$HOME/.acme.sh/acme.sh" --update-account --accountemail "${USEREMAIL}"; } command_prefixes() { declare -ag ISSUE_CMD_PREFIX DEPLOY_CMD_PREFIX ISSUE_CMD_PREFIX=("$HOME/.acme.sh/acme.sh" "--issue" "--force") [[ -v DEBUG ]] && ISSUE_CMD_PREFIX=("$HOME/.acme.sh/acme.sh" "--issue" "--staging") DEPLOY_CMD_PREFIX=("$HOME/.acme.sh/acme.sh" "--deploy" "--deploy-hook" "cpanel_uapi") [[ -v DEBUG ]] && DEPLOY_CMD_PREFIX=("$HOME/.acme.sh/acme.sh" "--deploy" "--deploy-hook" "cpanel_uapi") } # 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 } 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" } issue_and_deploy_certs() { local domain_root domain domain_group local -a issue_cmd=() local -a deploy_cmd=() if [[ -v GROUP ]]; then for domain_group in "${DOMAIN_GROUPS[@]}"; do unset i for domain in $domain_group; do # we want to split on whitespace [[ "$domain" == "" ]] && continue # Get the webroot from the first domain if [[ ! -v i ]]; then local i="set" domain_root=$(get_webroot "$domain") issue_cmd=("${ISSUE_CMD_PREFIX[@]}" "-w" "$domain_root") fi issue_cmd+=("-d" "$domain" "-d" "www.$domain") done # Issue certificate for entire domain group echo "Running:" "${issue_cmd[@]}" "${issue_cmd[@]}" # Deploy certificates one by one for domain in $domain_group; do deploy_cmd=("${DEPLOY_CMD_PREFIX[@]}" "-w" "$domain_root" "-d" "$domain") echo "Running:" "${deploy_cmd[@]}" "${deploy_cmd[@]}" done done else for domain_group in "${DOMAIN_GROUPS[@]}"; do # Issue and deploy certificates one by one for domain in $domain_group; do # we want to split on whitespace domain_root=$(get_webroot "$domain") issue_cmd=("${ISSUE_CMD_PREFIX[@]}" "-w" "$domain_root" "-d" "$domain" "-d" "www.$domain") deploy_cmd=("${DEPLOY_CMD_PREFIX[@]}" "-w" "$domain_root" "-d" "$domain") # I think we only need to deploy to the domain, not subdomains echo "Running:" "${issue_cmd[@]}" if ! "${issue_cmd[@]}"; then echo "Certificate issue failed for $domain" exit_err=1 fi echo "Running:" "${deploy_cmd[@]}" if ! "${deploy_cmd[@]}"; then echo "Certificate deployment failed for $domain" exit_err=1 fi done done fi } main() { parse_input "$@" get_acme update_email command_prefixes load_domains issue_and_deploy_certs } main "$@" exit "${exit_err:-0}"