]> git.ipfire.org Git - thirdparty/dehydrated.git/commitdiff
Add DNS-PERSIST-01 challenge support
authorYoufu Zhang <zhangyoufu@gmail.com>
Sun, 29 Mar 2026 07:17:22 +0000 (07:17 +0000)
committerLukas Schauer <lukas@schauer.dev>
Thu, 30 Apr 2026 14:10:16 +0000 (16:10 +0200)
- Add dns-persist-01 to allowed challenge types in verify_config()
- Implement dns-persist-01 case in challenge preparation (no dynamic token)
- Skip deployment and cleanup for dns-persist-01
- Update help text and documentation
- Add man page and README updates
- Update CHANGELOG

CHANGELOG
README.md
dehydrated
docs/dns-verification.md
docs/man/dehydrated.1

index 0802e61d2c37b38a8f1c31ed64091104d83a40f7..2f8965df9c38c81f74ac840dca5d6309e1a9ac5d 100644 (file)
--- 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
index 6bed86ff9dda7558ba85a6431059f83c031a1941..0d5f2350b86924132b2c752b4053127abdd82533 100644 (file)
--- 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
index de3ac8b0510bfab799d2745ba9e9272834f13845..dcdfbfbf1a4f9273a69385740a400cc297ce4550 100755 (executable)
@@ -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:-}"
index 3b6ddb997e52b37ab10c88a8d97f135ec8b3a1cd..b5a777c39671f1ff28395c1996b267dccc51b473 100644 (file)
@@ -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.
index e51884e4cba431911c564908118d33dd07a43477..c4a76a8e8f4b83925881cc50803f60a2fed13aec 100644 (file)
@@ -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.