From 014a7814260025cac0aa7d3c0e95ac2cfb4d5230 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Sun, 6 Jul 2025 20:12:10 -0400 Subject: [PATCH 01/38] Create localcopy deploy-hook Deploy-hook to very simply copy files to set directories and then execute whatever reloadcmd the admin needs afterwards. This can be useful for configurations where the "multideploy" hook (in development) is used or when an admin wants ACME.SH to renew certs but needs to manually configure deployment via an external script (e.g. The deploy-freenas script for TrueNAS Core/Scale https://github.com/danb35/deploy-freenas/ Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- deploy/localcopy.sh | 100 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 deploy/localcopy.sh diff --git a/deploy/localcopy.sh b/deploy/localcopy.sh new file mode 100644 index 00000000..3b4fc219 --- /dev/null +++ b/deploy/localcopy.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env sh + +# Deploy-hook to very simply copy files to set directories and then +# execute whatever reloadcmd the admin needs afterwards. This can be +# useful for configurations where the "multideploy" hook (in development) +# is used or when an admin wants ACME.SH to renew certs but needs to +# manually configure deployment via an external script +# (e.g. The deploy-freenas script for TrueNAS Core/Scale +# https://github.com/danb35/deploy-freenas/ ) +# +# +# Environment variables to be utilized are as follows: +# +# DEPLOY_LOCALCOPY_CERTIFICATE - /path/to/target/cert.cer +# DEPLOY_LOCALCOPY_CERTKEY - /path/to/target/cert.key +# DEPLOY_LOCALCOPY_FULLCHAIN - /path/to/target/fullchain.cer +# DEPLOY_LOCALCOPY_CA - /path/to/target/ca.cer +# DEPLOY_LOCALCOPY_RELOADCMD - "echo 'this is my cmd'" + +######## Public functions ##################### + +#domain keyfile certfile cafile fullchain +localcopy_deploy() { + _cdomain="$1" + _ckey="$2" + _ccert="$3" + _cca="$4" + _cfullchain="$5" + + _debug _cdomain "$_cdomain" + _debug _ckey "$_ckey" + _debug _ccert "$_ccert" + _debug _cca "$_cca" + _debug _cfullchain "$_cfullchain" + + _getdeployconf DEPLOY_LOCALCOPY_CERTIFICATE + _getdeployconf DEPLOY_LOCALCOPY_CERTKEY + _getdeployconf DEPLOY_LOCALCOPY_FULLCHAIN + _getdeployconf DEPLOY_LOCALCOPY_CA + _getdeployconf DEPLOY_LOCALCOPY_RELOADCMD + + if [ "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then + _info "Copying certificate" + _debug "Copying $_ccert to $DEPLOY_LOCALCOPY_CERTIFICATE" + if ! eval "cp $_ccert $DEPLOY_LOCALCOPY_CERTIFICATE"; then + _err "Failed to copy certificate, aborting." + return 1 + fi + _savedeployconf DEPLOY_LOCALCOPY_CERTIFICATE "$DEPLOY_LOCALCOPY_CERTIFICATE" + fi + + if [ "$DEPLOY_LOCALCOPY_CERTKEY" ]; then + _info "Copying certificate key" + _debug "Copying $_ckey to $DEPLOY_LOCALCOPY_CERTKEY" + if ! eval "cp $_ckey $DEPLOY_LOCALCOPY_CERTKEY"; then + _err "Failed to copy certificate key, aborting." + return 1 + fi + _savedeployconf DEPLOY_LOCALCOPY_CERTKEY "$DEPLOY_LOCALCOPY_CERTKEY" + fi + + if [ "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then + _info "Copying fullchain" + _debug "Copying $_cfullchain to $DEPLOY_LOCALCOPY_FULLCHAIN" + if ! eval "cp $_cfullchain $DEPLOY_LOCALCOPY_FULLCHAIN"; then + _err "Failed to copy fullchain, aborting." + return 1 + fi + _savedeployconf DEPLOY_LOCALCOPY_FULLCHAIN "$DEPLOY_LOCALCOPY_FULLCHAIN" + fi + + if [ "$DEPLOY_LOCALCOPY_CA" ]; then + _info "Copying CA" + _debug "Copying $_cca to $DEPLOY_LOCALCOPY_CA" + if ! eval "cp $_cca $DEPLOY_LOCALCOPY_CA"; then + _err "Failed to copy CA, aborting." + return 1 + fi + _savedeployconf DEPLOY_LOCALCOPY_CA "$DEPLOY_LOCALCOPY_CA" + fi + + _reload=$DEPLOY_LOCALCOPY_RELOADCMD + _debug "Running reloadcmd $_reload" + + if [ -z "$_reload" ]; then + _info "Reloadcmd not provided, skipping." + else + _info "Reloading" + if eval "$_reload"; then + _info "Reload successful." + _savedeployconf DEPLOY_LOCALCOPY_RELOADCMD "$DEPLOY_LOCALCOPY_RELOADCMD" "base64" + else + _err "Reload failed." + return 1 + fi + fi + + _info "$(__green "'localcopy' deploy success")" + return 0 +} From c2f8b4d1f2821e927b3b8b614c6e0eaf73fc7170 Mon Sep 17 00:00:00 2001 From: pileus-lines Date: Mon, 23 Jun 2025 22:50:32 +0200 Subject: [PATCH 02/38] Update dns_infomaniak.sh because infomaniak API v1 no longer works --- dnsapi/dns_infomaniak.sh | 92 ++++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 51 deletions(-) diff --git a/dnsapi/dns_infomaniak.sh b/dnsapi/dns_infomaniak.sh index ea5ef461..34795888 100755 --- a/dnsapi/dns_infomaniak.sh +++ b/dnsapi/dns_infomaniak.sh @@ -6,6 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_infomaniak Options: INFOMANIAK_API_TOKEN API Token Issues: github.com/acmesh-official/acme.sh/issues/3188 + ' # To use this API you need visit the API dashboard of your account @@ -65,33 +66,32 @@ dns_infomaniak_add() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" - fqdn=${fulldomain#_acme-challenge.} - # guess which base domain to add record to - zone_and_id=$(_find_zone "$fqdn") - if [ -z "$zone_and_id" ]; then - _err "cannot find zone to modify" + zone=$(_get_zone "$fulldomain") + if [ -z "$zone" ]; then + _err "cannot find zone:<${zone}> to modify" return 1 fi - zone=${zone_and_id% *} - domain_id=${zone_and_id#* } # extract first part of domain key=${fulldomain%."$zone"} - _debug "zone:$zone id:$domain_id key:$key" + _debug "key:$key" + _debug "txtvalue: $txtvalue" # payload data="{\"type\": \"TXT\", \"source\": \"$key\", \"target\": \"$txtvalue\", \"ttl\": $INFOMANIAK_TTL}" # API call - response=$(_post "$data" "${INFOMANIAK_API_URL}/1/domain/$domain_id/dns/record") - if [ -n "$response" ] && echo "$response" | _contains '"result":"success"'; then - _info "Record added" - _debug "Response: $response" - return 0 + response=$(_post "$data" "${INFOMANIAK_API_URL}/2/zones/${zone}/records") + if [ -n "$response" ]; then + if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then + _info "Record added" + _debug "response: $response" + return 0 + fi fi - _err "could not create record" + _err "Could not create record." _debug "Response: $response" return 1 } @@ -106,7 +106,7 @@ dns_infomaniak_rm() { if [ -z "$INFOMANIAK_API_TOKEN" ]; then INFOMANIAK_API_TOKEN="" - _err "Please provide a valid Infomaniak API token in variable INFOMANIAK_API_TOKEN" + _err "Please provide a valid Infomaniak API token in variable INFOMANIAK_API_TOKEN." return 1 fi @@ -138,63 +138,53 @@ dns_infomaniak_rm() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" - fqdn=${fulldomain#_acme-challenge.} - # guess which base domain to add record to - zone_and_id=$(_find_zone "$fqdn") - if [ -z "$zone_and_id" ]; then - _err "cannot find zone to modify" + zone=$(_get_zone "$fulldomain") + if [ -z "$zone" ]; then + _err "cannot find zone:<$zone> to modify" return 1 fi - zone=${zone_and_id% *} - domain_id=${zone_and_id#* } # extract first part of domain key=${fulldomain%."$zone"} + key=$(echo "$key" | _lower_case) - _debug "zone:$zone id:$domain_id key:$key" + _debug "zone:$zone" + _debug "key:$key" # find previous record - # shellcheck disable=SC1004 - record_id=$(_get "${INFOMANIAK_API_URL}/1/domain/$domain_id/dns/record" | sed 's/.*"data":\[\(.*\)\]}/\1/; s/},{/}\ -{/g' | sed -n 's/.*"id":"*\([0-9]*\)"*.*"source_idn":"'"$fulldomain"'".*"target_idn":"'"$txtvalue"'".*/\1/p') + # shellcheck disable=SC2086 + response=$(_get "${INFOMANIAK_API_URL}/2/zones/${zone}/records" | sed 's/.*"data":\[\(.*\)\]}/\1/; s/},{/}{/g') + record_id=$(echo "$response" | sed -n 's/.*"id":"*\([0-9]*\)"*.*"source":"'"$key"'".*"target":"\\"'"$txtvalue"'\\"".*/\1/p') + _debug "key: $key" + _debug "txtvalue: $txtvalue" + _debug "record_id: $record_id" + if [ -z "$record_id" ]; then _err "could not find record to delete" + _debug "response: $response" return 1 fi - _debug "record_id: $record_id" # API call - response=$(_post "" "${INFOMANIAK_API_URL}/1/domain/$domain_id/dns/record/$record_id" "" DELETE) - if [ -n "$response" ] && echo "$response" | _contains '"result":"success"'; then - _info "Record deleted" - return 0 + response=$(_post "" "${INFOMANIAK_API_URL}/2/zones/${zone}/records/${record_id}" "" DELETE) + if [ -n "$response" ]; then + if [ ! "$(echo "$response" | _contains '"result":"success"')" ]; then + _info "Record deleted" + return 0 + fi fi - _err "could not delete record" + _err "Could not delete record." + _debug "Response: $response" return 1 } #################### Private functions below ################################## -_get_domain_id() { +_get_zone() { domain="$1" - + # Whatever the domain is, you can get the fqdn with the following. # shellcheck disable=SC1004 - _get "${INFOMANIAK_API_URL}/1/product?service_name=domain&customer_name=$domain" | sed 's/.*"data":\[{\(.*\)}\]}/\1/; s/,/\ -/g' | sed -n 's/^"id":\(.*\)/\1/p' -} - -_find_zone() { - zone="$1" - - # find domain in list, removing . parts sequentialy - while _contains "$zone" '\.'; do - _debug "testing $zone" - id=$(_get_domain_id "$zone") - if [ -n "$id" ]; then - echo "$zone $id" - return - fi - zone=${zone#*.} - done + response=$(_get "${INFOMANIAK_API_URL}/2/domains/${domain}/zones" | sed 's/.*\[{"fqdn"\:"\(.*\)/\1/') + echo "${response%%\"*}" } From 3252e0ce2e26fae5cc50edd9f61bd2b7a96b15b3 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Mon, 28 Jul 2025 12:14:12 -0400 Subject: [PATCH 03/38] Add outputs for PFX and PEM Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- deploy/localcopy.sh | 52 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/deploy/localcopy.sh b/deploy/localcopy.sh index 3b4fc219..38ae9599 100644 --- a/deploy/localcopy.sh +++ b/deploy/localcopy.sh @@ -8,13 +8,17 @@ # (e.g. The deploy-freenas script for TrueNAS Core/Scale # https://github.com/danb35/deploy-freenas/ ) # +# If the same file is configured for the certificate key +# and the certificate and/or full chain, a combined PEM file will +# be output instead. # # Environment variables to be utilized are as follows: # -# DEPLOY_LOCALCOPY_CERTIFICATE - /path/to/target/cert.cer # DEPLOY_LOCALCOPY_CERTKEY - /path/to/target/cert.key +# DEPLOY_LOCALCOPY_CERTIFICATE - /path/to/target/cert.cer # DEPLOY_LOCALCOPY_FULLCHAIN - /path/to/target/fullchain.cer # DEPLOY_LOCALCOPY_CA - /path/to/target/ca.cer +# DEPLOY_LOCALCOPY_PFX - /path/to/target/cert.pfx # DEPLOY_LOCALCOPY_RELOADCMD - "echo 'this is my cmd'" ######## Public functions ##################### @@ -26,18 +30,53 @@ localcopy_deploy() { _ccert="$3" _cca="$4" _cfullchain="$5" + _cpfx="$6" _debug _cdomain "$_cdomain" _debug _ckey "$_ckey" _debug _ccert "$_ccert" _debug _cca "$_cca" _debug _cfullchain "$_cfullchain" + _debug _cpfx "$_cpfx" _getdeployconf DEPLOY_LOCALCOPY_CERTIFICATE _getdeployconf DEPLOY_LOCALCOPY_CERTKEY _getdeployconf DEPLOY_LOCALCOPY_FULLCHAIN _getdeployconf DEPLOY_LOCALCOPY_CA _getdeployconf DEPLOY_LOCALCOPY_RELOADCMD + _getdeployconf DEPLOY_LOCALCOPY_PFX + _combined_target="" + _combined_srccert="" + + if [ "$DEPLOY_LOCALCOPY_CERTKEY" ] && + { [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_FULLCHAIN" ] || + [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; }; then + + _combined_target="$DEPLOY_LOCALCOPY_CERTKEY" + _savedeployconf DEPLOY_LOCALCOPY_CERTKEY "$DEPLOY_LOCALCOPY_CERTKEY" + + if [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then + _combined_srccert="$_ccert" + _savedeployconf DEPLOY_LOCALCOPY_CERTIFICATE "$DEPLOY_LOCALCOPY_CERTIFICATE" + DEPLOY_LOCALCOPY_CERTIFICATE="" + fi + if [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then + _combined_srccert="$_cfullchain" + _savedeployconf DEPLOY_LOCALCOPY_FULLCHAIN "$DEPLOY_LOCALCOPY_FULLCHAIN" + DEPLOY_LOCALCOPY_FULLCHAIN="" + fi + DEPLOY_LOCALCOPY_CERTKEY="" + _info "Creating combined PEM at $_combined_target" + _tmpfile="$(mktemp)" + if ! cat "$_combined_srccert" "$_ckey" >"$_tmpfile"; then + _err "Failed to build combined PEM file" + return 1 + fi + if ! mv "$_tmpfile" "$_combined_target"; then + _err "Failed to move combined PEM into place" + return 1 + fi + fi if [ "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then _info "Copying certificate" @@ -46,7 +85,6 @@ localcopy_deploy() { _err "Failed to copy certificate, aborting." return 1 fi - _savedeployconf DEPLOY_LOCALCOPY_CERTIFICATE "$DEPLOY_LOCALCOPY_CERTIFICATE" fi if [ "$DEPLOY_LOCALCOPY_CERTKEY" ]; then @@ -79,6 +117,16 @@ localcopy_deploy() { _savedeployconf DEPLOY_LOCALCOPY_CA "$DEPLOY_LOCALCOPY_CA" fi + if [ "$DEPLOY_LOCALCOPY_PFX" ]; then + _info "Copying PFX" + _debug "Copying $_cpfx to $DEPLOY_LOCALCOPY_PFX" + if ! eval "cp $_cpfx $DEPLOY_LOCALCOPY_PFX"; then + _err "Failed to copy PFX, aborting." + return 1 + fi + _savedeployconf DEPLOY_LOCALCOPY_PFX "$DEPLOY_LOCALCOPY_PFX" + fi + _reload=$DEPLOY_LOCALCOPY_RELOADCMD _debug "Running reloadcmd $_reload" From 45c4a98f1d8bcb28a051de8437be790a3a168266 Mon Sep 17 00:00:00 2001 From: Viktor Polyakov Date: Mon, 11 Aug 2025 16:05:09 +0300 Subject: [PATCH 04/38] feat: add message_thread_id to telegram notifications --- notify/telegram.sh | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) mode change 100644 => 100755 notify/telegram.sh diff --git a/notify/telegram.sh b/notify/telegram.sh old mode 100644 new mode 100755 index ccbd1533..97dd2861 --- a/notify/telegram.sh +++ b/notify/telegram.sh @@ -1,10 +1,14 @@ -#!/usr/bin/env sh +#!/usr/bin/bash #Support Telegram Bots #TELEGRAM_BOT_APITOKEN="" #TELEGRAM_BOT_CHATID="" #TELEGRAM_BOT_URLBASE="" +#TELEGRAM_BOT_THREADID="" + +# To get TELEGRAM_BOT_THREADID, just copy the link of the message from the thread. +# https://t.me/c/123456789/XXX/1520 - XXX is the TELEGRAM_BOT_THREADID telegram_send() { _subject="$1" @@ -28,6 +32,12 @@ telegram_send() { fi _saveaccountconf_mutable TELEGRAM_BOT_CHATID "$TELEGRAM_BOT_CHATID" + TELEGRAM_BOT_THREADID="${TELEGRAM_BOT_THREADID:-$(_readaccountconf_mutable TELEGRAM_BOT_THREADID)}" + if [ -z "$TELEGRAM_BOT_THREADID" ]; then + TELEGRAM_BOT_THREADID="" + fi + _saveaccountconf_mutable TELEGRAM_BOT_THREADID "$TELEGRAM_BOT_THREADID" + TELEGRAM_BOT_URLBASE="${TELEGRAM_BOT_URLBASE:-$(_readaccountconf_mutable TELEGRAM_BOT_URLBASE)}" if [ -z "$TELEGRAM_BOT_URLBASE" ]; then TELEGRAM_BOT_URLBASE="https://api.telegram.org" @@ -39,6 +49,9 @@ telegram_send() { _content="$(printf "*%s*\n%s" "$_subject" "$_content" | _json_encode)" _data="{\"text\": \"$_content\", " _data="$_data\"chat_id\": \"$TELEGRAM_BOT_CHATID\", " + if [ -n "$TELEGRAM_BOT_THREADID" ]; then + _data="$_data\"message_thread_id\": \"$TELEGRAM_BOT_THREADID\", " + fi _data="$_data\"parse_mode\": \"MarkdownV2\", " _data="$_data\"disable_web_page_preview\": \"1\"}" From 8713918bdb4061082e59b12a5e96ae03b0af0bcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Wed, 20 Aug 2025 12:41:27 +0800 Subject: [PATCH 05/38] Fix ipv6 cert cannot be found --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index d9ae208a..4b48036a 100755 --- a/acme.sh +++ b/acme.sh @@ -5565,7 +5565,7 @@ renewAll() { _set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT} _debug "_set_level" "$_set_level" export _ACME_IN_RENEWALL=1 - for di in "${CERT_HOME}"/*.*/; do + for di in "${CERT_HOME}"/*/; do _debug di "$di" if ! [ -d "$di" ]; then _debug "Not a directory, skipping: $di" From 2775def93aa25b978791bb63c896f4df30f5d1d1 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Wed, 3 Dec 2025 12:05:19 -0500 Subject: [PATCH 06/38] Use 'cat' instead of 'cp', removed use of temp file, keeps permissions Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- deploy/localcopy.sh | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/deploy/localcopy.sh b/deploy/localcopy.sh index 38ae9599..f420e62f 100644 --- a/deploy/localcopy.sh +++ b/deploy/localcopy.sh @@ -67,14 +67,21 @@ localcopy_deploy() { fi DEPLOY_LOCALCOPY_CERTKEY="" _info "Creating combined PEM at $_combined_target" - _tmpfile="$(mktemp)" - if ! cat "$_combined_srccert" "$_ckey" >"$_tmpfile"; then - _err "Failed to build combined PEM file" - return 1 - fi - if ! mv "$_tmpfile" "$_combined_target"; then - _err "Failed to move combined PEM into place" - return 1 + if [ -f "$_combined_target" ]; then + if ! cat "$_combined_srccert" "$_ckey" >"$_combined_target"; then + _err "Failed to create PEM file" + return 1 + fi + else + if ! touch "$_combined_target"; then + _err "Failed to create PEM file" + return 1 + fi + chmod 600 "$_combined_target" + if ! cat "$_combined_srccert" "$_ckey" >"$_combined_target"; then + _err "Failed to create PEM file" + return 1 + fi fi fi From f39a6fe517e56bc4b4e6bca190326366e8b98014 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Thu, 4 Dec 2025 11:06:50 -0500 Subject: [PATCH 07/38] Use cat instead of cp for all files Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- deploy/localcopy.sh | 80 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 61 insertions(+), 19 deletions(-) diff --git a/deploy/localcopy.sh b/deploy/localcopy.sh index f420e62f..ddb7d4b6 100644 --- a/deploy/localcopy.sh +++ b/deploy/localcopy.sh @@ -66,29 +66,35 @@ localcopy_deploy() { DEPLOY_LOCALCOPY_FULLCHAIN="" fi DEPLOY_LOCALCOPY_CERTKEY="" - _info "Creating combined PEM at $_combined_target" - if [ -f "$_combined_target" ]; then - if ! cat "$_combined_srccert" "$_ckey" >"$_combined_target"; then - _err "Failed to create PEM file" - return 1 - fi - else - if ! touch "$_combined_target"; then - _err "Failed to create PEM file" - return 1 - fi - chmod 600 "$_combined_target" - if ! cat "$_combined_srccert" "$_ckey" >"$_combined_target"; then + _info "Creating combined PEM" + _debug "Creating combined PEM at $_combined_target" + if ! [ -f "$_combined_target" ]; then + if ! ( + touch "$_combined_target" + chmod 600 "$_combined_target" + ); then _err "Failed to create PEM file" return 1 fi fi + if ! cat "$_combined_srccert" "$_ckey" >"$_combined_target"; then + _err "Failed to create PEM file" + return 1 + fi fi - if [ "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then _info "Copying certificate" _debug "Copying $_ccert to $DEPLOY_LOCALCOPY_CERTIFICATE" - if ! eval "cp $_ccert $DEPLOY_LOCALCOPY_CERTIFICATE"; then + if ! [ -f "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then + if ! ( + touch "$DEPLOY_LOCALCOPY_CERTIFICATE" + chmod 600 "$DEPLOY_LOCALCOPY_CERTIFICATE" + ); then + _err "Failed to copy certificate, aborting." + return 1 + fi + fi + if ! cat "$_ccert" >"$DEPLOY_LOCALCOPY_CERTIFICATE"; then _err "Failed to copy certificate, aborting." return 1 fi @@ -97,7 +103,16 @@ localcopy_deploy() { if [ "$DEPLOY_LOCALCOPY_CERTKEY" ]; then _info "Copying certificate key" _debug "Copying $_ckey to $DEPLOY_LOCALCOPY_CERTKEY" - if ! eval "cp $_ckey $DEPLOY_LOCALCOPY_CERTKEY"; then + if ! [ -f "$DEPLOY_LOCALCOPY_CERTKEY" ]; then + if ! ( + touch "$DEPLOY_LOCALCOPY_CERTKEY" + chmod 600 "$DEPLOY_LOCALCOPY_CERTKEY" + ); then + _err "Failed to copy certificate key, aborting." + return 1 + fi + fi + if ! cat "$_ckey" >"$DEPLOY_LOCALCOPY_CERTKEY"; then _err "Failed to copy certificate key, aborting." return 1 fi @@ -107,7 +122,16 @@ localcopy_deploy() { if [ "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then _info "Copying fullchain" _debug "Copying $_cfullchain to $DEPLOY_LOCALCOPY_FULLCHAIN" - if ! eval "cp $_cfullchain $DEPLOY_LOCALCOPY_FULLCHAIN"; then + if ! [ -f "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then + if ! ( + touch "$DEPLOY_LOCALCOPY_FULLCHAIN" + chmod 600 "$DEPLOY_LOCALCOPY_FULLCHAIN" + ); then + _err "Failed to copy fullchain, aborting." + return 1 + fi + fi + if ! cat "$_cfullchain" >"$DEPLOY_LOCALCOPY_FULLCHAIN"; then _err "Failed to copy fullchain, aborting." return 1 fi @@ -117,7 +141,16 @@ localcopy_deploy() { if [ "$DEPLOY_LOCALCOPY_CA" ]; then _info "Copying CA" _debug "Copying $_cca to $DEPLOY_LOCALCOPY_CA" - if ! eval "cp $_cca $DEPLOY_LOCALCOPY_CA"; then + if ! [ -f "$DEPLOY_LOCALCOPY_CA" ]; then + if ! ( + touch "$DEPLOY_LOCALCOPY_CA" + chmod 600 "$DEPLOY_LOCALCOPY_CA" + ); then + _err "Failed to copy CA, aborting." + return 1 + fi + fi + if ! cat "$_cca" >"$DEPLOY_LOCALCOPY_CA"; then _err "Failed to copy CA, aborting." return 1 fi @@ -127,7 +160,16 @@ localcopy_deploy() { if [ "$DEPLOY_LOCALCOPY_PFX" ]; then _info "Copying PFX" _debug "Copying $_cpfx to $DEPLOY_LOCALCOPY_PFX" - if ! eval "cp $_cpfx $DEPLOY_LOCALCOPY_PFX"; then + if ! [ -f "$DEPLOY_LOCALCOPY_PFX" ]; then + if ! ( + touch "$DEPLOY_LOCALCOPY_PFX" + chmod 600 "$DEPLOY_LOCALCOPY_PFX" + ); then + _err "Failed to copy PFX, aborting." + return 1 + fi + fi + if ! cat "$_cpfx" >"$DEPLOY_LOCALCOPY_PFX"; then _err "Failed to copy PFX, aborting." return 1 fi From 6ca19fb003b3393b066c5b8453e0cde8ad20e5f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Lal?= Date: Thu, 18 Dec 2025 09:50:31 +0100 Subject: [PATCH 08/38] Ensure ssh.sh sets 600 permissions on keyfile --- deploy/ssh.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deploy/ssh.sh b/deploy/ssh.sh index c66e2e19..3039c4ea 100644 --- a/deploy/ssh.sh +++ b/deploy/ssh.sh @@ -239,7 +239,7 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d fi else # ssh echo to the file - _cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE;" + _cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE; chmod 600 $DEPLOY_SSH_KEYFILE;" _info "will copy private key to remote file $DEPLOY_SSH_KEYFILE" if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if ! _ssh_remote_cmd "$_cmdstr"; then From 57db3889325ef95896d2677306e7d20d4d3360c8 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Mon, 22 Dec 2025 20:25:28 -0500 Subject: [PATCH 09/38] Docker with non-root using supercronic Replaces cronie with supercronic to allow non-root users to have cronjobs. Creates user/group acme:acme UID:1000/GID:1000 with home directory pointing to LE_CONFIG_HOME (default: /acme.sh) 'crontab' is generated in LE_CONFIG_HOME which is used by supercronic. Note that `acme.sh --installcronjob` and `--uninstallcronjob` when run as a non-root user will fail but neither of should be used in `daemon` mode anyway. Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- Dockerfile | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 36b2adac..64d14909 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,7 @@ RUN apk --no-cache add -f \ libidn \ jq \ yq-go \ - cronie + supercronic ENV LE_WORKING_DIR=/acmebin @@ -30,10 +30,12 @@ COPY ./deploy /install_acme.sh/deploy COPY ./dnsapi /install_acme.sh/dnsapi COPY ./notify /install_acme.sh/notify -RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/acme.sh --install || curl https://get.acme.sh | sh) && rm -rf /install_acme.sh/ +RUN addgroup -g 1000 acme && adduser -h $LE_CONFIG_HOME -s /bin/sh -G acme -D -H -u 1000 acme +RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/acme.sh --install || curl https://get.acme.sh | sh) && rm -rf /install_acme.sh/ -RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh && crontab -l | grep acme.sh | sed 's#> /dev/null#> /proc/1/fd/1 2>/proc/1/fd/2#' | crontab - +RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh \ + && crontab -l | grep acme.sh | sed 's#> /dev/null##' > $LE_CONFIG_HOME/crontab RUN for verb in help \ version \ @@ -72,12 +74,15 @@ RUN for verb in help \ RUN printf "%b" '#!'"/usr/bin/env sh\n \ if [ \"\$1\" = \"daemon\" ]; then \n \ - exec crond -n -s -m off \n \ + echo \"Running Supercronic using crontab at \$LE_CONFIG_HOME/crontab\" \n \ + exec -- /usr/bin/supercronic \"\$LE_CONFIG_HOME/crontab\" \n \ else \n \ exec -- \"\$@\"\n \ fi\n" >/entry.sh && chmod +x /entry.sh && chmod -R o+rwx $LE_WORKING_DIR && chmod -R o+rwx $LE_CONFIG_HOME VOLUME /acme.sh +USER 1000:1000 + ENTRYPOINT ["/entry.sh"] CMD ["--help"] From 6f5a0c5d5e961a7886cd3fae6ac72daab94787b1 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Tue, 23 Dec 2025 21:45:41 -0500 Subject: [PATCH 10/38] have entry.sh (instead of dockerfile) generate crontab file Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- Dockerfile | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index 64d14909..626f835d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,8 +34,7 @@ RUN addgroup -g 1000 acme && adduser -h $LE_CONFIG_HOME -s /bin/sh -G acme -D -H RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/acme.sh --install || curl https://get.acme.sh | sh) && rm -rf /install_acme.sh/ -RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh \ - && crontab -l | grep acme.sh | sed 's#> /dev/null##' > $LE_CONFIG_HOME/crontab +RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh RUN for verb in help \ version \ @@ -74,6 +73,13 @@ RUN for verb in help \ RUN printf "%b" '#!'"/usr/bin/env sh\n \ if [ \"\$1\" = \"daemon\" ]; then \n \ + if [ ! -f \"\$LE_CONFIG_HOME/crontab\" ]; then \n \ + echo \"\$LE_CONFIG_HOME/crontab not found, generating one\" \n \ + time=\$(date -u \"+%s\") \n \ + random_minute=\$((\$time % 60)) \n \ + random_hour=\$((\$time / 60 % 24)) \n \ + echo \"\$random_minute \$random_hour * * * \\\"\$LE_WORKING_DIR\\\"/acme.sh --cron --home \\\"\$LE_WORKING_DIR\\\" --config-home \\\"\$LE_CONFIG_HOME\\\"\" > \"\$LE_CONFIG_HOME\"/crontab \n \ + fi \n \ echo \"Running Supercronic using crontab at \$LE_CONFIG_HOME/crontab\" \n \ exec -- /usr/bin/supercronic \"\$LE_CONFIG_HOME/crontab\" \n \ else \n \ @@ -82,7 +88,5 @@ fi\n" >/entry.sh && chmod +x /entry.sh && chmod -R o+rwx $LE_WORKING_DIR && chmo VOLUME /acme.sh -USER 1000:1000 - ENTRYPOINT ["/entry.sh"] CMD ["--help"] From e03f8d3ad61d62f75ef34494fdb27109093debc4 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Sun, 28 Dec 2025 12:03:02 -0500 Subject: [PATCH 11/38] fix: savedeployconf for DEPLOY_LOCALCOPY_CERTIFICATE Co-authored-by: Kevin Hoser <45083826+hoser21@users.noreply.github.com> --- deploy/localcopy.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/deploy/localcopy.sh b/deploy/localcopy.sh index ddb7d4b6..b10fd1f0 100644 --- a/deploy/localcopy.sh +++ b/deploy/localcopy.sh @@ -98,6 +98,7 @@ localcopy_deploy() { _err "Failed to copy certificate, aborting." return 1 fi + _savedeployconf DEPLOY_LOCALCOPY_CERTIFICATE "$DEPLOY_LOCALCOPY_CERTIFICATE" fi if [ "$DEPLOY_LOCALCOPY_CERTKEY" ]; then From 4219f7b2f69364104b51ff9d3a3fac6dcb7d0c76 Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Tue, 30 Dec 2025 11:21:51 -0500 Subject: [PATCH 12/38] align logic to acme.sh installcert(), fix perms on non-key files Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- deploy/localcopy.sh | 57 +++++++-------------------------------------- 1 file changed, 8 insertions(+), 49 deletions(-) diff --git a/deploy/localcopy.sh b/deploy/localcopy.sh index b10fd1f0..9a1a0fcf 100644 --- a/deploy/localcopy.sh +++ b/deploy/localcopy.sh @@ -48,13 +48,13 @@ localcopy_deploy() { _combined_target="" _combined_srccert="" + # Create PEM file if [ "$DEPLOY_LOCALCOPY_CERTKEY" ] && { [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_FULLCHAIN" ] || [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; }; then _combined_target="$DEPLOY_LOCALCOPY_CERTKEY" _savedeployconf DEPLOY_LOCALCOPY_CERTKEY "$DEPLOY_LOCALCOPY_CERTKEY" - if [ "$DEPLOY_LOCALCOPY_CERTKEY" = "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then _combined_srccert="$_ccert" _savedeployconf DEPLOY_LOCALCOPY_CERTIFICATE "$DEPLOY_LOCALCOPY_CERTIFICATE" @@ -69,31 +69,18 @@ localcopy_deploy() { _info "Creating combined PEM" _debug "Creating combined PEM at $_combined_target" if ! [ -f "$_combined_target" ]; then - if ! ( - touch "$_combined_target" - chmod 600 "$_combined_target" - ); then - _err "Failed to create PEM file" - return 1 - fi + touch "$_combined_target" || return 1 + chmod 600 "$_combined_target" fi if ! cat "$_combined_srccert" "$_ckey" >"$_combined_target"; then _err "Failed to create PEM file" return 1 fi fi + if [ "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then _info "Copying certificate" _debug "Copying $_ccert to $DEPLOY_LOCALCOPY_CERTIFICATE" - if ! [ -f "$DEPLOY_LOCALCOPY_CERTIFICATE" ]; then - if ! ( - touch "$DEPLOY_LOCALCOPY_CERTIFICATE" - chmod 600 "$DEPLOY_LOCALCOPY_CERTIFICATE" - ); then - _err "Failed to copy certificate, aborting." - return 1 - fi - fi if ! cat "$_ccert" >"$DEPLOY_LOCALCOPY_CERTIFICATE"; then _err "Failed to copy certificate, aborting." return 1 @@ -105,13 +92,8 @@ localcopy_deploy() { _info "Copying certificate key" _debug "Copying $_ckey to $DEPLOY_LOCALCOPY_CERTKEY" if ! [ -f "$DEPLOY_LOCALCOPY_CERTKEY" ]; then - if ! ( - touch "$DEPLOY_LOCALCOPY_CERTKEY" - chmod 600 "$DEPLOY_LOCALCOPY_CERTKEY" - ); then - _err "Failed to copy certificate key, aborting." - return 1 - fi + touch "$DEPLOY_LOCALCOPY_CERTKEY" || return 1 + chmod 600 "$DEPLOY_LOCALCOPY_CERTKEY" fi if ! cat "$_ckey" >"$DEPLOY_LOCALCOPY_CERTKEY"; then _err "Failed to copy certificate key, aborting." @@ -123,15 +105,6 @@ localcopy_deploy() { if [ "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then _info "Copying fullchain" _debug "Copying $_cfullchain to $DEPLOY_LOCALCOPY_FULLCHAIN" - if ! [ -f "$DEPLOY_LOCALCOPY_FULLCHAIN" ]; then - if ! ( - touch "$DEPLOY_LOCALCOPY_FULLCHAIN" - chmod 600 "$DEPLOY_LOCALCOPY_FULLCHAIN" - ); then - _err "Failed to copy fullchain, aborting." - return 1 - fi - fi if ! cat "$_cfullchain" >"$DEPLOY_LOCALCOPY_FULLCHAIN"; then _err "Failed to copy fullchain, aborting." return 1 @@ -142,15 +115,6 @@ localcopy_deploy() { if [ "$DEPLOY_LOCALCOPY_CA" ]; then _info "Copying CA" _debug "Copying $_cca to $DEPLOY_LOCALCOPY_CA" - if ! [ -f "$DEPLOY_LOCALCOPY_CA" ]; then - if ! ( - touch "$DEPLOY_LOCALCOPY_CA" - chmod 600 "$DEPLOY_LOCALCOPY_CA" - ); then - _err "Failed to copy CA, aborting." - return 1 - fi - fi if ! cat "$_cca" >"$DEPLOY_LOCALCOPY_CA"; then _err "Failed to copy CA, aborting." return 1 @@ -162,13 +126,8 @@ localcopy_deploy() { _info "Copying PFX" _debug "Copying $_cpfx to $DEPLOY_LOCALCOPY_PFX" if ! [ -f "$DEPLOY_LOCALCOPY_PFX" ]; then - if ! ( - touch "$DEPLOY_LOCALCOPY_PFX" - chmod 600 "$DEPLOY_LOCALCOPY_PFX" - ); then - _err "Failed to copy PFX, aborting." - return 1 - fi + touch "$DEPLOY_LOCALCOPY_PFX" || return 1 + chmod 600 "$DEPLOY_LOCALCOPY_PFX" fi if ! cat "$_cpfx" >"$DEPLOY_LOCALCOPY_PFX"; then _err "Failed to copy PFX, aborting." From 6a98b9f81e9057cd0eda1427a446da20cc305d1d Mon Sep 17 00:00:00 2001 From: invario <67800603+invario@users.noreply.github.com> Date: Tue, 30 Dec 2025 12:44:46 -0500 Subject: [PATCH 13/38] chown /acme.sh to non-root user and set HOME to /acme.sh Signed-off-by: invario <67800603+invario@users.noreply.github.com> --- Dockerfile | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Dockerfile b/Dockerfile index 626f835d..15439e5a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -20,6 +20,8 @@ ENV LE_WORKING_DIR=/acmebin ENV LE_CONFIG_HOME=/acme.sh +ENV HOME=/acme.sh + ARG AUTO_UPGRADE=1 ENV AUTO_UPGRADE=$AUTO_UPGRADE @@ -36,6 +38,8 @@ RUN cd /install_acme.sh && ([ -f /install_acme.sh/acme.sh ] && /install_acme.sh/ RUN ln -s $LE_WORKING_DIR/acme.sh /usr/local/bin/acme.sh +RUN chown -R acme:acme $LE_CONFIG_HOME + RUN for verb in help \ version \ install \ From ef2089ceb1fc384971b7ab237c0816c255c6dbf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E6=89=87=E6=BB=91=E7=BF=94=E7=BF=BC?= Date: Fri, 2 Jan 2026 14:48:11 +0800 Subject: [PATCH 14/38] Update directory iteration pattern in acme.sh --- acme.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/acme.sh b/acme.sh index 4b48036a..fb1d3e12 100755 --- a/acme.sh +++ b/acme.sh @@ -5565,7 +5565,7 @@ renewAll() { _set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT} _debug "_set_level" "$_set_level" export _ACME_IN_RENEWALL=1 - for di in "${CERT_HOME}"/*/; do + for di in "${CERT_HOME}"/*[.:]*/; do _debug di "$di" if ! [ -d "$di" ]; then _debug "Not a directory, skipping: $di" From 282b048557f8f734792454238677bfb9bdb2c74a Mon Sep 17 00:00:00 2001 From: Patrick Zbinden Date: Wed, 14 Jan 2026 21:05:32 +0100 Subject: [PATCH 15/38] Fix dns_cyon to use correct regex --- dnsapi/dns_cyon.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dnsapi/dns_cyon.sh b/dnsapi/dns_cyon.sh index 0c74be2a..d4b6b6e8 100644 --- a/dnsapi/dns_cyon.sh +++ b/dnsapi/dns_cyon.sh @@ -332,11 +332,11 @@ _cyon_get_response_message() { } _cyon_get_response_status() { - _egrep_o '"status":[a-zA-z0-9]*' | cut -d : -f 2 + _egrep_o '"status":[a-zA-Z0-9]*' | cut -d : -f 2 } _cyon_get_validation_status() { - _egrep_o '"valid":[a-zA-z0-9]*' | cut -d : -f 2 + _egrep_o '"valid":[a-zA-Z0-9]*' | cut -d : -f 2 } _cyon_get_response_success() { @@ -344,7 +344,7 @@ _cyon_get_response_success() { } _cyon_get_environment_change_status() { - _egrep_o '"authenticated":[a-zA-z0-9]*' | cut -d : -f 2 + _egrep_o '"authenticated":[a-zA-Z0-9]*' | cut -d : -f 2 } _cyon_check_if_2fa_missed() { From 0cef5edac285a168fbfff3823a393118ea22258b Mon Sep 17 00:00:00 2001 From: neil Date: Wed, 21 Jan 2026 20:00:16 +0100 Subject: [PATCH 16/38] fix https://github.com/acmesh-official/acme.sh/issues/6196#issuecomment-3777530678 --- .github/workflows/DNS.yml | 22 +++++++++++----------- .github/workflows/DragonFlyBSD.yml | 2 +- .github/workflows/FreeBSD.yml | 2 +- .github/workflows/Haiku.yml | 2 +- .github/workflows/Linux.yml | 2 +- .github/workflows/MacOS.yml | 2 +- .github/workflows/NetBSD.yml | 2 +- .github/workflows/Omnios.yml | 2 +- .github/workflows/OpenBSD.yml | 2 +- .github/workflows/OpenIndiana.yml | 2 +- .github/workflows/PebbleStrict.yml | 4 ++-- .github/workflows/Solaris.yml | 2 +- .github/workflows/Ubuntu.yml | 2 +- .github/workflows/Windows.yml | 2 +- .github/workflows/dockerhub.yml | 2 +- .github/workflows/shellcheck.yml | 4 ++-- .github/workflows/wiki-monitor.yml | 2 +- acme.sh | 17 ++--------------- 18 files changed, 31 insertions(+), 44 deletions(-) diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml index 1a37b8a9..fbe1e61f 100644 --- a/.github/workflows/DNS.yml +++ b/.github/workflows/DNS.yml @@ -66,7 +66,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - name: Set env file @@ -114,7 +114,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install tools run: brew install socat - name: Clone acmetest @@ -165,7 +165,7 @@ jobs: - name: Set git to use LF run: | git config --global core.autocrlf false - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install cygwin base packages with chocolatey run: | choco config get cacheLocation @@ -224,7 +224,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/freebsd-vm@v1 @@ -279,7 +279,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/openbsd-vm@v1 @@ -334,7 +334,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/netbsd-vm@v1 @@ -390,7 +390,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/dragonflybsd-vm@v1 @@ -450,7 +450,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/solaris-vm@v1 @@ -508,7 +508,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/omnios-vm@v1 @@ -563,7 +563,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/openindiana-vm@v1 @@ -618,7 +618,7 @@ jobs: TokenName4: ${{ secrets.TokenName4}} TokenName5: ${{ secrets.TokenName5}} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/ - uses: vmactions/haiku-vm@v1 diff --git a/.github/workflows/DragonFlyBSD.yml b/.github/workflows/DragonFlyBSD.yml index dda8c99f..f3a85920 100644 --- a/.github/workflows/DragonFlyBSD.yml +++ b/.github/workflows/DragonFlyBSD.yml @@ -45,7 +45,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/FreeBSD.yml b/.github/workflows/FreeBSD.yml index 21123c4a..e9ccf7ac 100644 --- a/.github/workflows/FreeBSD.yml +++ b/.github/workflows/FreeBSD.yml @@ -51,7 +51,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/Haiku.yml b/.github/workflows/Haiku.yml index 1dbfc2c4..bfbde398 100644 --- a/.github/workflows/Haiku.yml +++ b/.github/workflows/Haiku.yml @@ -52,7 +52,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/Linux.yml b/.github/workflows/Linux.yml index f3352a41..9f3d3f38 100644 --- a/.github/workflows/Linux.yml +++ b/.github/workflows/Linux.yml @@ -33,7 +33,7 @@ jobs: TEST_PREFERRED_CHAIN: (STAGING) TEST_ACME_Server: "LetsEncrypt.org_test" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Clone acmetest run: | cd .. \ diff --git a/.github/workflows/MacOS.yml b/.github/workflows/MacOS.yml index f5d73ec9..21793c3e 100644 --- a/.github/workflows/MacOS.yml +++ b/.github/workflows/MacOS.yml @@ -44,7 +44,7 @@ jobs: CA_EMAIL: ${{ matrix.CA_EMAIL }} TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install tools run: brew install socat - name: Clone acmetest diff --git a/.github/workflows/NetBSD.yml b/.github/workflows/NetBSD.yml index 40421552..e8107d91 100644 --- a/.github/workflows/NetBSD.yml +++ b/.github/workflows/NetBSD.yml @@ -45,7 +45,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/Omnios.yml b/.github/workflows/Omnios.yml index 20eb24d7..a166e26b 100644 --- a/.github/workflows/Omnios.yml +++ b/.github/workflows/Omnios.yml @@ -51,7 +51,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/OpenBSD.yml b/.github/workflows/OpenBSD.yml index fab6e4fd..b34c795b 100644 --- a/.github/workflows/OpenBSD.yml +++ b/.github/workflows/OpenBSD.yml @@ -51,7 +51,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/OpenIndiana.yml b/.github/workflows/OpenIndiana.yml index abad376c..6447911b 100644 --- a/.github/workflows/OpenIndiana.yml +++ b/.github/workflows/OpenIndiana.yml @@ -51,7 +51,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/PebbleStrict.yml b/.github/workflows/PebbleStrict.yml index 729874ce..946d993a 100644 --- a/.github/workflows/PebbleStrict.yml +++ b/.github/workflows/PebbleStrict.yml @@ -33,7 +33,7 @@ jobs: TEST_CA: "Pebble Intermediate CA" steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install tools run: sudo apt-get install -y socat - name: Run Pebble @@ -58,7 +58,7 @@ jobs: TEST_IPCERT: 1 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install tools run: sudo apt-get install -y socat - name: Run Pebble diff --git a/.github/workflows/Solaris.yml b/.github/workflows/Solaris.yml index 2388da71..f5ce713b 100644 --- a/.github/workflows/Solaris.yml +++ b/.github/workflows/Solaris.yml @@ -51,7 +51,7 @@ jobs: TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: vmactions/cf-tunnel@v0 id: tunnel with: diff --git a/.github/workflows/Ubuntu.yml b/.github/workflows/Ubuntu.yml index e580828f..5ebf2d0d 100644 --- a/.github/workflows/Ubuntu.yml +++ b/.github/workflows/Ubuntu.yml @@ -70,7 +70,7 @@ jobs: TestingDomain: ${{ matrix.TestingDomain }} ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install tools run: sudo apt-get install -y socat wget - name: Start StepCA diff --git a/.github/workflows/Windows.yml b/.github/workflows/Windows.yml index c1fd1085..4c195917 100644 --- a/.github/workflows/Windows.yml +++ b/.github/workflows/Windows.yml @@ -49,7 +49,7 @@ jobs: - name: Set git to use LF run: | git config --global core.autocrlf false - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install cygwin base packages with chocolatey run: | choco config get cacheLocation diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml index 49173b4b..0d9046df 100644 --- a/.github/workflows/dockerhub.yml +++ b/.github/workflows/dockerhub.yml @@ -43,7 +43,7 @@ jobs: if: "contains(needs.CheckToken.outputs.hasToken, 'true')" steps: - name: checkout code - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: persist-credentials: false - name: Set up QEMU diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml index 746727d4..eb10b2b0 100644 --- a/.github/workflows/shellcheck.yml +++ b/.github/workflows/shellcheck.yml @@ -22,7 +22,7 @@ jobs: ShellCheck: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install Shellcheck run: sudo apt-get install -y shellcheck - name: DoShellcheck @@ -31,7 +31,7 @@ jobs: shfmt: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install shfmt run: curl -sSL https://github.com/mvdan/sh/releases/download/v3.1.2/shfmt_v3.1.2_linux_amd64 -o ~/shfmt && chmod +x ~/shfmt - name: shfmt diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml index 59cd0e5b..a706529a 100644 --- a/.github/workflows/wiki-monitor.yml +++ b/.github/workflows/wiki-monitor.yml @@ -9,7 +9,7 @@ jobs: if: github.actor != 'neilpang' steps: - name: Checkout wiki repository - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: repository: ${{ github.repository }}.wiki path: wiki diff --git a/acme.sh b/acme.sh index c66730cb..720e3c68 100755 --- a/acme.sh +++ b/acme.sh @@ -595,10 +595,6 @@ if [ "$(printf '\x41')" != 'A' ]; then _URGLY_PRINTF=1 fi -_ESCAPE_XARGS="" -if _exists xargs && [ "$(printf %s '\\x41' | xargs printf)" = 'A' ]; then - _ESCAPE_XARGS=1 -fi _h2b() { if _exists xxd; then @@ -618,17 +614,8 @@ _h2b() { jc="" _debug2 _URGLY_PRINTF "$_URGLY_PRINTF" if [ -z "$_URGLY_PRINTF" ]; then - if [ "$_ESCAPE_XARGS" ] && _exists xargs; then - _debug2 "xargs" - echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/g' | xargs printf - else - for h in $(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/ \1/g'); do - if [ -z "$h" ]; then - break - fi - printf "\x$h%s" - done - fi + # shellcheck disable=SC2059 + printf "$(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\{2\}\)/\\x\1/g')" else for c in $(echo "$hex" | _upper_case | sed 's/\([0-9A-F]\)/ \1/g'); do if [ -z "$ic" ]; then From bfd1f9bf6ce1a6aead0a711b7400e773010bed36 Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 16:43:26 +0100 Subject: [PATCH 17/38] [CLOUD-31] add acme.sh opusdns provider --- dnsapi/dns_opusdns.sh | 379 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100755 dnsapi/dns_opusdns.sh diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh new file mode 100755 index 00000000..cf088a07 --- /dev/null +++ b/dnsapi/dns_opusdns.sh @@ -0,0 +1,379 @@ +#!/usr/bin/env sh + +# shellcheck disable=SC2034 +dns_opusdns_info='OpusDNS.com +Site: OpusDNS.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_opusdns +Options: + OPUSDNS_API_Key API Key. Can be created at https://dashboard.opusdns.com/settings/api-keys + OPUSDNS_API_Endpoint API Endpoint URL. Default "https://api.opusdns.com". Optional. + OPUSDNS_TTL TTL for DNS challenge records in seconds. Default "60". Optional. + OPUSDNS_Polling_Interval DNS propagation check interval in seconds. Default "6". Optional. + OPUSDNS_Propagation_Timeout Maximum time to wait for DNS propagation in seconds. Default "120". Optional. +Issues: github.com/acmesh-official/acme.sh/issues/XXXX +Author: OpusDNS Team +' + +OPUSDNS_API_Endpoint_Default="https://api.opusdns.com" +OPUSDNS_TTL_Default=60 +OPUSDNS_Polling_Interval_Default=6 +OPUSDNS_Propagation_Timeout_Default=120 + +######## Public functions ########### + +# Add DNS TXT record +# Usage: dns_opusdns_add _acme-challenge.example.com "token_value" +dns_opusdns_add() { + fulldomain=$1 + txtvalue=$2 + + _info "Using OpusDNS DNS API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + # Load and validate credentials + OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}" + if [ -z "$OPUSDNS_API_Key" ]; then + _err "OPUSDNS_API_Key not set. Please set it and try again." + _err "You can create an API key at your OpusDNS dashboard." + return 1 + fi + + # Save credentials for future use + _saveaccountconf_mutable OPUSDNS_API_Key "$OPUSDNS_API_Key" + + # Load optional configuration + OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}" + if [ -z "$OPUSDNS_API_Endpoint" ]; then + OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default" + fi + _saveaccountconf_mutable OPUSDNS_API_Endpoint "$OPUSDNS_API_Endpoint" + + OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}" + if [ -z "$OPUSDNS_TTL" ]; then + OPUSDNS_TTL="$OPUSDNS_TTL_Default" + fi + _saveaccountconf_mutable OPUSDNS_TTL "$OPUSDNS_TTL" + + OPUSDNS_Polling_Interval="${OPUSDNS_Polling_Interval:-$OPUSDNS_Polling_Interval_Default}" + OPUSDNS_Propagation_Timeout="${OPUSDNS_Propagation_Timeout:-$OPUSDNS_Propagation_Timeout_Default}" + + _debug "API Endpoint: $OPUSDNS_API_Endpoint" + _debug "TTL: $OPUSDNS_TTL" + + # Detect zone from FQDN + if ! _get_zone "$fulldomain"; then + _err "Failed to detect zone for domain: $fulldomain" + return 1 + fi + + _info "Detected zone: $_zone" + _debug "Record name: $_record_name" + + # Add the TXT record + if ! _opusdns_add_record "$_zone" "$_record_name" "$txtvalue"; then + _err "Failed to add TXT record" + return 1 + fi + + _info "TXT record added successfully" + + # Wait for DNS propagation + if ! _opusdns_wait_for_propagation "$fulldomain" "$txtvalue"; then + _err "Warning: DNS record may not have propagated yet" + _err "Certificate issuance may fail. Please check your DNS configuration." + # Don't fail here - let ACME client decide + fi + + return 0 +} + +# Remove DNS TXT record +# Usage: dns_opusdns_rm _acme-challenge.example.com "token_value" +dns_opusdns_rm() { + fulldomain=$1 + txtvalue=$2 + + _info "Removing OpusDNS DNS record" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + # Load credentials + OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}" + OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}" + OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}" + + if [ -z "$OPUSDNS_API_Endpoint" ]; then + OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default" + fi + + if [ -z "$OPUSDNS_TTL" ]; then + OPUSDNS_TTL="$OPUSDNS_TTL_Default" + fi + + if [ -z "$OPUSDNS_API_Key" ]; then + _err "OPUSDNS_API_Key not found" + return 1 + fi + + # Detect zone from FQDN + if ! _get_zone "$fulldomain"; then + _err "Failed to detect zone for domain: $fulldomain" + # Don't fail cleanup - best effort + return 0 + fi + + _info "Detected zone: $_zone" + _debug "Record name: $_record_name" + + # Remove the TXT record (need to pass txtvalue) + if ! _opusdns_remove_record "$_zone" "$_record_name" "$txtvalue"; then + _err "Warning: Failed to remove TXT record (this is usually not critical)" + # Don't fail cleanup - best effort + return 0 + fi + + _info "TXT record removed successfully" + return 0 +} + +######## Private functions ########### + +# Detect zone from FQDN by querying OpusDNS API +# Sets global variables: _zone, _record_name +_get_zone() { + domain=$1 + _debug "Detecting zone for: $domain" + + # Remove trailing dot if present + domain=$(echo "$domain" | sed 's/\.$//') + + # Get all zones from OpusDNS with pagination support + export _H1="X-Api-Key: $OPUSDNS_API_Key" + + zones="" + page=1 + has_more=1 + + while [ $has_more -eq 1 ]; do + _debug2 "Fetching zones page $page" + response=$(_get "$OPUSDNS_API_Endpoint/v1/dns?page=$page&page_size=100") + if [ $? -ne 0 ]; then + _err "Failed to query zones from OpusDNS API (page $page)" + _debug "Response: $response" + return 1 + fi + + _debug2 "Zones response (page $page): $response" + + # Extract zone names from this page (try jq first, fallback to grep/sed) + if _exists jq; then + page_zones=$(echo "$response" | jq -r '.results[].name' 2>/dev/null | sed 's/\.$//') + has_next=$(echo "$response" | jq -r '.has_next_page // false' 2>/dev/null) + else + # Fallback: extract zone names using grep/sed + # Note: This simple parser does not handle escaped quotes in zone names. + # Zone names with escaped quotes are extremely rare and would require jq. + page_zones=$(echo "$response" | grep -oE '"name"[[:space:]]*:[[:space:]]*"[^"\\]*"' | sed 's/"name"[[:space:]]*:[[:space:]]*"\([^"\\]*\)"/\1/' | sed 's/\.$//') + has_next=$(echo "$response" | grep -oE '"has_next_page"[[:space:]]*:[[:space:]]*(true|false)' | grep -o 'true\|false') + fi + + # Append zones from this page + if [ -n "$page_zones" ]; then + if [ -z "$zones" ]; then + zones="$page_zones" + else + zones="$zones +$page_zones" + fi + fi + + # Check if there are more pages + if [ "$has_next" = "true" ]; then + page=$((page + 1)) + else + has_more=0 + fi + done + + if [ -z "$zones" ]; then + _err "No zones found in OpusDNS account" + _debug "API Response: $response" + return 1 + fi + + _debug2 "Available zones (all pages): $zones" + + # Find longest matching zone + _zone="" + _zone_length=0 + + for zone in $zones; do + zone_with_dot="${zone}." + if _endswith "$domain." "$zone_with_dot"; then + zone_length=${#zone} + if [ $zone_length -gt $_zone_length ]; then + _zone="$zone" + _zone_length=$zone_length + fi + fi + done + + if [ -z "$_zone" ]; then + _err "No matching zone found for domain: $domain" + _err "Available zones: $zones" + return 1 + fi + + # Calculate record name (subdomain part) + # Use parameter expansion instead of sed to avoid regex metacharacter issues + _record_name="${domain%.${_zone}}" + # Handle case where domain equals zone (remove trailing dot if present) + if [ "$_record_name" = "$domain" ]; then + _record_name="${domain%${_zone}}" + _record_name="${_record_name%.}" + fi + + if [ -z "$_record_name" ]; then + _record_name="@" + fi + + return 0 +} + +# Add TXT record using OpusDNS API +_opusdns_add_record() { + zone=$1 + record_name=$2 + txtvalue=$3 + + _debug "Adding TXT record: $record_name.$zone = $txtvalue" + + # Escape all JSON special characters in txtvalue + # Order matters: escape backslashes first, then other characters + escaped_value=$(printf '%s' "$txtvalue" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' | sed ':a;N;$!ba;s/\n/\\n/g') + + # Build JSON payload + # Note: TXT records need quotes around the value in rdata + json_payload="{\"ops\":[{\"op\":\"upsert\",\"record\":{\"name\":\"$record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$escaped_value\\\"\"}}]}" + + _debug2 "JSON payload: $json_payload" + + # Send PATCH request + export _H1="X-Api-Key: $OPUSDNS_API_Key" + export _H2="Content-Type: application/json" + + response=$(_post "$json_payload" "$OPUSDNS_API_Endpoint/v1/dns/$zone/records" "" "PATCH") + status=$? + + _debug2 "API Response: $response" + + if [ $status -ne 0 ]; then + _err "Failed to add TXT record" + _err "API Response: $response" + return 1 + fi + + # Check for error in response (OpusDNS returns JSON error even on failure) + # Use anchored pattern to avoid matching field names like "error_count" + if echo "$response" | grep -q '"error":'; then + _err "API returned error: $response" + return 1 + fi + + return 0 +} + +# Remove TXT record using OpusDNS API +_opusdns_remove_record() { + zone=$1 + record_name=$2 + txtvalue=$3 + + _debug "Removing TXT record: $record_name.$zone = $txtvalue" + + # Escape all JSON special characters in txtvalue (same as add) + escaped_value=$(printf '%s' "$txtvalue" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' | sed ':a;N;$!ba;s/\n/\\n/g') + + # Build JSON payload for removal - needs complete record specification + json_payload="{\"ops\":[{\"op\":\"remove\",\"record\":{\"name\":\"$record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$escaped_value\\\"\"}}]}" + + _debug2 "JSON payload: $json_payload" + + # Send PATCH request + export _H1="X-Api-Key: $OPUSDNS_API_Key" + export _H2="Content-Type: application/json" + + response=$(_post "$json_payload" "$OPUSDNS_API_Endpoint/v1/dns/$zone/records" "" "PATCH") + status=$? + + _debug2 "API Response: $response" + + if [ $status -ne 0 ]; then + _err "Failed to remove TXT record" + _err "API Response: $response" + return 1 + fi + + return 0 +} + +# Wait for DNS propagation by checking OpusDNS authoritative nameservers +_opusdns_wait_for_propagation() { + fulldomain=$1 + txtvalue=$2 + + _info "Waiting for DNS propagation to authoritative nameservers (max ${OPUSDNS_Propagation_Timeout}s)..." + + max_attempts=$((OPUSDNS_Propagation_Timeout / OPUSDNS_Polling_Interval)) + # Ensure at least one attempt even if interval > timeout + if [ "$max_attempts" -lt 1 ]; then + max_attempts=1 + fi + attempt=1 + + # OpusDNS authoritative nameservers + nameservers="ns1.opusdns.com ns2.opusdns.net" + + while [ $attempt -le $max_attempts ]; do + _debug "Propagation check attempt $attempt/$max_attempts" + + all_propagated=1 + + # Check all OpusDNS authoritative nameservers + for ns in $nameservers; do + if _exists dig; then + result=$(dig @$ns +short "$fulldomain" TXT 2>/dev/null | tr -d '"') + elif _exists nslookup; then + result=$(nslookup -type=TXT "$fulldomain" $ns 2>/dev/null | grep -A1 "text =" | tail -n1 | tr -d '"' | sed 's/^[[:space:]]*//') + else + _err "Neither dig nor nslookup found. Cannot verify DNS propagation." + return 1 + fi + + _debug2 "DNS query result from $ns: $result" + + if ! echo "$result" | grep -qF "$txtvalue"; then + _debug "Record not yet on $ns" + all_propagated=0 + else + _debug "Record found on $ns ✓" + fi + done + + if [ $all_propagated -eq 1 ]; then + _info "DNS record propagated to all OpusDNS nameservers!" + return 0 + fi + + if [ $attempt -lt $max_attempts ]; then + _debug "Record not propagated to all nameservers yet, waiting ${OPUSDNS_Polling_Interval}s..." + sleep "$OPUSDNS_Polling_Interval" + fi + + attempt=$((attempt + 1)) + done + + _err "DNS record did not propagate to all nameservers within ${OPUSDNS_Propagation_Timeout} seconds" + return 1 +} From 01c93b9bbd0b76488c8201876bb20ccbbfbb7cc3 Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 17:04:43 +0100 Subject: [PATCH 19/38] Fix shellcheck and shfmt issues - Add double quotes around variables to prevent globbing - Fix parameter expansion quoting in ${domain%.${_zone}} - Remove trailing whitespace for shfmt compliance --- dnsapi/dns_opusdns.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index cf088a07..feb5507e 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -102,7 +102,7 @@ dns_opusdns_rm() { OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}" OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}" OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}" - + if [ -z "$OPUSDNS_API_Endpoint" ]; then OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default" fi @@ -150,11 +150,11 @@ _get_zone() { # Get all zones from OpusDNS with pagination support export _H1="X-Api-Key: $OPUSDNS_API_Key" - + zones="" page=1 has_more=1 - + while [ $has_more -eq 1 ]; do _debug2 "Fetching zones page $page" response=$(_get "$OPUSDNS_API_Endpoint/v1/dns?page=$page&page_size=100") @@ -212,7 +212,7 @@ $page_zones" zone_with_dot="${zone}." if _endswith "$domain." "$zone_with_dot"; then zone_length=${#zone} - if [ $zone_length -gt $_zone_length ]; then + if [ "$zone_length" -gt "$_zone_length" ]; then _zone="$zone" _zone_length=$zone_length fi @@ -227,10 +227,10 @@ $page_zones" # Calculate record name (subdomain part) # Use parameter expansion instead of sed to avoid regex metacharacter issues - _record_name="${domain%.${_zone}}" + _record_name="${domain%."${_zone}"}" # Handle case where domain equals zone (remove trailing dot if present) if [ "$_record_name" = "$domain" ]; then - _record_name="${domain%${_zone}}" + _record_name="${domain%"${_zone}"}" _record_name="${_record_name%.}" fi @@ -343,9 +343,9 @@ _opusdns_wait_for_propagation() { # Check all OpusDNS authoritative nameservers for ns in $nameservers; do if _exists dig; then - result=$(dig @$ns +short "$fulldomain" TXT 2>/dev/null | tr -d '"') + result=$(dig @"$ns" +short "$fulldomain" TXT 2>/dev/null | tr -d '"') elif _exists nslookup; then - result=$(nslookup -type=TXT "$fulldomain" $ns 2>/dev/null | grep -A1 "text =" | tail -n1 | tr -d '"' | sed 's/^[[:space:]]*//') + result=$(nslookup -type=TXT "$fulldomain" "$ns" 2>/dev/null | grep -A1 "text =" | tail -n1 | tr -d '"' | sed 's/^[[:space:]]*//') else _err "Neither dig nor nslookup found. Cannot verify DNS propagation." return 1 From dc65223da1c99dcd7351f7c4d6e9c410f9ff7386 Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 17:10:12 +0100 Subject: [PATCH 20/38] Remove all trailing whitespace for shfmt compliance --- dnsapi/dns_opusdns.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index feb5507e..a57d2188 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -177,7 +177,7 @@ _get_zone() { page_zones=$(echo "$response" | grep -oE '"name"[[:space:]]*:[[:space:]]*"[^"\\]*"' | sed 's/"name"[[:space:]]*:[[:space:]]*"\([^"\\]*\)"/\1/' | sed 's/\.$//') has_next=$(echo "$response" | grep -oE '"has_next_page"[[:space:]]*:[[:space:]]*(true|false)' | grep -o 'true\|false') fi - + # Append zones from this page if [ -n "$page_zones" ]; then if [ -z "$zones" ]; then @@ -187,7 +187,7 @@ _get_zone() { $page_zones" fi fi - + # Check if there are more pages if [ "$has_next" = "true" ]; then page=$((page + 1)) @@ -233,7 +233,7 @@ $page_zones" _record_name="${domain%"${_zone}"}" _record_name="${_record_name%.}" fi - + if [ -z "$_record_name" ]; then _record_name="@" fi @@ -339,7 +339,7 @@ _opusdns_wait_for_propagation() { _debug "Propagation check attempt $attempt/$max_attempts" all_propagated=1 - + # Check all OpusDNS authoritative nameservers for ns in $nameservers; do if _exists dig; then From 30c9332327b2c3f5a2a5aacf7bc5389b76cc126d Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 17:28:41 +0100 Subject: [PATCH 21/38] Remove custom DNS propagation check acme.sh handles DNS propagation checking internally via --dnssleep and _check_dns_record. Custom propagation checks are unnecessary and can conflict with acme.sh's own timing. Removed: - _opusdns_wait_for_propagation() function - OPUSDNS_Polling_Interval option - OPUSDNS_Propagation_Timeout option Script is now consistent with other DNS API implementations (Cloudflare, AWS, etc.) which don't have custom propagation checks. --- dnsapi/dns_opusdns.sh | 75 ------------------------------------------- 1 file changed, 75 deletions(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index a57d2188..6c23a904 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -8,16 +8,12 @@ Options: OPUSDNS_API_Key API Key. Can be created at https://dashboard.opusdns.com/settings/api-keys OPUSDNS_API_Endpoint API Endpoint URL. Default "https://api.opusdns.com". Optional. OPUSDNS_TTL TTL for DNS challenge records in seconds. Default "60". Optional. - OPUSDNS_Polling_Interval DNS propagation check interval in seconds. Default "6". Optional. - OPUSDNS_Propagation_Timeout Maximum time to wait for DNS propagation in seconds. Default "120". Optional. Issues: github.com/acmesh-official/acme.sh/issues/XXXX Author: OpusDNS Team ' OPUSDNS_API_Endpoint_Default="https://api.opusdns.com" OPUSDNS_TTL_Default=60 -OPUSDNS_Polling_Interval_Default=6 -OPUSDNS_Propagation_Timeout_Default=120 ######## Public functions ########### @@ -55,9 +51,6 @@ dns_opusdns_add() { fi _saveaccountconf_mutable OPUSDNS_TTL "$OPUSDNS_TTL" - OPUSDNS_Polling_Interval="${OPUSDNS_Polling_Interval:-$OPUSDNS_Polling_Interval_Default}" - OPUSDNS_Propagation_Timeout="${OPUSDNS_Propagation_Timeout:-$OPUSDNS_Propagation_Timeout_Default}" - _debug "API Endpoint: $OPUSDNS_API_Endpoint" _debug "TTL: $OPUSDNS_TTL" @@ -77,14 +70,6 @@ dns_opusdns_add() { fi _info "TXT record added successfully" - - # Wait for DNS propagation - if ! _opusdns_wait_for_propagation "$fulldomain" "$txtvalue"; then - _err "Warning: DNS record may not have propagated yet" - _err "Certificate issuance may fail. Please check your DNS configuration." - # Don't fail here - let ACME client decide - fi - return 0 } @@ -317,63 +302,3 @@ _opusdns_remove_record() { return 0 } - -# Wait for DNS propagation by checking OpusDNS authoritative nameservers -_opusdns_wait_for_propagation() { - fulldomain=$1 - txtvalue=$2 - - _info "Waiting for DNS propagation to authoritative nameservers (max ${OPUSDNS_Propagation_Timeout}s)..." - - max_attempts=$((OPUSDNS_Propagation_Timeout / OPUSDNS_Polling_Interval)) - # Ensure at least one attempt even if interval > timeout - if [ "$max_attempts" -lt 1 ]; then - max_attempts=1 - fi - attempt=1 - - # OpusDNS authoritative nameservers - nameservers="ns1.opusdns.com ns2.opusdns.net" - - while [ $attempt -le $max_attempts ]; do - _debug "Propagation check attempt $attempt/$max_attempts" - - all_propagated=1 - - # Check all OpusDNS authoritative nameservers - for ns in $nameservers; do - if _exists dig; then - result=$(dig @"$ns" +short "$fulldomain" TXT 2>/dev/null | tr -d '"') - elif _exists nslookup; then - result=$(nslookup -type=TXT "$fulldomain" "$ns" 2>/dev/null | grep -A1 "text =" | tail -n1 | tr -d '"' | sed 's/^[[:space:]]*//') - else - _err "Neither dig nor nslookup found. Cannot verify DNS propagation." - return 1 - fi - - _debug2 "DNS query result from $ns: $result" - - if ! echo "$result" | grep -qF "$txtvalue"; then - _debug "Record not yet on $ns" - all_propagated=0 - else - _debug "Record found on $ns ✓" - fi - done - - if [ $all_propagated -eq 1 ]; then - _info "DNS record propagated to all OpusDNS nameservers!" - return 0 - fi - - if [ $attempt -lt $max_attempts ]; then - _debug "Record not propagated to all nameservers yet, waiting ${OPUSDNS_Polling_Interval}s..." - sleep "$OPUSDNS_Polling_Interval" - fi - - attempt=$((attempt + 1)) - done - - _err "DNS record did not propagate to all nameservers within ${OPUSDNS_Propagation_Timeout} seconds" - return 1 -} From 25a3ee48df9c6af194bcc2c0dbd65b441481b0cb Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 17:58:09 +0100 Subject: [PATCH 22/38] Fix zone pagination parsing - Fixed jq path: .pagination.has_next_page instead of .has_next_page - Fixed grep fallback: remove rrsets before extracting zone names to avoid matching nested 'name' fields - Simplified has_next_page detection with simple grep -q - Added debug output for page zones and has_next status --- dnsapi/dns_opusdns.sh | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index 6c23a904..f6bd507e 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -151,18 +151,27 @@ _get_zone() { _debug2 "Zones response (page $page): $response" - # Extract zone names from this page (try jq first, fallback to grep/sed) + # Extract zone names from this page + # The API returns: {"results":[{"name":"zone.com.",...},...],"pagination":{"has_next_page":true,...}} if _exists jq; then page_zones=$(echo "$response" | jq -r '.results[].name' 2>/dev/null | sed 's/\.$//') - has_next=$(echo "$response" | jq -r '.has_next_page // false' 2>/dev/null) + has_next=$(echo "$response" | jq -r '.pagination.has_next_page // false' 2>/dev/null) else # Fallback: extract zone names using grep/sed - # Note: This simple parser does not handle escaped quotes in zone names. - # Zone names with escaped quotes are extremely rare and would require jq. - page_zones=$(echo "$response" | grep -oE '"name"[[:space:]]*:[[:space:]]*"[^"\\]*"' | sed 's/"name"[[:space:]]*:[[:space:]]*"\([^"\\]*\)"/\1/' | sed 's/\.$//') - has_next=$(echo "$response" | grep -oE '"has_next_page"[[:space:]]*:[[:space:]]*(true|false)' | grep -o 'true\|false') + # Extract only top-level zone names from results array (before rrsets) + # Pattern: "results":[{"...","name":"zonename.com.","domain_parts": + page_zones=$(echo "$response" | sed 's/,"rrsets":\[[^]]*\]//g' | grep -o '"results":\[.*\]' | grep -o '"name":"[^"]*"' | sed 's/"name":"//g;s/"//g;s/\.$//') + # Extract has_next_page from pagination object + if echo "$response" | grep -q '"has_next_page":true'; then + has_next="true" + else + has_next="false" + fi fi + _debug2 "Page $page zones: $page_zones" + _debug2 "Has next page: $has_next" + # Append zones from this page if [ -n "$page_zones" ]; then if [ -z "$zones" ]; then From 163eb1acb9a7f1170e407d14358599a563dbd68f Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 18:11:01 +0100 Subject: [PATCH 23/38] Simplify zone detection with API check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instead of fetching all zones and matching, iterate through domain parts and check each against the API until a valid zone is found. Same approach as GoDaddy DNS plugin. Example: _acme-challenge.test.example.com - Try: test.example.com → 404 - Try: example.com → 200 ✓ → zone found! Script reduced from 304 to 255 lines. --- dnsapi/dns_opusdns.sh | 112 ++++++++++-------------------------------- 1 file changed, 27 insertions(+), 85 deletions(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index f6bd507e..b9337b89 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -124,7 +124,8 @@ dns_opusdns_rm() { ######## Private functions ########### -# Detect zone from FQDN by querying OpusDNS API +# Detect zone from FQDN by checking against OpusDNS API +# Iterates through domain parts until a valid zone is found # Sets global variables: _zone, _record_name _get_zone() { domain=$1 @@ -133,100 +134,41 @@ _get_zone() { # Remove trailing dot if present domain=$(echo "$domain" | sed 's/\.$//') - # Get all zones from OpusDNS with pagination support export _H1="X-Api-Key: $OPUSDNS_API_Key" - zones="" - page=1 - has_more=1 - - while [ $has_more -eq 1 ]; do - _debug2 "Fetching zones page $page" - response=$(_get "$OPUSDNS_API_Endpoint/v1/dns?page=$page&page_size=100") - if [ $? -ne 0 ]; then - _err "Failed to query zones from OpusDNS API (page $page)" - _debug "Response: $response" + # Start from position 2 (skip first part like _acme-challenge) + i=2 + p=1 + while true; do + # Extract potential zone (domain parts from position i onwards) + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) + _debug "Trying zone: $h" + + if [ -z "$h" ]; then + # No more parts to try + _err "Could not find a valid zone for: $domain" return 1 fi - _debug2 "Zones response (page $page): $response" - - # Extract zone names from this page - # The API returns: {"results":[{"name":"zone.com.",...},...],"pagination":{"has_next_page":true,...}} - if _exists jq; then - page_zones=$(echo "$response" | jq -r '.results[].name' 2>/dev/null | sed 's/\.$//') - has_next=$(echo "$response" | jq -r '.pagination.has_next_page // false' 2>/dev/null) - else - # Fallback: extract zone names using grep/sed - # Extract only top-level zone names from results array (before rrsets) - # Pattern: "results":[{"...","name":"zonename.com.","domain_parts": - page_zones=$(echo "$response" | sed 's/,"rrsets":\[[^]]*\]//g' | grep -o '"results":\[.*\]' | grep -o '"name":"[^"]*"' | sed 's/"name":"//g;s/"//g;s/\.$//') - # Extract has_next_page from pagination object - if echo "$response" | grep -q '"has_next_page":true'; then - has_next="true" - else - has_next="false" - fi - fi - - _debug2 "Page $page zones: $page_zones" - _debug2 "Has next page: $has_next" - - # Append zones from this page - if [ -n "$page_zones" ]; then - if [ -z "$zones" ]; then - zones="$page_zones" - else - zones="$zones -$page_zones" - fi - fi + # Check if this zone exists in OpusDNS + response=$(_get "$OPUSDNS_API_Endpoint/v1/dns/$h") - # Check if there are more pages - if [ "$has_next" = "true" ]; then - page=$((page + 1)) - else - has_more=0 + if _contains "$response" '"name"'; then + # Zone found + _record_name=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + _zone="$h" + _debug "Found zone: $_zone" + _debug "Record name: $_record_name" + return 0 fi - done - - if [ -z "$zones" ]; then - _err "No zones found in OpusDNS account" - _debug "API Response: $response" - return 1 - fi - _debug2 "Available zones (all pages): $zones" - - # Find longest matching zone - _zone="" - _zone_length=0 - - for zone in $zones; do - zone_with_dot="${zone}." - if _endswith "$domain." "$zone_with_dot"; then - zone_length=${#zone} - if [ "$zone_length" -gt "$_zone_length" ]; then - _zone="$zone" - _zone_length=$zone_length - fi - fi + _debug "$h not found, trying next" + p="$i" + i=$(_math "$i" + 1) done - if [ -z "$_zone" ]; then - _err "No matching zone found for domain: $domain" - _err "Available zones: $zones" - return 1 - fi - - # Calculate record name (subdomain part) - # Use parameter expansion instead of sed to avoid regex metacharacter issues - _record_name="${domain%."${_zone}"}" - # Handle case where domain equals zone (remove trailing dot if present) - if [ "$_record_name" = "$domain" ]; then - _record_name="${domain%"${_zone}"}" - _record_name="${_record_name%.}" - fi + return 1 +} if [ -z "$_record_name" ]; then _record_name="@" From 9e584e346debc9f8d008ab5bcf11489b8926c22c Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 18:13:09 +0100 Subject: [PATCH 24/38] Refactor: Add _opusdns_api helper, simplify code - Added _opusdns_api() for all API requests - Added _opusdns_init() for config initialization - Removed duplicate code in add/rm functions - Removed dead code (orphaned lines 173-178) - Script now 158 lines (was 255, originally 379) --- dnsapi/dns_opusdns.sh | 223 ++++++++++++------------------------------ 1 file changed, 63 insertions(+), 160 deletions(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index b9337b89..19205256 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -18,7 +18,6 @@ OPUSDNS_TTL_Default=60 ######## Public functions ########### # Add DNS TXT record -# Usage: dns_opusdns_add _acme-challenge.example.com "token_value" dns_opusdns_add() { fulldomain=$1 txtvalue=$2 @@ -27,44 +26,17 @@ dns_opusdns_add() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" - # Load and validate credentials - OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}" - if [ -z "$OPUSDNS_API_Key" ]; then - _err "OPUSDNS_API_Key not set. Please set it and try again." - _err "You can create an API key at your OpusDNS dashboard." + if ! _opusdns_init; then return 1 fi - # Save credentials for future use - _saveaccountconf_mutable OPUSDNS_API_Key "$OPUSDNS_API_Key" - - # Load optional configuration - OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}" - if [ -z "$OPUSDNS_API_Endpoint" ]; then - OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default" - fi - _saveaccountconf_mutable OPUSDNS_API_Endpoint "$OPUSDNS_API_Endpoint" - - OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}" - if [ -z "$OPUSDNS_TTL" ]; then - OPUSDNS_TTL="$OPUSDNS_TTL_Default" - fi - _saveaccountconf_mutable OPUSDNS_TTL "$OPUSDNS_TTL" - - _debug "API Endpoint: $OPUSDNS_API_Endpoint" - _debug "TTL: $OPUSDNS_TTL" - - # Detect zone from FQDN if ! _get_zone "$fulldomain"; then - _err "Failed to detect zone for domain: $fulldomain" return 1 fi - _info "Detected zone: $_zone" - _debug "Record name: $_record_name" + _info "Zone: $_zone, Record: $_record_name" - # Add the TXT record - if ! _opusdns_add_record "$_zone" "$_record_name" "$txtvalue"; then + if ! _opusdns_api PATCH "/v1/dns/$_zone/records" "{\"ops\":[{\"op\":\"upsert\",\"record\":{\"name\":\"$_record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$txtvalue\\\"\"}}]}"; then _err "Failed to add TXT record" return 1 fi @@ -74,7 +46,6 @@ dns_opusdns_add() { } # Remove DNS TXT record -# Usage: dns_opusdns_rm _acme-challenge.example.com "token_value" dns_opusdns_rm() { fulldomain=$1 txtvalue=$2 @@ -83,38 +54,19 @@ dns_opusdns_rm() { _debug fulldomain "$fulldomain" _debug txtvalue "$txtvalue" - # Load credentials - OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}" - OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}" - OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}" - - if [ -z "$OPUSDNS_API_Endpoint" ]; then - OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default" - fi - - if [ -z "$OPUSDNS_TTL" ]; then - OPUSDNS_TTL="$OPUSDNS_TTL_Default" - fi - - if [ -z "$OPUSDNS_API_Key" ]; then - _err "OPUSDNS_API_Key not found" + if ! _opusdns_init; then return 1 fi - # Detect zone from FQDN if ! _get_zone "$fulldomain"; then - _err "Failed to detect zone for domain: $fulldomain" - # Don't fail cleanup - best effort + _err "Zone not found, cleanup skipped" return 0 fi - _info "Detected zone: $_zone" - _debug "Record name: $_record_name" + _info "Zone: $_zone, Record: $_record_name" - # Remove the TXT record (need to pass txtvalue) - if ! _opusdns_remove_record "$_zone" "$_record_name" "$txtvalue"; then - _err "Warning: Failed to remove TXT record (this is usually not critical)" - # Don't fail cleanup - best effort + if ! _opusdns_api PATCH "/v1/dns/$_zone/records" "{\"ops\":[{\"op\":\"remove\",\"record\":{\"name\":\"$_record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$txtvalue\\\"\"}}]}"; then + _err "Warning: Failed to remove TXT record" return 0 fi @@ -124,132 +76,83 @@ dns_opusdns_rm() { ######## Private functions ########### -# Detect zone from FQDN by checking against OpusDNS API -# Iterates through domain parts until a valid zone is found -# Sets global variables: _zone, _record_name -_get_zone() { - domain=$1 - _debug "Detecting zone for: $domain" - - # Remove trailing dot if present - domain=$(echo "$domain" | sed 's/\.$//') - - export _H1="X-Api-Key: $OPUSDNS_API_Key" - - # Start from position 2 (skip first part like _acme-challenge) - i=2 - p=1 - while true; do - # Extract potential zone (domain parts from position i onwards) - h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - _debug "Trying zone: $h" - - if [ -z "$h" ]; then - # No more parts to try - _err "Could not find a valid zone for: $domain" - return 1 - fi - - # Check if this zone exists in OpusDNS - response=$(_get "$OPUSDNS_API_Endpoint/v1/dns/$h") - - if _contains "$response" '"name"'; then - # Zone found - _record_name=$(printf "%s" "$domain" | cut -d . -f 1-"$p") - _zone="$h" - _debug "Found zone: $_zone" - _debug "Record name: $_record_name" - return 0 - fi +# Initialize and validate configuration +_opusdns_init() { + OPUSDNS_API_Key="${OPUSDNS_API_Key:-$(_readaccountconf_mutable OPUSDNS_API_Key)}" + OPUSDNS_API_Endpoint="${OPUSDNS_API_Endpoint:-$(_readaccountconf_mutable OPUSDNS_API_Endpoint)}" + OPUSDNS_TTL="${OPUSDNS_TTL:-$(_readaccountconf_mutable OPUSDNS_TTL)}" - _debug "$h not found, trying next" - p="$i" - i=$(_math "$i" + 1) - done + if [ -z "$OPUSDNS_API_Key" ]; then + _err "OPUSDNS_API_Key not set" + return 1 + fi - return 1 -} + [ -z "$OPUSDNS_API_Endpoint" ] && OPUSDNS_API_Endpoint="$OPUSDNS_API_Endpoint_Default" + [ -z "$OPUSDNS_TTL" ] && OPUSDNS_TTL="$OPUSDNS_TTL_Default" - if [ -z "$_record_name" ]; then - _record_name="@" - fi + _saveaccountconf_mutable OPUSDNS_API_Key "$OPUSDNS_API_Key" + _saveaccountconf_mutable OPUSDNS_API_Endpoint "$OPUSDNS_API_Endpoint" + _saveaccountconf_mutable OPUSDNS_TTL "$OPUSDNS_TTL" + _debug "Endpoint: $OPUSDNS_API_Endpoint" return 0 } -# Add TXT record using OpusDNS API -_opusdns_add_record() { - zone=$1 - record_name=$2 - txtvalue=$3 +# Make API request +# Usage: _opusdns_api METHOD PATH [DATA] +_opusdns_api() { + method=$1 + path=$2 + data=$3 - _debug "Adding TXT record: $record_name.$zone = $txtvalue" - - # Escape all JSON special characters in txtvalue - # Order matters: escape backslashes first, then other characters - escaped_value=$(printf '%s' "$txtvalue" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' | sed ':a;N;$!ba;s/\n/\\n/g') - - # Build JSON payload - # Note: TXT records need quotes around the value in rdata - json_payload="{\"ops\":[{\"op\":\"upsert\",\"record\":{\"name\":\"$record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$escaped_value\\\"\"}}]}" - - _debug2 "JSON payload: $json_payload" - - # Send PATCH request export _H1="X-Api-Key: $OPUSDNS_API_Key" export _H2="Content-Type: application/json" - response=$(_post "$json_payload" "$OPUSDNS_API_Endpoint/v1/dns/$zone/records" "" "PATCH") - status=$? - - _debug2 "API Response: $response" + url="$OPUSDNS_API_Endpoint$path" + _debug2 "API: $method $url" + [ -n "$data" ] && _debug2 "Data: $data" - if [ $status -ne 0 ]; then - _err "Failed to add TXT record" - _err "API Response: $response" - return 1 + if [ -n "$data" ]; then + response=$(_post "$data" "$url" "" "$method") + else + response=$(_get "$url") fi - # Check for error in response (OpusDNS returns JSON error even on failure) - # Use anchored pattern to avoid matching field names like "error_count" - if echo "$response" | grep -q '"error":'; then - _err "API returned error: $response" + if [ $? -ne 0 ]; then + _err "API request failed" + _debug "Response: $response" return 1 fi + _debug2 "Response: $response" return 0 } -# Remove TXT record using OpusDNS API -_opusdns_remove_record() { - zone=$1 - record_name=$2 - txtvalue=$3 - - _debug "Removing TXT record: $record_name.$zone = $txtvalue" - - # Escape all JSON special characters in txtvalue (same as add) - escaped_value=$(printf '%s' "$txtvalue" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g' -e 's/ /\\t/g' | sed ':a;N;$!ba;s/\n/\\n/g') - - # Build JSON payload for removal - needs complete record specification - json_payload="{\"ops\":[{\"op\":\"remove\",\"record\":{\"name\":\"$record_name\",\"type\":\"TXT\",\"ttl\":$OPUSDNS_TTL,\"rdata\":\"\\\"$escaped_value\\\"\"}}]}" - - _debug2 "JSON payload: $json_payload" - - # Send PATCH request - export _H1="X-Api-Key: $OPUSDNS_API_Key" - export _H2="Content-Type: application/json" +# Detect zone from FQDN +# Sets: _zone, _record_name +_get_zone() { + domain=$(echo "$1" | sed 's/\.$//') + _debug "Finding zone for: $domain" - response=$(_post "$json_payload" "$OPUSDNS_API_Endpoint/v1/dns/$zone/records" "" "PATCH") - status=$? + i=2 + p=1 + while true; do + h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) - _debug2 "API Response: $response" + if [ -z "$h" ]; then + _err "No valid zone found for: $domain" + return 1 + fi - if [ $status -ne 0 ]; then - _err "Failed to remove TXT record" - _err "API Response: $response" - return 1 - fi + _debug "Trying: $h" + if _opusdns_api GET "/v1/dns/$h" && _contains "$response" '"name"'; then + _zone="$h" + _record_name=$(printf "%s" "$domain" | cut -d . -f 1-"$p") + [ -z "$_record_name" ] && _record_name="@" + return 0 + fi - return 0 + p="$i" + i=$(_math "$i" + 1) + done } From 2e85e6f9bb0587450d9fea755203538c6dedab13 Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Thu, 22 Jan 2026 18:13:51 +0100 Subject: [PATCH 25/38] Fix zone detection: check for dnssec_status instead of name The error response also contains 'name' in 'zone_name' field, causing false positives. Check for 'dnssec_status' which only exists in valid zone responses. --- dnsapi/dns_opusdns.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index 19205256..2ef401eb 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -145,7 +145,7 @@ _get_zone() { fi _debug "Trying: $h" - if _opusdns_api GET "/v1/dns/$h" && _contains "$response" '"name"'; then + if _opusdns_api GET "/v1/dns/$h" && _contains "$response" '"dnssec_status"'; then _zone="$h" _record_name=$(printf "%s" "$domain" | cut -d . -f 1-"$p") [ -z "$_record_name" ] && _record_name="@" From 9c245eb37a2f6568a5f316ca7ba23a230acf4452 Mon Sep 17 00:00:00 2001 From: Kilian Ries Date: Fri, 23 Jan 2026 09:20:00 +0100 Subject: [PATCH 26/38] fix: start zone detection from i=1 per acme.sh convention --- dnsapi/dns_opusdns.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_opusdns.sh b/dnsapi/dns_opusdns.sh index 2ef401eb..37177696 100755 --- a/dnsapi/dns_opusdns.sh +++ b/dnsapi/dns_opusdns.sh @@ -134,7 +134,7 @@ _get_zone() { domain=$(echo "$1" | sed 's/\.$//') _debug "Finding zone for: $domain" - i=2 + i=1 p=1 while true; do h=$(printf "%s" "$domain" | cut -d . -f "$i"-100) From e44809c18074aeb886e25aea9adb29aa194c4027 Mon Sep 17 00:00:00 2001 From: neil Date: Sat, 24 Jan 2026 07:49:18 +0100 Subject: [PATCH 27/38] Update acme.sh --- acme.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/acme.sh b/acme.sh index 720e3c68..7a3c35a8 100755 --- a/acme.sh +++ b/acme.sh @@ -595,7 +595,6 @@ if [ "$(printf '\x41')" != 'A' ]; then _URGLY_PRINTF=1 fi - _h2b() { if _exists xxd; then if _contains "$(xxd --help 2>&1)" "assumes -c30"; then From 477277bd2dbab0d4f4d3db9592e979fc553679b3 Mon Sep 17 00:00:00 2001 From: JF DAGUIN <74184010+jf-lines@users.noreply.github.com> Date: Fri, 30 Jan 2026 23:00:25 +0100 Subject: [PATCH 28/38] Rewrite token scope and URL to add one Updated comments for clarity and formatting. --- dnsapi/dns_infomaniak.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/dnsapi/dns_infomaniak.sh b/dnsapi/dns_infomaniak.sh index 34795888..0ae32b47 100755 --- a/dnsapi/dns_infomaniak.sh +++ b/dnsapi/dns_infomaniak.sh @@ -9,12 +9,13 @@ Issues: github.com/acmesh-official/acme.sh/issues/3188 ' -# To use this API you need visit the API dashboard of your account -# once logged into https://manager.infomaniak.com add /api/dashboard to the URL -# +# To use this API you need visit the API dashboard of your account. # Note: the URL looks like this: -# https://manager.infomaniak.com/v3//api/dashboard -# Then generate a token with the scope Domain +# https://manager.infomaniak.com/v3//ng/profile/user/token/list +# Then generate a token with following scopes : +# - domain:read +# - dns:read +# - dns:write # this is given as an environment variable INFOMANIAK_API_TOKEN # base variables From 6a60695549c6417afb7ef73e0b5b00879376b4e0 Mon Sep 17 00:00:00 2001 From: David Gallay Date: Tue, 3 Feb 2026 10:51:42 +0100 Subject: [PATCH 29/38] Allowing panos deploy-hook to only depend on PANOS_KEY. Previous version add bugs that were not properly using the _api_key. It also enforced to provide PANOS_USER and PANOS_PASSWORD which can be very constraining. PANOS_KEY now has precedence. If not provided, the script falls back to PANOS_USER and PANOS_PASSWORD. --- deploy/panos.sh | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/deploy/panos.sh b/deploy/panos.sh index c54d21fe..019d8c62 100644 --- a/deploy/panos.sh +++ b/deploy/panos.sh @@ -207,13 +207,12 @@ panos_deploy() { fi # PANOS_KEY - _getdeployconf PANOS_KEY if [ "$PANOS_KEY" ]; then - _debug "Detected saved key." - _panos_key=$PANOS_KEY + _debug "Detected ENV variable PANOS_KEY. Saving to file." + _savedeployconf PANOS_KEY "$PANOS_KEY" 1 else - _debug "No key detected" - unset _panos_key + _debug "Attempting to load variable PANOS_KEY from file." + _getdeployconf PANOS_KEY fi # PANOS_TEMPLATE @@ -256,6 +255,7 @@ panos_deploy() { _panos_host=$PANOS_HOST _panos_user=$PANOS_USER _panos_pass=$PANOS_PASS + _panos_key=$PANOS_KEY _panos_template=$PANOS_TEMPLATE _panos_template_stack=$PANOS_TEMPLATE_STACK _panos_vsys=$PANOS_VSYS @@ -271,12 +271,6 @@ panos_deploy() { if [ -z "$_panos_host" ]; then _err "No host found. If this is your first time deploying, please set PANOS_HOST in ENV variables. You can delete it after you have successfully deployed the certs." return 1 - elif [ -z "$_panos_user" ]; then - _err "No user found. If this is your first time deploying, please set PANOS_USER in ENV variables. You can delete it after you have successfully deployed the certs." - return 1 - elif [ -z "$_panos_pass" ]; then - _err "No password found. If this is your first time deploying, please set PANOS_PASS in ENV variables. You can delete it after you have successfully deployed the certs." - return 1 else # Use certificate name based on the first domain on the certificate if no custom certificate name is set if [ -z "$_panos_certname" ]; then @@ -286,6 +280,13 @@ panos_deploy() { # Generate a new API key if no valid API key is found if [ -z "$_panos_key" ]; then + if [ -z "$_panos_user" ]; then + _err "No user found. If this is your first time deploying, please set PANOS_USER in ENV variables. You can delete it after you have successfully deployed the certs." + return 1 + elif [ -z "$_panos_pass" ]; then + _err "No password found. If this is your first time deploying, please set PANOS_PASS in ENV variables. You can delete it after you have successfully deployed the certs." + return 1 + fi _debug "**** Generating new PANOS API KEY ****" deployer keygen _savedeployconf PANOS_KEY "$_panos_key" 1 From 61e986f23c212211010488365d10aa775a3feb9d Mon Sep 17 00:00:00 2001 From: dga-nagra <147379886+dga-nagra@users.noreply.github.com> Date: Wed, 4 Feb 2026 11:01:44 +0100 Subject: [PATCH 30/38] Conditionnaly change permissions (#1) --- deploy/ssh.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/deploy/ssh.sh b/deploy/ssh.sh index 3039c4ea..848380a5 100644 --- a/deploy/ssh.sh +++ b/deploy/ssh.sh @@ -238,8 +238,10 @@ then rm -rf \"\$fn\"; echo \"Backup \$fn deleted as older than 180 days\"; fi; d return $_err_code fi else + # If file doesn't exist, create it and change its permissions. + _cmdstr="$_cmdstr test ! -f $DEPLOY_SSH_KEYFILE && touch $DEPLOY_SSH_KEYFILE && chmod 600 $DEPLOY_SSH_KEYFILE;" # ssh echo to the file - _cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE; chmod 600 $DEPLOY_SSH_KEYFILE;" + _cmdstr="$_cmdstr echo \"$(cat "$_ckey")\" > $DEPLOY_SSH_KEYFILE;" _info "will copy private key to remote file $DEPLOY_SSH_KEYFILE" if [ "$DEPLOY_SSH_MULTI_CALL" = "yes" ]; then if ! _ssh_remote_cmd "$_cmdstr"; then From 7236ba2d7c594a79111d1f62b42ef5fcedb9f4d6 Mon Sep 17 00:00:00 2001 From: alexandergott-afk Date: Thu, 5 Feb 2026 09:20:51 +0100 Subject: [PATCH 32/38] Update dns_nsupdate.sh --- dnsapi/dns_nsupdate.sh | 62 ++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 21 deletions(-) diff --git a/dnsapi/dns_nsupdate.sh b/dnsapi/dns_nsupdate.sh index d5dbbcbc..e2df39a3 100755 --- a/dnsapi/dns_nsupdate.sh +++ b/dnsapi/dns_nsupdate.sh @@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_nsupdate Options: NSUPDATE_SERVER Server hostname. Default: "localhost". NSUPDATE_SERVER_PORT Server port. Default: "53". - NSUPDATE_KEY File path to TSIG key. + NSUPDATE_KEY File path to TSIG key. Default: "" NSUPDATE_ZONE Domain zone to update. Optional. ' @@ -22,8 +22,6 @@ dns_nsupdate_add() { NSUPDATE_ZONE="${NSUPDATE_ZONE:-$(_readaccountconf_mutable NSUPDATE_ZONE)}" NSUPDATE_OPT="${NSUPDATE_OPT:-$(_readaccountconf_mutable NSUPDATE_OPT)}" - _checkKeyFile || return 1 - # save the dns server and key to the account conf file. _saveaccountconf_mutable NSUPDATE_SERVER "${NSUPDATE_SERVER}" _saveaccountconf_mutable NSUPDATE_SERVER_PORT "${NSUPDATE_SERVER_PORT}" @@ -33,6 +31,7 @@ dns_nsupdate_add() { [ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost" [ -n "${NSUPDATE_SERVER_PORT}" ] || NSUPDATE_SERVER_PORT=53 + [ -n "${NSUPDATE_KEY}" ] || NSUPDATE_KEY="" [ -n "${NSUPDATE_OPT}" ] || NSUPDATE_OPT="" _info "adding ${fulldomain}. 60 in txt \"${txtvalue}\"" @@ -40,19 +39,36 @@ dns_nsupdate_add() { [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D" if [ -z "${NSUPDATE_ZONE}" ]; then #shellcheck disable=SC2086 - nsupdate -k "${NSUPDATE_KEY}" $nsdebug $NSUPDATE_OPT < Date: Sat, 7 Feb 2026 22:10:48 +0800 Subject: [PATCH 33/38] Change shebang to use env for portability --- notify/telegram.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notify/telegram.sh b/notify/telegram.sh index 97dd2861..c6532dc6 100755 --- a/notify/telegram.sh +++ b/notify/telegram.sh @@ -1,4 +1,4 @@ -#!/usr/bin/bash +#!/usr/bin/env sh #Support Telegram Bots From 4807df0c3e24786d42d1f07579313e2c557414a4 Mon Sep 17 00:00:00 2001 From: alexandergott-afk Date: Mon, 9 Feb 2026 10:03:26 +0100 Subject: [PATCH 34/38] Fix Tab --- dnsapi/dns_nsupdate.sh | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/dnsapi/dns_nsupdate.sh b/dnsapi/dns_nsupdate.sh index e2df39a3..c2fa8fc1 100755 --- a/dnsapi/dns_nsupdate.sh +++ b/dnsapi/dns_nsupdate.sh @@ -39,36 +39,36 @@ dns_nsupdate_add() { [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D" if [ -z "${NSUPDATE_ZONE}" ]; then #shellcheck disable=SC2086 - if [ -z "${NSUPDATE_KEY}" ]; then - nsupdate $nsdebug $NSUPDATE_OPT < Date: Mon, 9 Feb 2026 10:39:27 +0100 Subject: [PATCH 35/38] too many spaces removed --- dnsapi/dns_nsupdate.sh | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dnsapi/dns_nsupdate.sh b/dnsapi/dns_nsupdate.sh index c2fa8fc1..9b14553b 100755 --- a/dnsapi/dns_nsupdate.sh +++ b/dnsapi/dns_nsupdate.sh @@ -40,13 +40,13 @@ dns_nsupdate_add() { if [ -z "${NSUPDATE_ZONE}" ]; then #shellcheck disable=SC2086 if [ -z "${NSUPDATE_KEY}" ]; then - nsupdate $nsdebug $NSUPDATE_OPT < Date: Thu, 12 Feb 2026 15:24:45 +0100 Subject: [PATCH 36/38] Update dns_nsupdate.sh --- dnsapi/dns_nsupdate.sh | 87 +++++++++++++++++++++++++----------------- 1 file changed, 51 insertions(+), 36 deletions(-) diff --git a/dnsapi/dns_nsupdate.sh b/dnsapi/dns_nsupdate.sh index 9b14553b..0b54b358 100755 --- a/dnsapi/dns_nsupdate.sh +++ b/dnsapi/dns_nsupdate.sh @@ -34,42 +34,49 @@ dns_nsupdate_add() { [ -n "${NSUPDATE_KEY}" ] || NSUPDATE_KEY="" [ -n "${NSUPDATE_OPT}" ] || NSUPDATE_OPT="" + NSUPDATE_SERVER_LIST=$(printf "%s" "$NSUPDATE_SERVER" | tr ',' ' ') + _info "adding ${fulldomain}. 60 in txt \"${txtvalue}\"" [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_1" ] && nsdebug="-d" [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D" - if [ -z "${NSUPDATE_ZONE}" ]; then - #shellcheck disable=SC2086 - if [ -z "${NSUPDATE_KEY}" ]; then - nsupdate $nsdebug $NSUPDATE_OPT < Date: Thu, 12 Feb 2026 15:27:23 +0100 Subject: [PATCH 37/38] Allow more than one DNS server for HA environments --- dnsapi/dns_nsupdate.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_nsupdate.sh b/dnsapi/dns_nsupdate.sh index 0b54b358..cc57cc38 100755 --- a/dnsapi/dns_nsupdate.sh +++ b/dnsapi/dns_nsupdate.sh @@ -98,9 +98,9 @@ dns_nsupdate_rm() { [ -n "${NSUPDATE_SERVER}" ] || NSUPDATE_SERVER="localhost" [ -n "${NSUPDATE_SERVER_PORT}" ] || NSUPDATE_SERVER_PORT=53 [ -n "${NSUPDATE_KEY}" ] || NSUPDATE_KEY="" - + NSUPDATE_SERVER_LIST=$(printf "%s" "$NSUPDATE_SERVER" | tr ',' ' ') - + _info "removing ${fulldomain}. txt" [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_1" ] && nsdebug="-d" [ -n "$DEBUG" ] && [ "$DEBUG" -ge "$DEBUG_LEVEL_2" ] && nsdebug="-D" From 83424e7ba4c8e116ca5d39bf352c84258482d8c0 Mon Sep 17 00:00:00 2001 From: alexandergott-afk Date: Thu, 12 Feb 2026 15:32:53 +0100 Subject: [PATCH 38/38] Add the information from my last accepted pull that the TSIG key is optional. --- dnsapi/dns_nsupdate.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dnsapi/dns_nsupdate.sh b/dnsapi/dns_nsupdate.sh index cc57cc38..8d7fe2c0 100755 --- a/dnsapi/dns_nsupdate.sh +++ b/dnsapi/dns_nsupdate.sh @@ -6,7 +6,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_nsupdate Options: NSUPDATE_SERVER Server hostname. Default: "localhost". NSUPDATE_SERVER_PORT Server port. Default: "53". - NSUPDATE_KEY File path to TSIG key. Default: "" + NSUPDATE_KEY File path to TSIG key. Default: "". Optional. NSUPDATE_ZONE Domain zone to update. Optional. '