[__letsencrypt_cert] Fix various issues with hooks.

Closes #853, see issue for full description / discussion.

Short summary:
- There was about 6.53% chances of `--renewal-hook` not being applied
- Using --automatic-renewal in one cert and not in another was an error.
- It was not possible to use different hooks for different certificates.
- FreeBSD support was utterly broken.
This commit is contained in:
evilham 2021-02-09 19:58:47 +01:00
parent 65a6a2ed52
commit bc145bbc27
5 changed files with 220 additions and 42 deletions

View file

@ -1,16 +1,33 @@
cdist-type__letsencrypt_cert(7)
===============================
NAME
----
cdist-type__letsencrypt_cert - Get an SSL certificate from Let's Encrypt
DESCRIPTION
-----------
Automatically obtain a Let's Encrypt SSL certificate using Certbot.
This type attempts to setup automatic renewals always. In many Linux
distributions, that is the case out of the box, see:
https://certbot.eff.org/docs/using.html#automated-renewals
For Alpine Linux and Arch Linux, we setup a system-wide cronjob that
attempts to renew certificates daily.
If you are using FreeBSD, we configure periodic(8) as recommended by
the port mantainer, so there will be a weekly attempt at renewal.
If your OS is not mentioned here or on Certbot's docs as having
support for automated renewals, please make sure you check your OS
and possibly patch this type so the system-wide cronjob is installed.
REQUIRED PARAMETERS
-------------------
@ -21,6 +38,7 @@ object id
admin-email
Where to send Let's Encrypt emails like "certificate needs renewal".
OPTIONAL PARAMETERS
-------------------
@ -36,25 +54,68 @@ webroot
The path to your webroot, as set up in your webserver config. If this
parameter is not present, Certbot will be run in standalone mode.
OPTIONAL MULTIPLE PARAMETERS
----------------------------
renew-hook
Renew hook command directly passed to Certbot in cron job.
domain
Domains to be included in the certificate. When specified then object id
is not used as a domain.
deploy-hook
Command to be executed only when the certificate associated with this
``$__object_id`` is issued or renewed.
You can specify it multiple times, but any failure will prevent further
commands from being executed.
For this command, the
shell variable ``$RENEWED_LINEAGE`` will point to the
config live subdirectory (for example,
``/etc/letsencrypt/live/${__object_id}``) containing the
new certificates and keys; the shell variable
``$RENEWED_DOMAINS`` will contain a space-delimited list
of renewed certificate domains (for example,
``example.com www.example.com``)
pre-hook
Command to be run in a shell before obtaining any
certificates.
You can specify it multiple times, but any failure will prevent further
commands from being executed.
Note these run regardless of which certificate is attempted, you may want to
manage these system-wide hooks with ``__file`` in
``/etc/letsencrypt/renewal-hooks/pre/``.
Intended primarily for renewal, where it
can be used to temporarily shut down a webserver that
might conflict with the standalone plugin. This will
only be called if a certificate is actually to be
obtained/renewed.
post-hook
Command to be run in a shell after attempting to
obtain/renew certificates.
You can specify it multiple times, but any failure will prevent further
commands from being executed.
Note these run regardless of which certificate was attempted, you may want to
manage these system-wide hooks with ``__file`` in
``/etc/letsencrypt/renewal-hooks/post/``.
Can be used to deploy
renewed certificates, or to restart any servers that
were stopped by --pre-hook. This is only run if an
attempt was made to obtain/renew a certificate.
BOOLEAN PARAMETERS
------------------
automatic-renewal
Install a cron job, which attempts to renew certificates daily.
staging
Obtain a test certificate from a staging server.
MESSAGES
--------
@ -67,6 +128,7 @@ create
remove
Certificate was removed.
EXAMPLES
--------
@ -75,8 +137,7 @@ EXAMPLES
# use object id as domain
__letsencrypt_cert example.com \
--admin-email root@example.com \
--automatic-renewal \
--renew-hook "service nginx reload" \
--deploy-hook "service nginx reload" \
--webroot /data/letsencrypt/root
.. code-block:: sh
@ -85,11 +146,10 @@ EXAMPLES
# and example.com needs to be included again with domain parameter
__letsencrypt_cert example.com \
--admin-email root@example.com \
--automatic-renewal \
--domain example.com \
--domain foo.example.com \
--domain bar.example.com \
--renew-hook "service nginx reload" \
--deploy-hook "service nginx reload" \
--webroot /data/letsencrypt/root
AUTHORS
@ -99,11 +159,13 @@ AUTHORS
| Kamila Součková <kamila--@--ksp.sk>
| Darko Poljak <darko.poljak--@--gmail.com>
| Ľubomír Kučera <lubomir.kucera.jr at gmail.com>
| Evilham <contact@evilham.com>
COPYING
-------
Copyright \(C) 2017-2018 Nico Schottelius, Kamila Součková, Darko Poljak and
Copyright \(C) 2017-2021 Nico Schottelius, Kamila Součková, Darko Poljak and
Ľubomír Kučera. You can redistribute it and/or modify it under the terms of
the GNU General Public License as published by the Free Software Foundation,
either version 3 of the License, or (at your option) any later version.

149
cdist/conf/type/__letsencrypt_cert/manifest Executable file → Normal file
View file

