#!/bin/bash # install-acme_certd.sh — taskserv installer for acme.sh + systemd timer. # Runs on the target Debian/Ubuntu host as root. Idempotent. # # Inputs (env file scp'd alongside this script): # ACME_SH_VERSION — acme.sh release tag (e.g. 3.0.9) # ACME_SERVER — letsencrypt | letsencrypt_test | zerossl # ACME_EMAIL — ACME account email # ACME_DNS_PROVIDER — acme.sh DNS plugin (e.g. dns_cf) # ACME_DOMAINS — comma-separated domain list (e.g. "*.librecloud.online,librecloud.online") # ACME_CERT_DIR — where to install issued certs (default /etc/ssl/acme) # ACME_RENEWAL_CHECK — systemd timer interval (default 12h) # ACME_USER — OS user to run acme.sh (default acme) # ACME_GROUP — OS group (default acme) # ACME_RELOAD_HOOKS — semicolon-separated commands to run after renewal # DNS_TOKEN — DNS provider API token (resolved from SOPS by provisioning) set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" for f in env-acme_certd _credentials.env; do [ -f "${SCRIPT_DIR}/${f}" ] && source "${SCRIPT_DIR}/${f}" || true done : "${ACME_SH_VERSION:?ACME_SH_VERSION must be set}" : "${ACME_EMAIL:?ACME_EMAIL must be set}" : "${ACME_DNS_PROVIDER:?ACME_DNS_PROVIDER must be set}" : "${ACME_DOMAINS:?ACME_DOMAINS must be set}" : "${DNS_TOKEN:?DNS_TOKEN must be set}" ACME_SERVER="${ACME_SERVER:-letsencrypt}" ACME_CERT_DIR="${ACME_CERT_DIR:-/etc/ssl/acme}" ACME_RENEWAL_CHECK="${ACME_RENEWAL_CHECK:-12h}" ACME_USER="${ACME_USER:-acme}" ACME_GROUP="${ACME_GROUP:-acme}" ACME_RELOAD_HOOKS="${ACME_RELOAD_HOOKS:-}" ACME_HOME="/home/${ACME_USER}/.acme.sh" bold() { printf "\033[1m%s\033[0m\n" "$*"; } fatal() { printf "\033[31mFATAL:\033[0m %s\n" "$*" >&2; exit 1; } [ "$(id -u)" -eq 0 ] || fatal "must run as root" [ "$ACME_EMAIL" != "UNSET" ] || fatal "ACME_EMAIL is sentinel 'UNSET'" [ "$DNS_TOKEN" != "UNSET" ] || fatal "DNS_TOKEN is sentinel 'UNSET'" ACTION="${1:-install}" _acme() { sudo -u "${ACME_USER}" \ env HOME="/home/${ACME_USER}" \ CF_Token="${DNS_TOKEN}" \ "${ACME_HOME}/acme.sh" "$@" } _server_url() { case "$ACME_SERVER" in letsencrypt) echo "https://acme-v02.api.letsencrypt.org/directory" ;; letsencrypt_test) echo "https://acme-staging-v02.api.letsencrypt.org/directory" ;; zerossl) echo "https://acme.zerossl.com/v2/DV90" ;; *) echo "$ACME_SERVER" ;; esac } cmd_install() { bold "==> [1/5] System packages" export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get install -y -qq curl socat openssl cron bold "==> [2/5] OS user ${ACME_USER}" if ! id "${ACME_USER}" &>/dev/null; then useradd --system --create-home --shell /bin/bash "${ACME_USER}" echo " created user ${ACME_USER}" else echo " user ${ACME_USER} already exists" fi bold "==> [3/5] Install acme.sh ${ACME_SH_VERSION}" if [ -x "${ACME_HOME}/acme.sh" ] && \ "${ACME_HOME}/acme.sh" --version 2>/dev/null | grep -q "${ACME_SH_VERSION}"; then echo " acme.sh ${ACME_SH_VERSION} already installed" else TMP=$(sudo -u "${ACME_USER}" mktemp -d) curl -fsSL "https://github.com/acmesh-official/acme.sh/archive/refs/tags/${ACME_SH_VERSION}.tar.gz" \ | sudo -u "${ACME_USER}" tar -xz -C "${TMP}" --strip-components=1 sudo -u "${ACME_USER}" \ env HOME="/home/${ACME_USER}" \ bash -c "cd '${TMP}' && bash acme.sh --install \ --home '${ACME_HOME}' \ --accountemail '${ACME_EMAIL}' \ --server '$(_server_url)' \ --no-cron" rm -rf "${TMP}" echo " installed acme.sh ${ACME_SH_VERSION}" fi bold "==> [4/5] Issue certificates" install -d -m 0755 -o "${ACME_USER}" -g "${ACME_GROUP}" "${ACME_CERT_DIR}" IFS=',' read -ra DOMAIN_LIST <<< "${ACME_DOMAINS}" DOMAIN_ARGS="" for d in "${DOMAIN_LIST[@]}"; do DOMAIN_ARGS="${DOMAIN_ARGS} -d ${d}" done # Build reload hook args HOOK_ARGS="" if [ -n "${ACME_RELOAD_HOOKS}" ]; then IFS=';' read -ra HOOKS <<< "${ACME_RELOAD_HOOKS}" HOOK_CMD="${HOOKS[*]}" HOOK_ARGS="--reloadcmd \"${HOOK_CMD}\"" fi # Issue or renew — idempotent. --ecc forces ECC key (P-256); cert stored under _ecc/. eval _acme --issue \ --dns "${ACME_DNS_PROVIDER}" \ --ecc \ ${DOMAIN_ARGS} \ --cert-home "${ACME_CERT_DIR}" \ --server "$(_server_url)" \ ${HOOK_ARGS} \ --log || true # exit 2 = already up to date, not an error # Install certs to cert_dir// PRIMARY="${DOMAIN_LIST[0]//\*/wildcard}" CERT_TARGET="${ACME_CERT_DIR}/${PRIMARY}" install -d -m 0750 -o "${ACME_USER}" -g "${ACME_GROUP}" "${CERT_TARGET}" eval _acme --install-cert \ --ecc \ --cert-home "${ACME_CERT_DIR}" \ -d "${DOMAIN_LIST[0]}" \ --cert-file "${CERT_TARGET}/cert.pem" \ --key-file "${CERT_TARGET}/key.pem" \ --fullchain-file "${CERT_TARGET}/fullchain.pem" \ ${HOOK_ARGS} bold "==> [5/5] systemd timer (${ACME_RENEWAL_CHECK})" cat > /etc/systemd/system/acme-certd.service < /etc/systemd/system/acme-certd.timer < Done" systemctl list-timers acme-certd.timer --no-pager } cmd_status() { systemctl status acme-certd.timer --no-pager echo "" _acme --list } cmd_uninstall() { systemctl disable --now acme-certd.timer 2>/dev/null || true rm -f /etc/systemd/system/acme-certd.{service,timer} systemctl daemon-reload echo "uninstalled (certs in ${ACME_CERT_DIR} and acme.sh in ${ACME_HOME} preserved)" } case "$ACTION" in install) cmd_install ;; status) cmd_status ;; uninstall) cmd_uninstall ;; *) fatal "unknown action: $ACTION (install|status|uninstall)" ;; esac