commit 73410ccfa711855fec05dfbc819ac7e1fc96ed51 Author: bryan Date: Thu Jan 29 10:53:12 2026 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..32d5a4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.old/ +multisites/flatwhitedesign.pw +multisites/greengingerdesign.pw +multisites/greengingerdesign.space +multisites/greengingermultisite.website + diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b49c25 --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# WIP + +`acme-cpanel.sh` reads in a list of domains from one or more files. These files may only contain domains and empty lines (see `domains.txt` for example format). + +"*www.*" subdomains will be added automatically (do not add them to the domains file list). + +## Notes + +The `--method webroot` may require the following additions to .htaccess so that challenges are not automatically redirected to https: + +```text +RewriteCond %{REQUEST_FILENAME} !-f +RewriteRule ^\.well-known/.+ - [END] +``` + +## Installation + +Command-line (Linux): + +* Move script to user home directory on the server: `scp ./* username@ip:port:~` +* Log in to server: `ssh user@ip -p port` +* Make script executable: `chmod +x $HOME/acme-cpanel.sh` +* Run script (ex. `$HOME/acme-cpanel.sh -s multisites`) +* Follow prompts to enter credentials, issue certificates, and deploy them +* Double-check that the acme cron job is enabled: `crontab -l` + +cPanel: + +* Use File Manager to upload files to the home directory (/home/username/) +* You may need to make file executable in Terminal: `chmod +x $HOME/acme-cpanel.sh` +* Use Terminal to run the script (ex. `$HOME/acme-cpanel.sh -s multisites`) +* Follow prompts to enter credentials, issue certificates, and deploy them +* Use Cron Jobs app to double-check that the acme cron job is present + +## Usage + +#### `./acme-cpanel.sh [OPTIONS] [FILES...]` + +#### Options + +```text +--method, -m dns,webroot + Choose the authentication method (default: dns) +--email, -e EMAIL + E-mail to be notified of certificate renewal failures +--group-by-file, -g + Issue multidomain certificates for all domains with the same webroot, grouped by input file + The first domain in each file will be used to determine the shared webroot +--sites-dir, -s DIR + Load domain list files from this directory +--force, -f + Override default debug +--debug, -d (default) + Use --staging to issue certificates and do not deploy +``` + +#### Examples + +`./acme-cpanel.sh --force` + +Load all sites from domains.txt, issue and deploy certificates using the webroot method + +`./acme-cpanel.sh --method dns --force -s multisites` + +Load sites from multisites directory, issue and deploy certificates using the dns method + +`./acme-cpanel.sh --force -g multisites/site1.org multisites/site2.org` + +Load sites from multisites directory, issue and deploy multidomain certificates with same webroot based on the grouping in the file using the webroot method diff --git a/acme-cpanel.sh b/acme-cpanel.sh new file mode 100644 index 0000000..4f24ce6 --- /dev/null +++ b/acme-cpanel.sh @@ -0,0 +1,225 @@ +#!/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 +# MIT Licensed + +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") # only deploy to the domain, not subdomains + 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}" \ No newline at end of file diff --git a/domains.txt b/domains.txt new file mode 100644 index 0000000..4f13ca6 --- /dev/null +++ b/domains.txt @@ -0,0 +1,2 @@ +multisite1.org +multisite2.org diff --git a/multisites/multisite1.org b/multisites/multisite1.org new file mode 100644 index 0000000..b1c0f50 --- /dev/null +++ b/multisites/multisite1.org @@ -0,0 +1,15 @@ +example1.com +testsite.net +demo-website.org +myproject.io +sampleapp.dev +placeholder-domain.com +fictional-business.co +random-site.online +test-company.tech +mock-website.app +example-store.shop +demo-blog.info +sample-portal.xyz +fake-domain.club +test-platform.digital \ No newline at end of file diff --git a/multisites/multisite2.org b/multisites/multisite2.org new file mode 100644 index 0000000..aca5073 --- /dev/null +++ b/multisites/multisite2.org @@ -0,0 +1,20 @@ +example1.com +testsite.net +mydomain.org +samplewebsite.io +demopage.co +fictionalsite.dev +placeholder.app +mockwebsite.online +exampledomain.xyz +testproject.com +randomsite.net +demobusiness.org +fakecompany.io +samplestore.shop +placeholdersite.com +myexample.net +testingdomain.org +dummysite.co +mockbrand.com +virtualsite.net \ No newline at end of file