@ -1,11 +1,13 @@
#!/bin/sh
certbot_fullpath="$(cat "${__object:?}/explorer/certbot-path")"
state=$(cat "${__object}/parameter/state")
os="$(cat "${__global:?}/explorer/os")"
if [ -z "${certbot_fullpath}" ]; then
os="$(cat "${__global:?}/explorer/os")"
os_version="$(cat "${__global}/explorer/os_version")"
# Use this, very common value, as a default. It is OS-dependent
certbot_fullpath="/usr/bin/certbot"
case "$os" in
archlinux)
__package certbot
@ -48,8 +50,6 @@ if [ -z "${certbot_fullpath}" ]; then
exit 1
;;
esac
certbot_fullpath=/usr/bin/certbot
;;
devuan)
case "$os_version" in
@ -83,13 +83,10 @@ if [ -z "${certbot_fullpath}" ]; then
exit 1
;;
esac
certbot_fullpath=/usr/bin/certbot
;;
freebsd)
__package py27-certbot
certbot_fullpath=/usr/local/bin/certbot
__package py37-certbot
certbot_fullpath="/usr/local/bin/certbot"
;;
ubuntu)
__package certbot
@ -101,18 +98,130 @@ if [ -z "${certbot_fullpath}" ]; then
esac
fi
if [ -f "${__object}/parameter/automatic-renewal" ]; then
renew_hook_param="${__object}/parameter/renew-hook"
renew_hook=""
if [ -f "${renew_hook_param}" ]; then
while read -r hook; do
renew_hook="${renew_hook} --renew-hook \"${hook}\""
done < "${renew_hook_param}"
fi
# Other OS-dependent values that we want to set every time
LE_DIR="/etc/letsencrypt"
certbot_cronjob_state="absent"
case "$os" in
archlinux|alpine)
certbot_cronjob_state="present"
;;
freebsd)
LE_DIR="/usr/local/etc/letsencrypt"
# FreeBSD uses periodic(8) instead of crontabs for this
__line "periodic.conf_weekly_certbot" \
--file "/etc/periodic.conf" \
--regex "^(#[[:space:]]*)?weekly_certbot_enable=.*" \
--state "replace" \
--line 'weekly_certbot_enable="YES"'
;;
*)
;;
esac
__cron letsencrypt-certbot \
# This is only necessary in certain OS
__cron letsencrypt-certbot \
--user root \
--command "${certbot_fullpath} renew -q ${renew_hook}" \
--command "${certbot_fullpath} renew -q" \
--hour 0 \
--minute 47
--minute 47 \
--state "${certbot_cronjob_state}"
# Ensure hook directories
HOOKS_DIR="${LE_DIR}/renewal-hooks"
__directory "${LE_DIR}" --mode 0755
require="__directory/${LE_DIR}" __directory "${HOOKS_DIR}" --mode 0755
if [ -f "${__object}/parameter/domain" ]; then
domains="$(sort "${__object}/parameter/domain")"
else
domains="${__object_id}"
fi
# Install hooks as needed
for hook in deploy pre post; do
# Using something unique and specific to this object
hook_file="${HOOKS_DIR}/${hook}/${__object_id}.cdist.sh"
# Reasonable defaults
hook_source="${__object}/parameter/${hook}-hook"
hook_state="absent"
hook_contents_head="#!/bin/sh -e"
hook_contents_logic=""
hook_contents_tail=""
# Backwards compatibility
# Remove this when renew-hook is removed
# Falling back to renew-hook if deploy-hook is not passed
if [ "${hook}" = "deploy" ] && [ ! -f "${hook_source}" ]; then
hook_source="${__object}/parameter/renew-hook"
fi
if [ "${state}" = "present" ] && \
[ -f "${hook_source}" ]; then
# This hook is to be installed, let's generate it with some
# safety boilerplate
# Since certbot runs all hooks for all renewal processes
# (at each state for deploy, pre, post), it is up to us to
# differentiate whether or not the hook must run
hook_state="present"
hook_contents_head="$(cat <<EOF
#!/bin/sh -e
#
# Managed remotely with https://cdi.st
#
# Domains for which this hook is supposed to apply
lineage="${LE_DIR}/live/${__object_id}"
domains="\$(cat <<eof
${domains}
eof
)"
EOF
)"
case "${hook}" in
pre|post)
# Certbot is kind of terrible, we have
# no way of knowing what domain/lineage the
# hook is running for
hook_contents_logic="$(cat <<EOF
# pre/post-hooks apply always due to a certbot limitation
APPLY_HOOK="YES"
EOF
)"
;;
deploy)
hook_contents_logic="$(cat <<EOF
# certbot defines these:
# RENEWED_DOMAINS: DOMAIN1,DOMAIN2
# RENEWED_LINEAGE: /etc/letsencrypt/live/__object_id
# It feels more stable to use RENEWED_LINEAGE
if [ "\${lineage}" = "\${RENEWED_LINEAGE}" ]; then
APPLY_HOOK="YES"
fi
EOF
)"
;;
*)
echo "Unknown hook '${hook}'" >> /dev/stderr
exit 1
;;
esac
hook_contents_tail="$(cat <<EOF
if [ -n "\${APPLY_HOOK}" ]; then
$(sed -e 's/^/ //' "${hook_source}")
fi
EOF
)"
fi
# Ensure hook directory exists
require="__directory/${HOOKS_DIR}" __directory "${HOOKS_DIR}/${hook}" \
--mode 0755
require="__directory/${HOOKS_DIR}/${hook}" __file "${hook_file}" \
--mode 0555 \
--source '-' \
--state "${hook_state}" <<EOF
${hook_contents_head}
${hook_contents_logic}
${hook_contents_tail}
EOF
done

View file

@ -0,0 +1,2 @@
Deprecated in favour of consistent behaviour. It has no effect, see:
https://code.ungleich.ch/ungleich-public/cdist/-/issues/853

View file

@ -0,0 +1,2 @@
This parameter has been deprecated in favour of --deploy-hook.
See: https://code.ungleich.ch/ungleich-public/cdist/-/issues/853

View file

@ -1,2 +1,5 @@
deploy-hook
domain
post-hook
pre-hook
renew-hook