192 lines
6.1 KiB
Bash
192 lines
6.1 KiB
Bash
#!/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 <domain>_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>/
|
|
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 <<EOF
|
|
[Unit]
|
|
Description=acme.sh certificate renewal
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Service]
|
|
Type=oneshot
|
|
User=${ACME_USER}
|
|
Environment=HOME=/home/${ACME_USER}
|
|
Environment=CF_Token=${DNS_TOKEN}
|
|
ExecStart=${ACME_HOME}/acme.sh --cron --home ${ACME_HOME}
|
|
EOF
|
|
|
|
cat > /etc/systemd/system/acme-certd.timer <<EOF
|
|
[Unit]
|
|
Description=acme.sh renewal timer
|
|
|
|
[Timer]
|
|
OnBootSec=5min
|
|
OnUnitActiveSec=${ACME_RENEWAL_CHECK}
|
|
Persistent=true
|
|
|
|
[Install]
|
|
WantedBy=timers.target
|
|
EOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable --now acme-certd.timer
|
|
bold "==> 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
|