From d897eaf5cdb0c34888771bd8aa94fd494e67af3d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sat, 28 Aug 2021 06:28:53 +0200 Subject: Adding dehydrated tools. Signed-off-by: Daniel Baumann --- dehydrated/Makefile | 118 +++++++++++++++++++++ dehydrated/bin/dehydrated-cron | 7 ++ dehydrated/bin/dehydrated-hook.d | 96 +++++++++++++++++ dehydrated/bin/dehydrated-nsupdate | 92 ++++++++++++++++ dehydrated/share/cron/dehydrated | 3 + .../share/hooks/deploy_cert.fullchain-privkey | 9 ++ .../share/hooks/deploy_ocsp.fullchain-privkey | 8 ++ dehydrated/share/hooks/exit_hook.fix-permissions | 18 ++++ dehydrated/share/hooks/exit_hook.service-reload | 17 +++ dehydrated/share/logrotate/dehydrated | 13 +++ 10 files changed, 381 insertions(+) create mode 100644 dehydrated/Makefile create mode 100755 dehydrated/bin/dehydrated-cron create mode 100755 dehydrated/bin/dehydrated-hook.d create mode 100755 dehydrated/bin/dehydrated-nsupdate create mode 100755 dehydrated/share/cron/dehydrated create mode 100755 dehydrated/share/hooks/deploy_cert.fullchain-privkey create mode 100755 dehydrated/share/hooks/deploy_ocsp.fullchain-privkey create mode 100755 dehydrated/share/hooks/exit_hook.fix-permissions create mode 100755 dehydrated/share/hooks/exit_hook.service-reload create mode 100644 dehydrated/share/logrotate/dehydrated diff --git a/dehydrated/Makefile b/dehydrated/Makefile new file mode 100644 index 0000000..0c9da96 --- /dev/null +++ b/dehydrated/Makefile @@ -0,0 +1,118 @@ +# Open Infrastructure: service-tools + +# Copyright (C) 2014-2021 Daniel Baumann +# +# SPDX-License-Identifier: GPL-3.0+ +# +# This program is free software: 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. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +SHELL := sh -e + +SCRIPTS = bin/* share/hooks/* + +all: build + +test: + @echo -n "Checking for syntax errors with sh... " + @for SCRIPT in $(SCRIPTS); \ + do \ + sh -n $${SCRIPT}; \ + echo -n "."; \ + done + @echo " done." + + @echo -n "Checking for bashisms... " + @if [ -x /usr/bin/checkbashisms ]; \ + then \ + for SCRIPT in $(SCRIPTS); \ + do \ + checkbashisms -f -x $${SCRIPT}; \ + echo -n "."; \ + done; \ + else \ + echo "Note: devscripts not installed, skipping checkbashisms."; \ + fi + @echo " done." + + @echo -n "Checking with shellcheck... " + @if [ -x /usr/bin/shellcheck ]; \ + then \ + for SCRIPT in $(SCRIPTS); \ + do \ + shellcheck -e SC2039 $${SCRIPT}; \ + echo -n "."; \ + done; \ + else \ + echo "Note: shellcheck not installed, skipping shellcheck."; \ + fi + @echo " done." + +build: + +install: build + mkdir -p $(DESTDIR)/etc/dehydrated/hook.d + + mkdir -p $(DESTDIR)/etc/cron.d + cp -r share/cron/* $(DESTDIR)/etc/cron.d + + mkdir -p $(DESTDIR)/etc/cron.daily + ln -s /usr/bin/dehydrated-cron $(DESTDIR)/etc/cron.daily/dehydrated + + mkdir -p $(DESTDIR)/etc/logrotate.d + cp -r share/logrotate/* $(DESTDIR)/etc/logrotate.d + + mkdir -p $(DESTDIR)/usr/bin + cp -r bin/* $(DESTDIR)/usr/bin + + mkdir -p $(DESTDIR)/usr/share/dehydrated/hooks + cp -r share/hooks/* $(DESTDIR)/usr/share/dehydrated/hooks + + ln -sf /usr/bin/dehydrated-nsupdate $(DESTDIR)/usr/share/dehydrated/hooks/clean_challenge.nsupdate + ln -sf /usr/bin/dehydrated-nsupdate $(DESTDIR)/usr/share/dehydrated/hooks/deploy_challenge.nsupdate + +uninstall: + rm -rf $(DESTDIR)/usr/share/dehydrated/hooks + rmdir --ignore-fail-on-non-empty --parents $(DESTDIR)/usr/share/dehydrated || true + + for FILE in bin/*; \ + do \ + rm -f $(DESTDIR)/usr/bin/$$(basename $${FILE}); \ + done + rmdir --ignore-fail-on-non-empty --parents $(DESTDIR)/usr/bin || true + + for FILE in share/logrotate/*; \ + do \ + rm -f $(DESTDIR)/etc/logrotate.d/$$(basename $${FILE}); \ + done + rmdir --ignore-fail-on-non-empty --parents $(DESTDIR)/etc/logrotate.d || true + + rm -f $(DESTDIR)/etc/cron.daily/dehydrated + rmdir --ignore-fail-on-non-empty --parents $(DESTDIR)/etc/cron.daily || true + + for FILE in share/cron/*; \ + do \ + rm -f $(DESTDIR)/etc/cron.d/$$(basename $${FILE}); \ + done + rmdir --ignore-fail-on-non-empty --parents $(DESTDIR)/etc/cron.d || true + + rm -rf $(DESTDIR)/etc/dehydrated/hook.d + rmdir --ignore-fail-on-non-empty --parents $(DESTDIR)/etc/dehydrated || true + + rmdir --ignore-fail-on-non-empty --parents $(DESTDIR) || true + +clean: + +distclean: + +reinstall: uninstall install diff --git a/dehydrated/bin/dehydrated-cron b/dehydrated/bin/dehydrated-cron new file mode 100755 index 0000000..09e1a44 --- /dev/null +++ b/dehydrated/bin/dehydrated-cron @@ -0,0 +1,7 @@ +#!/bin/sh + +set -e + +mkdir -p /var/log/dehydrated +dehydrated -gcd >> /var/log/dehydrated/dehydrated.log +chown -R root:adm /var/log/dehydrated diff --git a/dehydrated/bin/dehydrated-hook.d b/dehydrated/bin/dehydrated-hook.d new file mode 100755 index 0000000..2d9a5af --- /dev/null +++ b/dehydrated/bin/dehydrated-hook.d @@ -0,0 +1,96 @@ +#!/bin/sh + +set -e + +HOOKS="/etc/dehydrated/hook.d" + +deploy_challenge () +{ + export DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}" + + run-parts --regex '^deploy_challenge.*' "${HOOKS}" +} + +clean_challenge () +{ + export DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}" + + run-parts --regex '^clean_challenge.*' "${HOOKS}" +} + +sync_cert () +{ + export KEYFILE="${1}" CERTFILE="${2}" FULLCHAINFILE="${3}" CHAINFILE="${4}" REQUESTFILE="${5}" + + run-parts --regex '^sync_cert.*' "${HOOKS}" +} + +deploy_cert () +{ + export DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}" + + run-parts --regex '^deploy_cert.*' "${HOOKS}" +} + +deploy_ocsp () +{ + export DOMAIN="${1}" OCSPFILE="${2}" TIMESTAMP="${3}" + + run-parts --regex '^deploy_ocsp.*' "${HOOKS}" +} + +unchanged_cert () +{ + export DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" + + run-parts --regex '^unchanged_cert.*' "${HOOKS}" +} + +invalid_challenge () +{ + export DOMAIN="${1}" RESPONSE="${2}" + + run-parts --regex '^invalid_challenge.*' "${HOOKS}" +} + +request_failure () +{ + export STATUSCODE="${1}" REASON="${2}" REQTYPE="${3}" HEADERS="${4}" + + run-parts --regex '^request_failure.*' "${HOOKS}" +} + +generate_csr () +{ + export DOMAIN="${1}" CERTDIR="${2}" ALTNAMES="${3}" + + run-parts --regex '^generate_csr.*' "${HOOKS}" +} + +startup_hook () +{ + run-parts --regex '^startup_hook.*' "${HOOKS}" +} + +exit_hook () +{ + export ERROR="${1:-}" + + run-parts --regex '^exit_hook.*' "${HOOKS}" +} + +HANDLER="${1}" + +if [ -z "${HANDLER}" ] +then + echo "Usage: ${0} HANDLER" >&2 + exit 1 +fi + +shift + +case "${HANDLER}" in + deploy_challenge|clean_challenge|sync_cert|deploy_cert|deploy_ocsp|unchanged_cert|invalid_challenge|request_failure|generate_csr|startup_hook|exit_hook) + "${HANDLER}" "${@}" + ;; +esac diff --git a/dehydrated/bin/dehydrated-nsupdate b/dehydrated/bin/dehydrated-nsupdate new file mode 100755 index 0000000..f901d2e --- /dev/null +++ b/dehydrated/bin/dehydrated-nsupdate @@ -0,0 +1,92 @@ +#!/bin/sh + +set -e + +HOOK="$(basename "${0}")" +HOOK_ACTION="$(echo "${HOOK}" | awk -F. '{ print $1 }')" + +# set nsupdate action +case "${HOOK}" in + clean_challenge.*) + HOOK_ACTION="delete" + ;; + + deploy_challenge.*) + HOOK_ACTION="add" + ;; + + *) + echo "'${HOOK}': no such hook action '${HOOK_ACTION}'" >&2 + echo "'${HOOK}': use 'clean_challenge.' or 'deploy_challenge.' as prefix in your symlink" >&2 + exit 1 + ;; +esac + +# alternatives handling for dig +if command -v kdig > /dev/null 2>&1 +then + # knot-dnsutils + DIG="kdig" +elif command -v dig > /dev/null 2>&1 +then + # bind-dnsutils + DIG="dig" +else + echo "'${HOOK}': need dig from bind-dnsutils or knot-dnsutils" >&2 + exit 1 +fi + +# alternatives handling for nsupdate +if command -v knsupdate > /dev/null 2>&1 +then + # knot-dnsutils + NSUPDATE="knsupdate" +elif command -v nsupdate > /dev/null 2>&1 +then + # bind-dnsutils + NSUPDATE="nsupdate" +else + echo "'${HOOK}': need nsupdate from bind-dnsutils or knot-dnsutils" >&2 + exit 1 +fi + +# find txt record to update +CNAME="$(${DIG} "_acme-challenge.${DOMAIN}" 2>&1 | awk '/CNAME/ { print $5 }' | tail -n1)" + +if [ -n "${CNAME}" ] +then + UPDATE_DOMAIN="${CNAME}" +else + UPDATE_DOMAIN="_acme-challenge.${DOMAIN}" +fi + +# find nameservers to update +ZONE="${UPDATE_DOMAIN}" + +while true +do + NAMESERVERS="$(${DIG} NS "${ZONE}" 2>&1 | awk '/NS/ { print $5 }' | tail -n1)" + + if [ -n "${NAMESERVERS}" ] + then + break + else + ZONE="$(echo "${ZONE}" | cut -d '.' -f 2-)" + fi +done + +NAMESERVERS="$(${DIG} +short NS "${ZONE}")" + +# update nameservers +for NAMESERVER in ${NAMESERVERS} +do + echo -n " + Adding TXT record (${UPDATE_DOMAIN})..." + +echo "server ${NAMESERVER} +zone ${ZONE} +ttl 0 +update ${HOOK_ACTION} ${UPDATE_DOMAIN} 0 TXT ${TOKEN_VALUE} +send" | "${NSUPDATE}" + + echo " done." +done diff --git a/dehydrated/share/cron/dehydrated b/dehydrated/share/cron/dehydrated new file mode 100755 index 0000000..bece74f --- /dev/null +++ b/dehydrated/share/cron/dehydrated @@ -0,0 +1,3 @@ +# /etc/cron.d/dehydrated + +@reboot root /usr/bin/dehydrated-cron diff --git a/dehydrated/share/hooks/deploy_cert.fullchain-privkey b/dehydrated/share/hooks/deploy_cert.fullchain-privkey new file mode 100755 index 0000000..5457036 --- /dev/null +++ b/dehydrated/share/hooks/deploy_cert.fullchain-privkey @@ -0,0 +1,9 @@ +#!/bin/sh + +set -e + +DIRECTORY="$(dirname "${FULLCHAINFILE}")" +FILE="cert.fullchain-privkey-${TIMESTAMP}.pem" + +cat "${FULLCHAINFILE}" "${KEYFILE}" > "${DIRECTORY}/${FILE}" +ln -sf "${FILE}" "${DIRECTORY}/cert.fullchain-privkey.pem" diff --git a/dehydrated/share/hooks/deploy_ocsp.fullchain-privkey b/dehydrated/share/hooks/deploy_ocsp.fullchain-privkey new file mode 100755 index 0000000..e68716b --- /dev/null +++ b/dehydrated/share/hooks/deploy_ocsp.fullchain-privkey @@ -0,0 +1,8 @@ +#!/bin/sh + +set -e + +FILE="$(readlink "${OCSPFILE}")" +DIRECTORY="$(dirname "${OCSPFILE}")" + +ln -sf "${FILE}" "${DIRECTORY}/cert.fullchain-privkey.pem.ocsp" diff --git a/dehydrated/share/hooks/exit_hook.fix-permissions b/dehydrated/share/hooks/exit_hook.fix-permissions new file mode 100755 index 0000000..c5bb646 --- /dev/null +++ b/dehydrated/share/hooks/exit_hook.fix-permissions @@ -0,0 +1,18 @@ +#!/bin/sh + +set -e + +echo " + Fixing permissions..." + +if getent group ssl-cert > /dev/null 2>&1 +then + echo -n " + /var/lib/dehydrated/certs:" + + find /var/lib/dehydrated/certs -type d -exec chmod 0750 {} \; + find /var/lib/dehydrated/certs -type f -exec chmod 0640 {} \; + + # https://bugs.debian.org/854431 + chown -R root:ssl-cert /var/lib/dehydrated/certs + + echo " done." +fi diff --git a/dehydrated/share/hooks/exit_hook.service-reload b/dehydrated/share/hooks/exit_hook.service-reload new file mode 100755 index 0000000..2da8c1b --- /dev/null +++ b/dehydrated/share/hooks/exit_hook.service-reload @@ -0,0 +1,17 @@ +#!/bin/sh + +set -e + +echo " + Reloading services..." + +for SERVICE in apache2 haproxy postgresql redis-server +do + if service ${SERVICE} status > /dev/null 2>&1 + then + echo -n " + ${SERVICE}:" + + service ${SERVICE} reload || service ${SERVICE} restart + + echo " done." + fi +done diff --git a/dehydrated/share/logrotate/dehydrated b/dehydrated/share/logrotate/dehydrated new file mode 100644 index 0000000..385a4aa --- /dev/null +++ b/dehydrated/share/logrotate/dehydrated @@ -0,0 +1,13 @@ +# /etc/logrotate.d/dehydrated + +/var/log/dehydrated/dehydrated.log { + compress + create 0640 root adm + dateext + dateformat -%Y%m + dateyesterday + missingok + monthly + notifempty + rotate 12 +} -- cgit v1.2.3