From cbb8e9068c2ee602c436bfd67e99e4c032f576fa Mon Sep 17 00:00:00 2001 From: neil Date: Fri, 1 May 2026 12:56:00 +0200 Subject: [PATCH] support dns-persist-01 --- README.md | 68 +++++++++++++++++++----- acme.sh | 155 +++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 197 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 188af0e1..b2f22f3f 100644 --- a/README.md +++ b/README.md @@ -146,6 +146,7 @@ | 🌐 DNS mode | Use DNS TXT records | | 🔗 [DNS alias mode](https://github.com/acmesh-official/acme.sh/wiki/DNS-alias-mode) | Use DNS alias for verification | | 📡 [Stateless mode](https://github.com/acmesh-official/acme.sh/wiki/Stateless-Mode) | Stateless verification | +| 📌 DNS persist mode | Persistent DNS TXT record ([draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/)) | --- @@ -396,7 +397,50 @@ acme.sh --renew -d example.com --- -### 🔟 Issue Certificates of Different Key Types (ECC or RSA) +### 🔟 Use DNS Persist Mode + +📚 Spec: [draft-ietf-acme-dns-persist-01](https://datatracker.ietf.org/doc/draft-ietf-acme-dns-persist/) + +DNS persist mode lets you place a **single, long‑lived `_validation-persist` TXT record** in your zone and reuse it for every subsequent issuance and renewal. There is no per-issuance challenge token, so renewals require **no DNS edits** — useful when DNS API access is not available but you still want unattended renewals. + +#### 🪄 Step 1: Print the TXT record value + +```bash +acme.sh --make-dns-persist-value -d example.com [--server letsencrypt] [--dns-persist-wildcard] [--dns-persist-ca-name "sectigo.com"] +``` + +Options: + +| Flag | Description | +|------|-------------| +| `--server ` | Pick the CA (default is your configured default). The account is registered automatically if you have not used this CA before. | +| `--dns-persist-wildcard` | Adds `policy=wildcard` to the record so it also authorizes wildcard / subdomain certs. | +| `--dns-persist-ca-name ` | Use a specific CA identity domain (e.g. `sectigo.com`). If omitted, identities are read from the ACME directory's `caaIdentities` field and one record per identity is printed — you only need to add **any one** of them. | + +You should get an output like: + +```sh +TXT domain: _validation-persist.example.com +TXT value: "letsencrypt.org; accounturi=https://acme-v02.api.letsencrypt.org/acme/acct/123456789" +``` + +#### ✍️ Step 2: Add the TXT record to your DNS + +Add the printed `TXT domain` / `TXT value` pair as a TXT record at your DNS provider, then wait for it to propagate. + +#### 📜 Step 3: Issue the certificate + +```bash +acme.sh --issue -d example.com --dns-persist +``` + +✅ **Done!** No challenge token is provisioned during issuance — the CA reads the persistent TXT record directly. + +> 🔄 Renewals just work: `acme.sh --renew -d example.com` (or the cron job) reuses the same TXT record automatically — no further DNS edits needed. + +--- + +### 1️⃣1️⃣ Issue Certificates of Different Key Types (ECC or RSA) Just set the `keylength` to a valid, supported value. @@ -427,7 +471,7 @@ acme.sh --issue -w /home/wwwroot/example.com -d example.com -d www.example.com - --- -### 1️⃣1️⃣ Issue Wildcard Certificates +### 1️⃣2️⃣ Issue Wildcard Certificates It's simple! Just give a wildcard domain as the `-d` parameter: @@ -439,7 +483,7 @@ acme.sh --issue -d example.com -d '*.example.com' --dns dns_cf --- -### 1️⃣2️⃣ How to Renew Certificates +### 1️⃣3️⃣ How to Renew Certificates > 🔄 No need to renew manually! All certs will be renewed automatically every **30** days. @@ -457,7 +501,7 @@ acme.sh --renew -d example.com --force --ecc --- -### 1️⃣3️⃣ How to Stop Certificate Renewal +### 1️⃣4️⃣ How to Stop Certificate Renewal To stop renewal of a cert, you can execute the following to remove the cert from the renewal list: @@ -471,7 +515,7 @@ The cert/key file is not removed from the disk. --- -### 1️⃣4️⃣ How to Upgrade acme.sh +### 1️⃣5️⃣ How to Upgrade acme.sh > 🚀 acme.sh is in constant development — it's strongly recommended to use the latest code. @@ -495,25 +539,25 @@ acme.sh --upgrade --auto-upgrade 0 --- -### 1️⃣5️⃣ Issue a Certificate from an Existing CSR +### 1️⃣6️⃣ Issue a Certificate from an Existing CSR 📚 https://github.com/acmesh-official/acme.sh/wiki/Issue-a-cert-from-existing-CSR --- -### 1️⃣6️⃣ Send Notifications in Cronjob +### 1️⃣7️⃣ Send Notifications in Cronjob 📚 https://github.com/acmesh-official/acme.sh/wiki/notify --- -### 1️⃣7️⃣ Under the Hood +### 1️⃣8️⃣ Under the Hood > 🔧 Speak ACME language using shell, directly to "Let's Encrypt". --- -### 1️⃣8️⃣ Acknowledgments +### 1️⃣9️⃣ Acknowledgments | Project | Link | |---------|------| @@ -555,7 +599,7 @@ Support this project with your organization. Your logo will show up here with a --- -### 1️⃣9️⃣ License & Others +### 2️⃣0️⃣ License & Others 📄 **License:** GPLv3 @@ -565,7 +609,7 @@ Support this project with your organization. Your logo will show up here with a --- -### 2️⃣0️⃣ Donate +### 2️⃣1️⃣ Donate > 💝 Your donation makes **acme.sh** better! @@ -577,7 +621,7 @@ Support this project with your organization. Your logo will show up here with a --- -### 2️⃣1️⃣ About This Repository +### 2️⃣2️⃣ About This Repository > [!NOTE] > This repository is officially maintained by ZeroSSL as part of our commitment to providing secure and reliable SSL/TLS solutions. We welcome contributions and feedback from the community! diff --git a/acme.sh b/acme.sh index 71e40d24..dbed1359 100755 --- a/acme.sh +++ b/acme.sh @@ -59,6 +59,7 @@ DEFAULT_OPENSSL_BIN="openssl" VTYPE_HTTP="http-01" VTYPE_DNS="dns-01" VTYPE_ALPN="tls-alpn-01" +VTYPE_DNS_PERSIST="dns-persist-01" ID_TYPE_DNS="dns" ID_TYPE_IP="ip" @@ -71,6 +72,7 @@ NO_VALUE="no" W_DNS="dns" W_ALPN="alpn" +W_DNS_PERSIST="dns_persist" DNS_ALIAS_PREFIX="=" MODE_STATELESS="stateless" @@ -4028,6 +4030,85 @@ deactivateaccount() { fi } +#domain wildcard ca_name +#Print the TXT record(s) the user must add to enable persistent DNS validation +#per draft-ietf-acme-dns-persist-01. +makednspersistvalue() { + _mdpv_domain="$1" + _mdpv_wildcard="$2" + _mdpv_ca_name="$3" + + if [ -z "$_mdpv_domain" ]; then + _err "Please specify a domain with -d." + return 1 + fi + + _initpath + + _accUri="$(_readcaconf ACCOUNT_URL)" + if [ -z "$_accUri" ]; then + _info "No account is registered for $ACME_DIRECTORY yet, registering one now..." + if ! _regAccount "$DEFAULT_ACCOUNT_KEY_LENGTH"; then + _err "Cannot register account." + return 1 + fi + _accUri="$(_readcaconf ACCOUNT_URL)" + fi + + if [ -z "$_accUri" ]; then + _err "Cannot determine the ACME account URL." + return 1 + fi + _debug "Account URL" "$_accUri" + + _txt_name="_validation-persist.$_mdpv_domain" + + _txt_suffix="; accounturi=$_accUri" + if [ "$_mdpv_wildcard" = "1" ]; then + _txt_suffix="$_txt_suffix; policy=wildcard" + fi + + if [ -n "$_mdpv_ca_name" ]; then + _info "" + _info "Add the following DNS TXT record to enable persistent DNS validation:" + _info "" + _info "$(printf 'TXT domain: %s' "$(__green "$_txt_name")")" + _info "$(printf 'TXT value: %s' "$(__green "\"$_mdpv_ca_name$_txt_suffix\"")")" + _info "" + return 0 + fi + + _info "Fetching ACME directory: $ACME_DIRECTORY" + _dir_resp="$(_get "$ACME_DIRECTORY" "" 30)" + if [ "$?" != "0" ] || [ -z "$_dir_resp" ]; then + _err "Cannot fetch ACME directory: $ACME_DIRECTORY" + return 1 + fi + _dir_resp="$(echo "$_dir_resp" | _json_decode)" + _debug2 _dir_resp "$_dir_resp" + + _caa_array="$(echo "$_dir_resp" | tr -d ' \r\n\t' | _egrep_o '"caaIdentities":\[[^]]*\]')" + _debug2 _caa_array "$_caa_array" + _caaids="$(echo "$_caa_array" | sed 's/.*\[//' | sed 's/\].*//' | tr ',' '\n' | tr -d '"')" + _debug2 _caaids "$_caaids" + + if [ -z "$_caaids" ]; then + _err "The directory does not include 'caaIdentities'. Please specify --dns-persist-ca-name explicitly." + return 1 + fi + + _info "" + _info "Add ANY ONE of the following DNS TXT records to enable persistent DNS validation." + _info "(You only need to add one; pick whichever issuer identity you prefer.)" + for _id in $_caaids; do + [ -z "$_id" ] && continue + _info "" + _info "$(printf 'TXT domain: %s' "$(__green "$_txt_name")")" + _info "$(printf 'TXT value : %s' "$(__green "\"$_id$_txt_suffix\"")")" + done + _info "" +} + # domain folder file _findHook() { _hookdomain="$1" @@ -4806,7 +4887,9 @@ $_authorizations_map" vtype="$VTYPE_HTTP" #todo, v2 wildcard force to use dns - if _startswith "$_currentRoot" "$W_DNS"; then + if [ "$_currentRoot" = "$W_DNS_PERSIST" ]; then + vtype="$VTYPE_DNS_PERSIST" + elif _startswith "$_currentRoot" "$W_DNS"; then vtype="$VTYPE_DNS" fi @@ -4864,18 +4947,7 @@ $_authorizations_map" fi if [ -z "$keyauthorization" ]; then - token="$(echo "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" - _debug token "$token" - - if [ -z "$token" ]; then - _err "Cannot get domain token $entry" - _clearup - _on_issue_err "$_post_hook" - return 1 - fi - uri="$(echo "$entry" | _egrep_o '"url":"[^"]*' | cut -d '"' -f 4 | _head_n 1)" - _debug uri "$uri" if [ -z "$uri" ]; then @@ -4884,8 +4956,26 @@ $_authorizations_map" _on_issue_err "$_post_hook" return 1 fi - keyauthorization="$token.$thumbprint" - _debug keyauthorization "$keyauthorization" + + if [ "$vtype" = "$VTYPE_DNS_PERSIST" ]; then + # dns-persist-01 challenges have no token; the TXT record is + # provisioned out-of-band. Use a non-empty placeholder so the + # downstream code does not treat this entry as already verified. + keyauthorization="$VTYPE_DNS_PERSIST" + _debug keyauthorization "$keyauthorization" + else + token="$(echo "$entry" | _egrep_o '"token":"[^"]*' | cut -d : -f 2 | tr -d '"')" + _debug token "$token" + + if [ -z "$token" ]; then + _err "Cannot get domain token $entry" + _clearup + _on_issue_err "$_post_hook" + return 1 + fi + keyauthorization="$token.$thumbprint" + _debug keyauthorization "$keyauthorization" + fi fi dvlist="$d$sep$keyauthorization$sep$uri$sep$vtype$sep$_currentRoot$sep$_authz_url" @@ -7158,6 +7248,8 @@ Commands: --update-account Update account info. --register-account Register account key. --deactivate-account Deactivate the account. + --make-dns-persist-value Print the DNS TXT record(s) to enable persistent DNS validation + (draft-ietf-acme-dns-persist-01). Use with -d . --create-account-key Create an account private key, professional use. --install-cronjob Install the cron job to renew certs, you don't need to call this. The 'install' command can automatically install the cron job. --uninstall-cronjob Uninstall the cron job. The 'uninstall' command can do this automatically. @@ -7205,6 +7297,10 @@ Parameters: --dns [dns_hook] Use dns manual mode or dns api. Defaults to manual mode when argument is omitted. See: $_DNS_API_WIKI + --dns-persist Use dns-persist-01 validation (draft-ietf-acme-dns-persist-01). + Requires the persistent _validation-persist TXT record to already + exist. Use '--make-dns-persist-value' to print the value to add. + --dnssleep The time in seconds to wait for all the txt records to propagate in dns api mode. It's not necessary to use this by default, $PROJECT_NAME polls dns status by DOH automatically. -k, --keylength Specifies the domain key length: 2048, 3072, 4096, 8192 or ec-256, ec-384, ec-521. @@ -7215,6 +7311,14 @@ Parameters: --eab-kid Key Identifier for External Account Binding. --eab-hmac-key HMAC key for External Account Binding. + --dns-persist-wildcard Used with '--make-dns-persist-value'. Adds 'policy=wildcard' to the + generated TXT record so the issuer is also authorized for wildcards + and subdomains (draft-ietf-acme-dns-persist-01). + --dns-persist-ca-name Used with '--make-dns-persist-value'. Use the given CA identity domain + (e.g. 'ssl.com') as the issuer-domain-name in the TXT record. If + omitted, the identities are read from the ACME directory's + 'caaIdentities' field and one record is printed per identity. + These parameters are to install the cert to nginx/Apache or any other server after issue/renew a cert: @@ -7585,6 +7689,8 @@ _process() { _valid_to="" _certificate_profile="" _extended_key_usage="" + _dns_persist_wildcard="" + _dns_persist_ca_name="" while [ ${#} -gt 0 ]; do case "${1}" in @@ -7679,6 +7785,16 @@ _process() { --deactivate-account) _CMD="deactivateaccount" ;; + --make-dns-persist-value | --makednspersistvalue) + _CMD="makednspersistvalue" + ;; + --dns-persist-wildcard | --dnspersistwildcard) + _dns_persist_wildcard="1" + ;; + --dns-persist-ca-name | --dnspersistcaname) + _dns_persist_ca_name="$2" + shift + ;; --set-notify) _CMD="setnotify" ;; @@ -7822,6 +7938,14 @@ _process() { _webroot="$_webroot,$wvalue" fi ;; + --dns-persist) + wvalue="$W_DNS_PERSIST" + if [ -z "$_webroot" ]; then + _webroot="$wvalue" + else + _webroot="$_webroot,$wvalue" + fi + ;; --dnssleep) _dnssleep="$2" Le_DNSSleep="$_dnssleep" @@ -8238,6 +8362,9 @@ _process() { deactivateaccount) deactivateaccount ;; + makednspersistvalue) + makednspersistvalue "$_domain" "$_dns_persist_wildcard" "$_dns_persist_ca_name" + ;; list) list "$_listraw" "$_domain" ;;