diff --git a/CHANGELOG b/CHANGELOG index 0802e61..2f8965d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -9,6 +9,7 @@ This file contains a log of major changes in dehydrated ## Added - Added a configuration parameter to allow for timeouts during domain validation processing (`VALIDATION_TIMEOUT`, defaults to 0 = no timeout) - Added documentation for IP certificates +- Added support for DNS-PERSIST-01 challenge type ## Changed - Only validate existance of wellknown directory or hook script when actually needed diff --git a/README.md b/README.md index 6bed86f..0d5f235 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ Parameters: --preferred-chain issuer-cn Use alternative certificate chain identified by issuer CN --out (-o) certs/directory Output certificates into the specified directory --alpn alpn-certs/directory Output alpn verification certificates into the specified directory - --challenge (-t) http-01|dns-01|tls-alpn-01 Which challenge should be used? Currently http-01, dns-01, and tls-alpn-01 are supported + --challenge (-t) http-01|dns-01|dns-persist-01|tls-alpn-01 Which challenge should be used? Currently http-01, dns-01, dns-persist-01 and tls-alpn-01 are supported --algo (-a) rsa|prime256v1|secp384r1 Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1 --acme-profile profile_name Use specified ACME profile --order-timeout seconds Amount of seconds to wait for processing of order until erroring out diff --git a/dehydrated b/dehydrated index de3ac8b..dcdfbfb 100755 --- a/dehydrated +++ b/dehydrated @@ -366,7 +366,7 @@ hookscript_bricker_hook() { # verify configuration values verify_config() { - [[ "${CHALLENGETYPE}" == "http-01" || "${CHALLENGETYPE}" == "dns-01" || "${CHALLENGETYPE}" == "tls-alpn-01" ]] || _exiterr "Unknown challenge type ${CHALLENGETYPE}... cannot continue." + [[ "${CHALLENGETYPE}" == "http-01" || "${CHALLENGETYPE}" == "dns-01" || "${CHALLENGETYPE}" == "dns-persist-01" || "${CHALLENGETYPE}" == "tls-alpn-01" ]] || _exiterr "Unknown challenge type ${CHALLENGETYPE}... cannot continue." if [[ "${COMMAND:-}" =~ sign_domains|sign_csr ]]; then if [[ "${CHALLENGETYPE}" = "dns-01" ]] && [[ -z "${HOOK}" ]]; then _exiterr "Challenge type dns-01 needs a hook script for deployment... cannot continue." @@ -1283,6 +1283,10 @@ sign_csr() { # Generate DNS entry content for dns-01 validation keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)" ;; + "dns-persist-01") + # Pre-existing persistent DNS record is expected; no deploy/cleanup by dehydrated. + keyauth_hook="" + ;; "tls-alpn-01") keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -c -hex | awk '{print $NF}')" generate_alpn_certificate "${identifier}" "${identifier_type}" "${keyauth_hook}" @@ -1303,18 +1307,20 @@ sign_csr() { # Deploy challenge tokens if [[ ${num_pending_challenges} -ne 0 ]]; then - echo " + Deploying challenge tokens..." - if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]]; then - # shellcheck disable=SC2068 - "${HOOK}" "deploy_challenge" ${deploy_args[@]} || _exiterr 'deploy_challenge hook returned with non-zero exit code' - elif [[ -n "${HOOK}" ]]; then - # Run hook script to deploy the challenge token - local idx=0 - while [ ${idx} -lt ${num_pending_challenges} ]; do - # shellcheck disable=SC2086 - "${HOOK}" "deploy_challenge" ${deploy_args[${idx}]} || _exiterr 'deploy_challenge hook returned with non-zero exit code' - idx=$((idx+1)) - done + if [[ "${CHALLENGETYPE}" != "dns-persist-01" ]]; then + echo " + Deploying challenge tokens..." + if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]]; then + # shellcheck disable=SC2068 + "${HOOK}" "deploy_challenge" ${deploy_args[@]} || _exiterr 'deploy_challenge hook returned with non-zero exit code' + elif [[ -n "${HOOK}" ]]; then + # Run hook script to deploy the challenge token + local idx=0 + while [ ${idx} -lt ${num_pending_challenges} ]; do + # shellcheck disable=SC2086 + "${HOOK}" "deploy_challenge" ${deploy_args[${idx}]} || _exiterr 'deploy_challenge hook returned with non-zero exit code' + idx=$((idx+1)) + done + fi fi fi @@ -1361,24 +1367,26 @@ sign_csr() { done if [[ ${num_pending_challenges} -ne 0 ]]; then - echo " + Cleaning challenge tokens..." + if [[ "${CHALLENGETYPE}" != "dns-persist-01" ]]; then + echo " + Cleaning challenge tokens..." - # Clean challenge tokens using chained hook - # shellcheck disable=SC2068 - [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && ("${HOOK}" "clean_challenge" ${deploy_args[@]} || _exiterr 'clean_challenge hook returned with non-zero exit code') + # Clean challenge tokens using chained hook + # shellcheck disable=SC2068 + [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && ("${HOOK}" "clean_challenge" ${deploy_args[@]} || _exiterr 'clean_challenge hook returned with non-zero exit code') - # Clean remaining challenge tokens if validation has failed - local idx=0 - while [ ${idx} -lt ${num_pending_challenges} ]; do - # Delete challenge file - [[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}" - # Delete alpn verification certificates - [[ "${CHALLENGETYPE}" = "tls-alpn-01" ]] && rm -f "${ALPNCERTDIR}/${challenge_names[${idx}]}.crt.pem" "${ALPNCERTDIR}/${challenge_names[${idx}]}.key.pem" - # Clean challenge token using non-chained hook - # shellcheck disable=SC2086 - [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && ("${HOOK}" "clean_challenge" ${deploy_args[${idx}]} || _exiterr 'clean_challenge hook returned with non-zero exit code') - idx=$((idx+1)) - done + # Clean remaining challenge tokens if validation has failed + local idx=0 + while [ ${idx} -lt ${num_pending_challenges} ]; do + # Delete challenge file + [[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}" + # Delete alpn verification certificates + [[ "${CHALLENGETYPE}" = "tls-alpn-01" ]] && rm -f "${ALPNCERTDIR}/${challenge_names[${idx}]}.crt.pem" "${ALPNCERTDIR}/${challenge_names[${idx}]}.key.pem" + # Clean challenge token using non-chained hook + # shellcheck disable=SC2086 + [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && ("${HOOK}" "clean_challenge" ${deploy_args[${idx}]} || _exiterr 'clean_challenge hook returned with non-zero exit code') + idx=$((idx+1)) + done + fi if [[ "${reqstatus}" != "valid" ]]; then echo " + Challenge validation has failed :(" @@ -2506,8 +2514,8 @@ main() { PARAM_ALPNCERTDIR="${1}" ;; - # PARAM_Usage: --challenge (-t) http-01|dns-01|tls-alpn-01 - # PARAM_Description: Which challenge should be used? Currently http-01, dns-01, and tls-alpn-01 are supported + # PARAM_Usage: --challenge (-t) http-01|dns-01|dns-persist-01|tls-alpn-01 + # PARAM_Description: Which challenge should be used? Currently http-01, dns-01, dns-persist-01 and tls-alpn-01 are supported --challenge|-t) shift 1 check_parameters "${1:-}" diff --git a/docs/dns-verification.md b/docs/dns-verification.md index 3b6ddb9..b5a777c 100644 --- a/docs/dns-verification.md +++ b/docs/dns-verification.md @@ -29,3 +29,24 @@ Or when you do have a DNS API, pass the details accordingly to achieve the same You can delete the TXT record when called with operation `clean_challenge`, when $2 is also the domain name. Here are some examples: [Examples for DNS-01 hooks](https://github.com/dehydrated-io/dehydrated/wiki) + +### dns-persist-01 challenge + +This script also supports the `dns-persist-01`-type verification. This type of verification requires you to create a persistent `TXT` DNS record containing your Let's Encrypt account information. + +Unlike `dns-01`, which requires dynamic DNS record updates for each certificate request, `dns-persist-01` uses a single persistent record that remains in place indefinitely. + +You need to create a TXT record named `_validation-persist` in the domain for which you want to request certificates. The record should contain your account URI and other metadata. + +Example record: +``` +_validation-persist.example.com. IN TXT ( + "letsencrypt.org;" + " accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/1234567890;" + " policy=wildcard" +) +``` + +The account URI can be obtained by running `dehydrated --register --accept-terms` and checking the account registration response, or by examining the `accounts/*/registration.json` file after registration. + +This record should be set up once and left in place. No hook script is required for `dns-persist-01` as dehydrated does not perform any dynamic DNS updates for this challenge type. diff --git a/docs/man/dehydrated.1 b/docs/man/dehydrated.1 index e51884e..c4a76a8 100644 --- a/docs/man/dehydrated.1 +++ b/docs/man/dehydrated.1 @@ -26,7 +26,7 @@ single certificate valid for both "example.net" and "example.com" through the \f Alternative Name\fR (SAN) field. For the next step, one way of verifying domain name ownership needs to be -configured. Dehydrated implements \fIhttp-01\fR and \fIdns-01\fR verification. +configured. Dehydrated implements \fIhttp-01\fR, \fIdns-01\fR, and \fIdns-persist-01\fR verification. The \fIhttp-01\fR verification provides proof of ownership by providing a challenge token. In order to do that, the directory referenced in the @@ -44,6 +44,12 @@ the software or the DNS provider at hand, there are many third party hooks available for dehydrated. See \fIdns-verification.md\fR for hooks for popular DNS servers and DNS hosters. +The \fIdns-persist-01\fR verification works by providing a persistent DNS record +containing account information. Unlike \fIdns-01\fR, this requires setting up a +static TXT record once that remains in place indefinitely. No dynamic DNS +updates are performed during certificate requests. See \fIdns-verification.md\fR +for details on setting up the required DNS record. + Finally, the certificates need to be requested and updated on a regular basis. This can happen through a cron job or a timer. Initially, you may enforce this by invoking \fIdehydrated -c\fR manually.