From bc145bbc27a45e382adb744e6774fb803f4fda0a Mon Sep 17 00:00:00 2001 From: Evilham Date: Tue, 9 Feb 2021 19:58:47 +0100 Subject: [PATCH] [__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. --- cdist/conf/type/__letsencrypt_cert/man.rst | 84 +++++++-- cdist/conf/type/__letsencrypt_cert/manifest | 171 ++++++++++++++---- .../parameter/deprecated/automatic-renewal | 2 + .../parameter/deprecated/renew-hook | 2 + .../parameter/optional_multiple | 3 + 5 files changed, 220 insertions(+), 42 deletions(-) mode change 100755 => 100644 cdist/conf/type/__letsencrypt_cert/manifest create mode 100644 cdist/conf/type/__letsencrypt_cert/parameter/deprecated/automatic-renewal create mode 100644 cdist/conf/type/__letsencrypt_cert/parameter/deprecated/renew-hook diff --git a/cdist/conf/type/__letsencrypt_cert/man.rst b/cdist/conf/type/__letsencrypt_cert/man.rst index 85eb88ea..43be8424 100644 --- a/cdist/conf/type/__letsencrypt_cert/man.rst +++ b/cdist/conf/type/__letsencrypt_cert/man.rst @@ -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á | Darko Poljak | Ľubomír Kučera +| Evilham + 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. diff --git a/cdist/conf/type/__letsencrypt_cert/manifest b/cdist/conf/type/__letsencrypt_cert/manifest old mode 100755 new mode 100644 index b4464366..9c8cc043 --- a/cdist/conf/type/__letsencrypt_cert/manifest +++ b/cdist/conf/type/__letsencrypt_cert/manifest @@ -1,18 +1,20 @@ #!/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 - ;; - alpine) - __package certbot - ;; + archlinux) + __package certbot + ;; + alpine) + __package certbot + ;; debian) case "$os_version" in 8*) @@ -48,9 +50,7 @@ if [ -z "${certbot_fullpath}" ]; then exit 1 ;; esac - - certbot_fullpath=/usr/bin/certbot - ;; + ;; devuan) case "$os_version" in jessie) @@ -83,17 +83,14 @@ 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 - ;; + __package certbot + ;; *) echo "Unsupported os: $os" >&2 exit 1 @@ -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 \ - --user root \ - --command "${certbot_fullpath} renew -q ${renew_hook}" \ - --hour 0 \ - --minute 47 +# This is only necessary in certain OS +__cron letsencrypt-certbot \ + --user root \ + --command "${certbot_fullpath} renew -q" \ + --hour 0 \ + --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 <> /dev/stderr + exit 1 + ;; + esac + + hook_contents_tail="$(cat <