Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save aont/a973f0606e58a387d8d4e31b11851c47 to your computer and use it in GitHub Desktop.

Select an option

Save aont/a973f0606e58a387d8d4e31b11851c47 to your computer and use it in GitHub Desktop.

Automating TLS Certificate Renewal with systemd and certbot

[Unit]
Description=cert & deploy
After=multi-user.target
[Service]
Type=simple
ExecStart=/usr/libexec/acmebot-certbot/cert-deploy.sh
EnvironmentFile=/etc/acmebot-certbot/env
PrivateTmp=true
[Install]
WantedBy=multi-user.target
#!/bin/bash
# Must run as root
if (( EUID != 0 )); then
echo "This script must be run as root. Exiting."
exit 1
fi
set -xe
set -o pipefail
FLAGFILE="/run/acmebot-certbot/deploy-flag.$(date "+%Y%m%d%H%M%S%N")"
CERTPATH="$CONFIG_PATH/live/$CERT_NAME"
local_ip=""
cleanup() {
# Always close port even on failure
echo "[info] upnp port close" 1>&2
sudo -u"$EXECUTE_USER" -H upnpc -d 80 tcp || true
}
trap cleanup EXIT
echo "[info] getting local address (ip -j route get + jq)" 1>&2
# ip -j route get 1.1.1.1 returns a JSON array; take the first element's "prefsrc"
local_ip="$(ip -j route get 1.1.1.1 | jq -r '.[0].prefsrc // empty')"
if [ -z "$local_ip" ] || [ "$local_ip" = "null" ]; then
echo "[error] failed to detect local_ip via ip -j route get" 1>&2
exit 1
fi
echo "[info] local_ip=${local_ip}" 1>&2
# NOTE: This opens WAN:80 -> LAN:80. If you intend WAN:80 -> LAN:8080, change the first "80" to "8080".
echo "[info] upnp port open (WAN 80 -> LAN 80)" 1>&2
sudo -u"$EXECUTE_USER" -H upnpc -a "${local_ip}" 80 80 tcp 600
echo "[info] certbot (webroot)" 1>&2
# Run certbot and capture exit code without aborting the script immediately
set +e
sudo -u "$EXECUTE_USER" -H \
"${CERTBOT_VENV}/bin/certbot" \
certonly -q --webroot -w "$WEB_ROOT" \
--cert-name "${CERT_NAME}" \
--key-type rsa --rsa-key-size 4096 \
-d "$DOMAINLIST" \
--agree-tos --no-eff-email --email "$EMAIL" \
--deploy-hook "touch '$FLAGFILE'" \
--config-dir "$CONFIG_PATH" \
--work-dir "$WORK_DIR" \
--logs-dir "$LOGS_DIR"
CERTBOT_STATUS=$?
set -e
if [ $CERTBOT_STATUS -ne 0 ]; then
echo "[warn] Certbot exited with code $CERTBOT_STATUS" 1>&2
exit $CERTBOT_STATUS
fi
if [ ! -e "$FLAGFILE" ]; then
echo "[info] cert not renewed. skip deploy/restart." 1>&2
exit 0
fi
echo "[info] cert renewed. proceed deploy/restart." 1>&2
rm -f "$FLAGFILE"
# Ensure cert files exist and are non-empty
for f in fullchain.pem privkey.pem chain.pem; do
if [ ! -s "${CERTPATH}/${f}" ]; then
echo "[error] missing or empty ${CERTPATH}/${f}" 1>&2
exit 2
fi
done
echo "[info] put certs" 1>&2
# nginx
cp -t "${NGINX_CERTS}" "${CERTPATH}/fullchain.pem" "${CERTPATH}/privkey.pem"
# strongswan
cp "${CERTPATH}/privkey.pem" "${IPSEC_ETC}"/private/privkey.pem
cp "${CERTPATH}/chain.pem" "${IPSEC_ETC}"/cacerts/chain.pem
cp "${CERTPATH}/fullchain.pem" "${IPSEC_ETC}"/certs/fullchain.pem
echo "[info] nginx config test" 1>&2
nginx -t
echo "[info] restart nginx and strongswan" 1>&2
systemctl restart nginx
systemctl restart strongswan-starter
[Unit]
Description=Run certbot_update_cert periodically
[Timer]
OnCalendar=*-*-* 00:00:00
OnCalendar=*-*-* 12:00:00
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
EXECUTE_USER=acmebot
ETC_DIR=/etc/acmebot-certbot
LIBEXEC_DIR=/usr/libexec/acmebot-certbot
SYSTEMD_DIR=/etc/systemd/system
RUN_DIR=/run/acmebot-certbot
CERT_NAME=hoge.ddns.net
DOMAINLIST=hoge.ddns.net
EMAIL=hoge@mail.com
CERTBOT_VENV=/usr/libexec/acmebot-certbot/venv
CONFIG_PATH=/var/lib/acmebot-certbot/config
WORK_DIR=/run/acmebot-certbot/work
LOGS_DIR=/var/log/acmebot-certbot
WEB_ROOT=/var/lib/acmebot-certbot/webroot
NGINX_CERTS=/etc/nginx/pki/certs
IPSEC_ETC=/etc/ipsec.d
#!/bin/bash
# Must run as root
if (( EUID != 0 )); then
echo "[error] This script must be run as root. Exiting."
exit 1
fi
set -xe
missing=()
# Check if `ip -j address` runs successfully
if ! ip -j address >/dev/null 2>&1; then
missing+=("ip -j address (iproute2)")
fi
# Check if upnpc exists
if ! command -v upnpc >/dev/null 2>&1; then
missing+=("upnpc")
fi
# Check if jq exists
if ! command -v jq >/dev/null 2>&1; then
missing+=("jq")
fi
# Check if python3 exists
if ! command -v python3 >/dev/null 2>&1; then
missing+=("python3")
fi
# Final check
if [ "${#missing[@]}" -ne 0 ]; then
echo "The following requirements are missing or not working:"
for item in "${missing[@]}"; do
echo " - $item"
done
exit 1
fi
. ./env
mkdir -p "$ETC_DIR"
cp -t "$ETC_DIR" ./env
mkdir -p "${RUN_DIR}"
chown "${EXECUTE_USER}" "${RUN_DIR}"
mkdir -p "${LIBEXEC_DIR}"
install --mode=0755 cert-deploy.sh "${LIBEXEC_DIR}"
if [[ ! -x "${CERTBOT_VENV}/bin/certbot" ]]; then
if [[ ! -x "${CERTBOT_VENV}/bin/python" ]]; then
if [[ ! -d "${CERTBOT_VENV}" ]]; then
mkdir -p "${CERTBOT_VENV}"
fi
python3 -m venv "${CERTBOT_VENV}"
fi
"${CERTBOT_VENV}/bin/pip" install cffi==1.17.1 certbot
fi
mkdir -p "${CONFIG_PATH}"
chown "${EXECUTE_USER}" "${CONFIG_PATH}"
mkdir -p "${WORK_DIR}"
chown "${EXECUTE_USER}" "${WORK_DIR}"
mkdir -p "${LOGS_DIR}"
chown "${EXECUTE_USER}" "${LOGS_DIR}"
mkdir -p --mode=0755 "${WEB_ROOT}"
chown "${EXECUTE_USER}" "${WEB_ROOT}"
install --mode=0644 cert-deploy.service "${SYSTEMD_DIR}"
install --mode=0644 cert-deploy.timer "${SYSTEMD_DIR}"
systemctl daemon-reload
systemctl enable cert-deploy.timer
systemctl start cert-deploy.timer
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment