From 22d570ae6084710d7af60dcd36ad9f0ec8ea882c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=BDubom=C3=ADr=20Ku=C4=8Dera?= Date: Mon, 7 May 2018 12:57:48 +0200 Subject: [PATCH] Rewrite __letsencrypt_cert type This commit adds the following features: * Ability to expand existing certificate * Ability to manage object state * Ability to obtain test certificate * Ability to promote test certificate to production * Ability to specify custom certificate name * Ability to specify multiple domains per certificate * Ability to use Certbot in standalone mode * Messaging This commit also introduces the following behavioral changes: * Attempt to install Certbot only when it is not installed already * Installation of the cron job has to be enabled using `--automatic-renewal` parameter **Note:** Object ID is now treated as certificate name and new required parameter `--domain` was added. --- .../__letsencrypt_cert/explorer/certbot-path | 3 + .../explorer/certificate-domains | 4 + .../explorer/certificate-exists | 7 + .../explorer/certificate-is-test | 8 + .../type/__letsencrypt_cert/explorer/exists | 5 - .../type/__letsencrypt_cert/gencode-remote | 85 ++++++++-- cdist/conf/type/__letsencrypt_cert/man.rst | 73 +++++++-- cdist/conf/type/__letsencrypt_cert/manifest | 152 ++++++++++-------- .../type/__letsencrypt_cert/parameter/boolean | 2 + .../parameter/default/state | 1 + .../parameter/default/webroot | 0 .../__letsencrypt_cert/parameter/optional | 2 + .../__letsencrypt_cert/parameter/required | 1 - .../parameter/required_multiple | 1 + 14 files changed, 239 insertions(+), 105 deletions(-) create mode 100755 cdist/conf/type/__letsencrypt_cert/explorer/certbot-path create mode 100755 cdist/conf/type/__letsencrypt_cert/explorer/certificate-domains create mode 100755 cdist/conf/type/__letsencrypt_cert/explorer/certificate-exists create mode 100755 cdist/conf/type/__letsencrypt_cert/explorer/certificate-is-test delete mode 100644 cdist/conf/type/__letsencrypt_cert/explorer/exists mode change 100644 => 100755 cdist/conf/type/__letsencrypt_cert/gencode-remote mode change 100644 => 100755 cdist/conf/type/__letsencrypt_cert/manifest create mode 100644 cdist/conf/type/__letsencrypt_cert/parameter/boolean create mode 100644 cdist/conf/type/__letsencrypt_cert/parameter/default/state create mode 100644 cdist/conf/type/__letsencrypt_cert/parameter/default/webroot create mode 100644 cdist/conf/type/__letsencrypt_cert/parameter/optional create mode 100644 cdist/conf/type/__letsencrypt_cert/parameter/required_multiple diff --git a/cdist/conf/type/__letsencrypt_cert/explorer/certbot-path b/cdist/conf/type/__letsencrypt_cert/explorer/certbot-path new file mode 100755 index 00000000..3c6076df --- /dev/null +++ b/cdist/conf/type/__letsencrypt_cert/explorer/certbot-path @@ -0,0 +1,3 @@ +#!/bin/sh -e + +command -v certbot 2>/dev/null || true diff --git a/cdist/conf/type/__letsencrypt_cert/explorer/certificate-domains b/cdist/conf/type/__letsencrypt_cert/explorer/certificate-domains new file mode 100755 index 00000000..367fda93 --- /dev/null +++ b/cdist/conf/type/__letsencrypt_cert/explorer/certificate-domains @@ -0,0 +1,4 @@ +#!/bin/sh -e + +certbot certificates --cert-name "${__object_id:?}" | grep ' Domains: ' | \ + cut -d ' ' -f 6- | tr ' ' '\n' diff --git a/cdist/conf/type/__letsencrypt_cert/explorer/certificate-exists b/cdist/conf/type/__letsencrypt_cert/explorer/certificate-exists new file mode 100755 index 00000000..d2ea35cc --- /dev/null +++ b/cdist/conf/type/__letsencrypt_cert/explorer/certificate-exists @@ -0,0 +1,7 @@ +#!/bin/sh -e + +if certbot certificates | grep -q " Certificate Name: ${__object_id:?}$"; then + echo yes +else + echo no +fi diff --git a/cdist/conf/type/__letsencrypt_cert/explorer/certificate-is-test b/cdist/conf/type/__letsencrypt_cert/explorer/certificate-is-test new file mode 100755 index 00000000..6d7b0ae9 --- /dev/null +++ b/cdist/conf/type/__letsencrypt_cert/explorer/certificate-is-test @@ -0,0 +1,8 @@ +#!/bin/sh -e + +if certbot certificates --cert-name "${__object_id:?}" | \ + grep -q 'INVALID: TEST_CERT'; then + echo yes +else + echo no +fi diff --git a/cdist/conf/type/__letsencrypt_cert/explorer/exists b/cdist/conf/type/__letsencrypt_cert/explorer/exists deleted file mode 100644 index cb967663..00000000 --- a/cdist/conf/type/__letsencrypt_cert/explorer/exists +++ /dev/null @@ -1,5 +0,0 @@ -domain=$__object_id - -if [ -f "/etc/letsencrypt/live/$domain/fullchain.pem" ]; then - echo yes -fi diff --git a/cdist/conf/type/__letsencrypt_cert/gencode-remote b/cdist/conf/type/__letsencrypt_cert/gencode-remote old mode 100644 new mode 100755 index 62ada241..4bd05eff --- a/cdist/conf/type/__letsencrypt_cert/gencode-remote +++ b/cdist/conf/type/__letsencrypt_cert/gencode-remote @@ -1,18 +1,75 @@ -domain="$__object_id" +#!/bin/sh -e -exists=$(cat "$__object/explorer/exists") -webroot="$(cat "$__object/parameter/webroot")" -admin_email="$(cat "$__object/parameter/admin-email")" +certificate_exists=$(cat "${__object:?}/explorer/certificate-exists") +name="${__object_id:?}" +state=$(cat "${__object}/parameter/state") -if [ -n "$exists" ]; then - exit 0 -fi +case "${state}" in + absent) + if [ "${certificate_exists}" = "no" ]; then + exit 0 + fi -cat <> "${__messages_out:?}" + ;; + present) + requested_domains="${__object}/parameter/domain" + + staging=no + if [ -f "${__object}/parameter/staging" ]; then + staging=yes + fi + + if [ "${certificate_exists}" = "yes" ]; then + existing_domains="${__object}/explorer/certificate-domains" + certificate_is_test=$(cat "${__object}/explorer/certificate-is-test") + + sort -uo "${requested_domains}" "${requested_domains}" + sort -uo "${existing_domains}" "${existing_domains}" + + if [ -z "$(comm -23 "${requested_domains}" "${existing_domains}")" ] && \ + [ "${certificate_is_test}" = "${staging}" ]; then + exit 0 + fi + fi + + admin_email="$(cat "$__object/parameter/admin-email")" + webroot="$(cat "$__object/parameter/webroot")" + + cat <<-EOF + certbot certonly \ + --agree-tos \ + --cert-name '${name}' \ + --email '${admin_email}' \ + --expand \ + --non-interactive \ + --quiet \ + $(if [ "${staging}" = "yes" ]; then + echo "--staging" + elif [ "${certificate_is_test}" != "${staging}" ]; then + echo "--force-renewal" + fi) \ + $(if [ -z "${webroot}" ]; then + echo "--standalone" + else + echo "--webroot --webroot-path '${webroot}'" + fi) \ + $(while read -r domain; do + echo "--domain '${domain}' \\" + done < "${requested_domains}") + EOF + + if [ "${certificate_exists}" = "no" ]; then + echo create >> "${__messages_out}" + else + echo change >> "${__messages_out}" + fi + ;; + *) + echo "Unsupported state: ${state}" >&2 + + exit 1 + ;; +esac diff --git a/cdist/conf/type/__letsencrypt_cert/man.rst b/cdist/conf/type/__letsencrypt_cert/man.rst index bb1e5d05..16b23fea 100644 --- a/cdist/conf/type/__letsencrypt_cert/man.rst +++ b/cdist/conf/type/__letsencrypt_cert/man.rst @@ -3,54 +3,95 @@ cdist-type__letsencrypt_cert(7) NAME ---- -cdist-type__letsencrypt_cert - Get an SSL certificate from Let's Encrypt +cdist-type__letsencrypt_cert - Get an SSL certificate from Let's Encrypt DESCRIPTION ----------- -Automatically obtain a Let's Encrypt SSL certificate. Uses certbot's webroot -method. You must set up your web server to work with webroot. +Automatically obtain a Let's Encrypt SSL certificate using Certbot. REQUIRED PARAMETERS ------------------- -webroot - The path to your webroot, as set up in your webserver config. admin-email - Where to send Let's Encrypt emails like "certificate needs renewal". + Where to send Let's Encrypt emails like "certificate needs renewal". +REQUIRED MULTIPLE PARAMETERS +---------------------------- + +domain + A domain to be included in the certificate. OPTIONAL PARAMETERS ------------------- -None. +state + 'present' or 'absent', defaults to 'present' where: + + present + if the certificate does not exist, it will be obtained + absent + the certificate will be removed + +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. + Renew hook command directly passed to Certbot in cron job. + +BOOLEAN PARAMETERS +------------------ + +automatic-renewal + Install a cron job, which attempts to renew certificates daily. + +staging + Obtain a test certificate from a staging server. + +MESSAGES +-------- + +change + Certificte was changed. + +create + Certificte was created. + +remove + Certificte was removed. EXAMPLES -------- .. code-block:: sh - __letsencrypt_cert example.com --admin-email root@example.com --webroot /data/letsencrypt/root - - __letsencrypt_cert example.com --admin-email root@example.com --webroot /data/letsencrypt/root --renew-hook "service nginx reload" + __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" \ + --webroot /data/letsencrypt/root AUTHORS ------- + | Nico Schottelius | Kamila Součková | Darko Poljak - +| Ľubomír Kučera COPYING ------- -Copyright \(C) 2017 Nico Schottelius, Kamila Součková, Darko Poljak. 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. + +Copyright \(C) 2017-2018 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 100644 new mode 100755 index 800e5e18..94e9d225 --- a/cdist/conf/type/__letsencrypt_cert/manifest +++ b/cdist/conf/type/__letsencrypt_cert/manifest @@ -1,79 +1,93 @@ -os=$(cat "$__global/explorer/os") -os_version=$(cat "$__global/explorer/os_version") +#!/bin/sh -case "$os" in - debian) - case "$os_version" in - 8*) - __apt_source jessie-backports \ - --uri http://http.debian.net/debian \ - --distribution jessie-backports \ - --component main +certbot_fullpath="$(cat "${__object:?}/explorer/certbot-path")" - require="__apt_source/jessie-backports" __package_apt python-certbot --target-release jessie-backports - require="__apt_source/jessie-backports" __package_apt certbot --target-release jessie-backports - # Seems to be a missing dependency on debian 8 - __package python-ndg-httpsclient - ;; - 9*) - __apt_source stretch-backports \ - --uri http://http.debian.net/debian \ - --distribution stretch-backports \ - --component main +if [ -z "${certbot_fullpath}" ]; then + os="$(cat "${__global:?}/explorer/os")" + os_version="$(cat "${__global}/explorer/os_version")" - require="__apt_source/stretch-backports" __package_apt python-certbot --target-release stretch-backports - require="__apt_source/stretch-backports" __package_apt certbot --target-release stretch-backports - ;; - *) - echo "Unsupported OS version: $os_version" >&2 - exit 1 - ;; - esac + case "$os" in + debian) + case "$os_version" in + 8*) + __apt_source jessie-backports \ + --uri http://http.debian.net/debian \ + --distribution jessie-backports \ + --component main - certbot_fullpath=/usr/bin/certbot - ;; - devuan) - case "$os_version" in - jessie) - __apt_source jessie-backports \ - --uri http://auto.mirror.devuan.org/merged \ - --distribution jessie-backports \ - --component main + require="__apt_source/jessie-backports" __package_apt python-certbot \ + --target-release jessie-backports + require="__apt_source/jessie-backports" __package_apt certbot \ + --target-release jessie-backports + # Seems to be a missing dependency on debian 8 + __package python-ndg-httpsclient + ;; + 9*) + __apt_source stretch-backports \ + --uri http://http.debian.net/debian \ + --distribution stretch-backports \ + --component main - require="__apt_source/jessie-backports" __package_apt python-certbot --target-release jessie-backports - require="__apt_source/jessie-backports" __package_apt certbot --target-release jessie-backports - # Seems to be a missing dependency on debian 8 - __package python-ndg-httpsclient - ;; - *) - echo "Unsupported OS version: $os_version" >&2 - exit 1 - ;; - esac + require="__apt_source/stretch-backports" __package_apt python-certbot \ + --target-release stretch-backports + require="__apt_source/stretch-backports" __package_apt certbot \ + --target-release stretch-backports + ;; + *) + echo "Unsupported OS version: $os_version" >&2 + exit 1 + ;; + esac - certbot_fullpath=/usr/bin/certbot - ;; - freebsd) - __package py27-certbot + certbot_fullpath=/usr/bin/certbot + ;; + devuan) + case "$os_version" in + jessie) + __apt_source jessie-backports \ + --uri http://auto.mirror.devuan.org/merged \ + --distribution jessie-backports \ + --component main - certbot_fullpath=/usr/local/bin/certbot - ;; - *) - echo "Unsupported os: $os" >&2 - exit 1 - ;; -esac + require="__apt_source/jessie-backports" __package_apt python-certbot \ + --target-release jessie-backports + require="__apt_source/jessie-backports" __package_apt certbot \ + --target-release jessie-backports + # Seems to be a missing dependency on debian 8 + __package python-ndg-httpsclient + ;; + *) + echo "Unsupported OS version: $os_version" >&2 + exit 1 + ;; + esac -renew_hook_param="$__object/parameter/renew-hook" -renew_hook="" -if [ -f "$renew_hook_param" ]; then - while read hook; do - renew_hook="$renew_hook --renew-hook \"$hook\"" - done < "$renew_hook_param" + certbot_fullpath=/usr/bin/certbot + ;; + freebsd) + __package py27-certbot + + certbot_fullpath=/usr/local/bin/certbot + ;; + *) + echo "Unsupported os: $os" >&2 + exit 1 + ;; + esac fi -__cron letsencrypt-certbot \ - --user root \ - --command "$certbot_fullpath renew -q $renew_hook" \ - --hour 0 \ - --minute 47 +if [ -f "${__object}/parameter/automatic-renewal" ]; then + renew_hook_param="${__object}/parameter/renew-hook" + renew_hook="" + if [ -f "${renew_hook_param}" ]; then + while read hook; do + renew_hook="${renew_hook} --renew-hook \"${hook}\"" + done < "${renew_hook_param}" + fi + + __cron letsencrypt-certbot \ + --user root \ + --command "${certbot_fullpath} renew -q ${renew_hook}" \ + --hour 0 \ + --minute 47 +fi diff --git a/cdist/conf/type/__letsencrypt_cert/parameter/boolean b/cdist/conf/type/__letsencrypt_cert/parameter/boolean new file mode 100644 index 00000000..d5b8be99 --- /dev/null +++ b/cdist/conf/type/__letsencrypt_cert/parameter/boolean @@ -0,0 +1,2 @@ +automatic-renewal +staging diff --git a/cdist/conf/type/__letsencrypt_cert/parameter/default/state b/cdist/conf/type/__letsencrypt_cert/parameter/default/state new file mode 100644 index 00000000..e7f6134f --- /dev/null +++ b/cdist/conf/type/__letsencrypt_cert/parameter/default/state @@ -0,0 +1 @@ +present diff --git a/cdist/conf/type/__letsencrypt_cert/parameter/default/webroot b/cdist/conf/type/__letsencrypt_cert/parameter/default/webroot new file mode 100644 index 00000000..e69de29b diff --git a/cdist/conf/type/__letsencrypt_cert/parameter/optional b/cdist/conf/type/__letsencrypt_cert/parameter/optional new file mode 100644 index 00000000..0a63b11e --- /dev/null +++ b/cdist/conf/type/__letsencrypt_cert/parameter/optional @@ -0,0 +1,2 @@ +state +webroot diff --git a/cdist/conf/type/__letsencrypt_cert/parameter/required b/cdist/conf/type/__letsencrypt_cert/parameter/required index 45fe4ea6..bfe77226 100644 --- a/cdist/conf/type/__letsencrypt_cert/parameter/required +++ b/cdist/conf/type/__letsencrypt_cert/parameter/required @@ -1,2 +1 @@ admin-email -webroot diff --git a/cdist/conf/type/__letsencrypt_cert/parameter/required_multiple b/cdist/conf/type/__letsencrypt_cert/parameter/required_multiple new file mode 100644 index 00000000..d23ab7ab --- /dev/null +++ b/cdist/conf/type/__letsencrypt_cert/parameter/required_multiple @@ -0,0 +1 @@ +domain