provisioning-catalog/components/acme_certd/taskserv/install-acme_certd.sh

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