diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..30a5125 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "acme/dehydrated"] + path = acme/dehydrated + url = https://github.com/dehydrated-io/dehydrated.git diff --git a/acme/README.md b/acme/README.md new file mode 100644 index 0000000..e70e1bc --- /dev/null +++ b/acme/README.md @@ -0,0 +1,18 @@ +# ACME - snippet to obtain let´s encrpyt certificates authenticated by DNS-01 + +## Dockerfile: +``` +ADD ./snippets/acme/dehydrated /dehydrated/ +ADD ./snippets/acme/config /dehydrated/config +ADD ./snippets/acme/get_cert_ddns01.sh /app/get_cert_ddns01.sh +ADD ./snippets/acme/zwackl_hook.sh /app/zwackl_hook.sh +``` +**Do not forget to include the crond-snippet!** + +## Environment +* ACME_FQDNS (required) +* ACME_RELOAD_CMD (required) +* ACME_STAGING_ENABLED (optional) +* STAGING_URI (optional) +* DDNS01URI (required) +* DDNS01KEY (required) diff --git a/acme/config b/acme/config new file mode 100644 index 0000000..caf3136 --- /dev/null +++ b/acme/config @@ -0,0 +1,3 @@ +PRIVATE_KEY_RENEW="no" +PRIVATE_KEY_ROLLOVER="no" +%STAGING% diff --git a/acme/dehydrated b/acme/dehydrated new file mode 160000 index 0000000..307eaad --- /dev/null +++ b/acme/dehydrated @@ -0,0 +1 @@ +Subproject commit 307eaadddfd49aeafda6984f11e6731329372cd5 diff --git a/acme/get_cert_ddns01.sh b/acme/get_cert_ddns01.sh new file mode 100755 index 0000000..d873b40 --- /dev/null +++ b/acme/get_cert_ddns01.sh @@ -0,0 +1,67 @@ +#!/bin/bash + +# ACME@LETSENCRYPT - DEHYDRATED +if [ ! -z "${ACME_FQDNS+x}" ]; then + if [ -f /dehydrated/lock ]; then + unlink /dehydrated/lock + fi + if [ -z "${ACME_RELOAD_CMD}" ]; then + echo "ENV[ACME_RELOAD_CMD] is mandatory!" + exit 1 + fi + if [ ! -d /secrets/ssl ]; then + mkdir -p /secrets/ssl + fi + if [ ! -z "${ACME_STAGING_ENABLED+x}" ]; then + STAGING_URI='CA="https://acme-staging-v02.api.letsencrypt.org/directory"' \ + ESCAPED=$(echo "${STAGING_URI}" | sed -e 's/\//\\\//g') + sed -i -e "s/%STAGING%/${ESCAPED}/g" /dehydrated/config + else + sed -i -e "s/%STAGING%//g" /dehydrated/config + fi + if [ ! -z "${DDNS01URI+x}" ]; then + ESCAPED=$(echo "${DDNS01URI}" | sed -e 's/\//\\\//g') + sed -i -e "s/%DDNS01URI%/${ESCAPED}/g" /app/zwackl_hook.sh + else + echo "ENV[DDNS01URI] is mandatory!" + exit 1 + fi + if [ ! -z "${DDNS01KEY+x}" ]; then + ESCAPED=$(echo "${DDNS01KEY}" | sed -e 's/\//\\\//g') + sed -i -e "s/%DDNS01KEY%/${ESCAPED}/g" /app/zwackl_hook.sh + else + echo "ENV[DDNS01KEY] is mandatory!" + exit 1 + fi + if [ -d /dehydrated ]; then + echo -n "" > /dehydrated/domains.txt + for fqdn in ${ACME_FQDNS}; do + echo "${fqdn}" >> /dehydrated/domains.txt + if [ ! -d "/secrets/ssl/${fqdn}" ]; then + mkdir -p "/secrets/ssl/${fqdn}" + fi + done + chmod +x /app/zwackl_hook.sh + chmod +x /dehydrated/renew_certs + ln -f -s /dehydrated/renew_certs /etc/periodic/daily/renew_certs + if [ -z "$(ls -A /dehydrated/accounts)" ]; then + cd /dehydrated && /dehydrated/dehydrated --register --accept-terms + fi + RUN_DEHYDRATED='' + for fqdn in ${ACME_FQDNS}; do + if [ ! -f "/dehydrated/certs/${fqdn}/fullchain.pem" ]; then + RUN_DEHYDRATED='yes' + fi + ln -f -s "/dehydrated/certs/${fqdn}/privkey.pem" "/secrets/ssl/${fqdn}/key.pem" + ln -f -s "/dehydrated/certs/${fqdn}/fullchain.pem" "/secrets/ssl/${fqdn}/cert.pem" + done + if [ ! -z "${RUN_DEHYDRATED}" ]; then + /dehydrated/dehydrated --cron -t dns-01 -k /app/zwackl_hook.sh + fi + export CROND_ENABLE='acme' + else + echo "Directory /dehydrated not found!" + exit 1 + fi +fi + diff --git a/acme/zwackl_hook.sh b/acme/zwackl_hook.sh new file mode 100755 index 0000000..ba2a841 --- /dev/null +++ b/acme/zwackl_hook.sh @@ -0,0 +1,111 @@ +#!/bin/bash + +function deploy_challenge { + # This hook is called once for every domain that needs to be + # validated, including any alternative names you may have listed. + # + # Parameters: + # - DOMAIN + # The domain name (CN or subject alternative name) being + # validated. + # - TOKEN_FILENAME + # The name of the file is irrelevant for the DNS challenge, yet still provided + # - TOKEN_VALUE + # The token value that needs to be served for validation. For DNS + # validation, this is what you want to put in the _acme-challenge + # TXT record. For HTTP validation it is the value that is expected + # be found in the $TOKEN_FILENAME file. + local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}" + + echo; + echo "Deploying challenge for domain $DOMAIN: $TOKEN_VALUE" + CMD_OUT=$(/usr/bin/curl -s --noproxy '*' "%DDNS01URI%/update?fqdn=_acme-challenge.${DOMAIN}&txtdata=${TOKEN_VALUE}&key=%DDNS01KEY%") + if [ $? -ne 0 ] + then + echo "${DOMAIN} -> ${TOKEN_VALUE}: ${CMD_OUT}" + exit 1; + else + echo "OK: ${CMD_OUT}" + fi + + echo "Sleeping for 66 seconds to avoid timing conflicts" + sleep 66 +} + + +function clean_challenge { + local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}" + + echo; + echo "Cleaning challenge for domain $DOMAIN" + # This hook is called after attempting to validate each domain, + # whether or not validation was successful. Here you can delete + # files or DNS records that are no longer needed. + # + # The parameters are the same as for deploy_challenge. +} + + +function deploy_cert { + # This hook is called once for each certificate that has been + # produced. Here you might, for instance, copy your new certificates + # to service-specific locations and reload the service. + # + # Parameters: + # - DOMAIN + # The primary domain name, i.e. the certificate common + # name (CN). + # - KEYFILE + # The path of the file containing the private key. + # - CERTFILE + # The path of the file containing the signed certificate. + # - FULLCHAINFILE + # The path of the file containing the full certificate chain. + # - CHAINFILE + # The path of the file containing the intermediate certificate(s). + local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" +} + +function unchanged_cert { + # This hook is called once for each certificate that is still valid at least 30 days + # + # Parameters: + # - DOMAIN + # The primary domain name, i.e. the certificate common + # name (CN). + # - KEYFILE + # The path of the file containing the private key. + # - CERTFILE + # The path of the file containing the signed certificate. + # - FULLCHAINFILE + # The path of the file containing the full certificate chain. + # - CHAINFILE + # The path of the file containing the intermediate certificate(s). + local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" + + echo "Certificate for domain $DOMAIN is still valid - no action taken" +} + +generate_csr() { + local DOMAIN="${1}" CERTDIR="${2}" ALTNAMES="${3}" +} + +startup_hook() { + # This hook is called before the cron command to do some initial tasks + # (e.g. starting a webserver). + + : +} + +exit_hook() { + # This hook is called at the end of the cron command and can be used to + # do some final (cleanup or other) tasks. + + : +} + +HANDLER="$1"; shift +if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|deploy_cert|unchanged_cert|invalid_challenge|request_failure|generate_csr|startup_hook|exit_hook)$ ]]; then + "$HANDLER" "$@" +fi +