Add DNS-PERSIST-01 challenge support

- 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
This commit is contained in:
Youfu Zhang
2026-03-29 07:17:22 +00:00
committed by Lukas Schauer
parent c63d1cb528
commit cfd637d769
5 changed files with 69 additions and 33 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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:-}"

View 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.

View 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.