From 010bd1111a7f44c02f6d8415a1ab915cd145226d Mon Sep 17 00:00:00 2001
From: Tom Sommer
Date: Sun, 24 May 2026 22:20:40 +0200
Subject: [PATCH 01/18] Improve Simply.com API (#6933)
Improve Simply.com API (#6933)
---
dnsapi/dns_simply.sh | 92 +++++++++++++++++++++++++++-----------------
1 file changed, 57 insertions(+), 35 deletions(-)
diff --git a/dnsapi/dns_simply.sh b/dnsapi/dns_simply.sh
index e0ad16e2..74e891ad 100644
--- a/dnsapi/dns_simply.sh
+++ b/dnsapi/dns_simply.sh
@@ -8,11 +8,7 @@ Options:
SIMPLY_ApiKey API Key
'
-#SIMPLY_Api="https://api.simply.com/2/"
-SIMPLY_Api_Default="https://api.simply.com/2"
-
-#This is used for determining success of REST call
-SIMPLY_SUCCESS_CODE='"status":200'
+SIMPLY_Api="https://api.simply.com/2"
######## Public functions #####################
#Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
@@ -72,7 +68,16 @@ dns_simply_rm() {
return 1
fi
- records=$(echo "$response" | tr '{' "\n" | grep 'record_id\|type\|data\|\name' | sed 's/\"record_id/;\"record_id/' | tr "\n" ' ' | tr -d ' ' | tr ';' ' ')
+ case "$_simply_http_code" in
+ 2*) ;;
+ *)
+ _err "Failed to fetch DNS records (HTTP $_simply_http_code)"
+ _err "$response"
+ return 1
+ ;;
+ esac
+
+ records=$(echo "$response" | tr '{' "\n" | grep -E 'record_id|type|data|name' | sed 's/\"record_id/;\"record_id/' | tr "\n" ' ' | tr -d ' ' | tr ';' ' ')
nr_of_deleted_records=0
_info "Fetching txt record"
@@ -95,7 +100,7 @@ dns_simply_rm() {
if [ "$record_id" -gt 0 ]; then
- if ! _simply_delete_record "$_domain" "$_sub_domain" "$record_id"; then
+ if ! _simply_delete_record "$_domain" "$record_id"; then
_err "Record with id $record_id could not be deleted"
return 1
fi
@@ -122,14 +127,9 @@ dns_simply_rm() {
#################### Private functions below ##################################
_simply_load_config() {
- SIMPLY_Api="${SIMPLY_Api:-$(_readaccountconf_mutable SIMPLY_Api)}"
SIMPLY_AccountName="${SIMPLY_AccountName:-$(_readaccountconf_mutable SIMPLY_AccountName)}"
SIMPLY_ApiKey="${SIMPLY_ApiKey:-$(_readaccountconf_mutable SIMPLY_ApiKey)}"
- if [ -z "$SIMPLY_Api" ]; then
- SIMPLY_Api="$SIMPLY_Api_Default"
- fi
-
if [ -z "$SIMPLY_AccountName" ] || [ -z "$SIMPLY_ApiKey" ]; then
SIMPLY_AccountName=""
SIMPLY_ApiKey=""
@@ -144,9 +144,6 @@ _simply_load_config() {
}
_simply_save_config() {
- if [ "$SIMPLY_Api" != "$SIMPLY_Api_Default" ]; then
- _saveaccountconf_mutable SIMPLY_Api "$SIMPLY_Api"
- fi
_saveaccountconf_mutable SIMPLY_AccountName "$SIMPLY_AccountName"
_saveaccountconf_mutable SIMPLY_ApiKey "$SIMPLY_ApiKey"
}
@@ -163,26 +160,39 @@ _simply_get_all_records() {
_get_root() {
domain=$1
+
+ if ! _simply_rest GET "my/products/"; then
+ return 1
+ fi
+
+ case "$_simply_http_code" in
+ 2*) ;;
+ *)
+ _err "Failed to fetch product list (HTTP $_simply_http_code)"
+ _err "$response"
+ return 1
+ ;;
+ esac
+
i=2
p=1
while true; do
h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
if [ -z "$h" ]; then
- #not valid
return 1
fi
- if ! _simply_rest GET "my/products/$h/dns/"; then
- return 1
- fi
+ _domain=$(printf "%s" "$response" | tr '}' '\n' |
+ grep -F -e "\"object\":\"$h\"" -e "\"name\":\"$h\"" -e "\"name_idn\":\"$h\"" |
+ sed -n 's/.*"object":"\([^"]*\)".*/\1/p' |
+ _head_n 1)
- if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then
- _debug "$h not found"
- else
+ if [ -n "$_domain" ]; then
_sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
- _domain="$h"
return 0
fi
+
+ _debug "No Simply.com product found for $h"
p="$i"
i=$(_math "$i" + 1)
done
@@ -194,39 +204,44 @@ _simply_add_record() {
sub_domain=$2
txtval=$3
- data="{\"name\": \"$sub_domain\", \"type\":\"TXT\", \"data\": \"$txtval\", \"priority\":0, \"ttl\": 3600}"
+ data="{\"name\": \"$sub_domain\", \"type\":\"TXT\", \"data\": \"$txtval\", \"priority\":0, \"ttl\": 120}"
if ! _simply_rest POST "my/products/$domain/dns/records/" "$data"; then
- _err "Adding record not successfull!"
+ _err "Adding record not successful!"
return 1
fi
- if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then
- _err "Call to API not sucessfull, see below message for more details"
+ case "$_simply_http_code" in
+ 2*) ;;
+ *)
+ _err "Call to API not successful (HTTP $_simply_http_code), see below message for more details"
_err "$response"
return 1
- fi
+ ;;
+ esac
return 0
}
_simply_delete_record() {
domain=$1
- sub_domain=$2
- record_id=$3
+ record_id=$2
_debug record_id "Delete record with id $record_id"
if ! _simply_rest DELETE "my/products/$domain/dns/records/$record_id/"; then
- _err "Deleting record not successfull!"
+ _err "Deleting record not successful!"
return 1
fi
- if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then
- _err "Call to API not sucessfull, see below message for more details"
+ case "$_simply_http_code" in
+ 2*) ;;
+ *)
+ _err "Call to API not successful (HTTP $_simply_http_code), see below message for more details"
_err "$response"
return 1
- fi
+ ;;
+ esac
return 0
}
@@ -248,17 +263,24 @@ _simply_rest() {
export _H2="Content-Type: application/json"
+ : >"$HTTP_HEADER"
+
if [ "$m" != "GET" ]; then
response="$(_post "$data" "$SIMPLY_Api/$ep" "" "$m")"
else
response="$(_get "$SIMPLY_Api/$ep")"
fi
- if [ "$?" != "0" ]; then
+ _ret="$?"
+ unset _H1 _H2
+
+ if [ "$_ret" != "0" ]; then
_err "error $ep"
return 1
fi
+ _simply_http_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d' ' -f2 | tr -d '\r\n')"
+
response="$(echo "$response" | _normalizeJson)"
_debug2 response "$response"
From ce07759cede1fddc70d303200137bc157e04e2fd Mon Sep 17 00:00:00 2001
From: Markus Ebner
Date: Sun, 24 May 2026 22:24:00 +0200
Subject: [PATCH 02/18] [dnsapi] add IP-Projects dns hook (#6959)
---
dnsapi/dns_ipprojects.sh | 91 ++++++++++++++++++++++++++++++++++++++++
1 file changed, 91 insertions(+)
create mode 100644 dnsapi/dns_ipprojects.sh
diff --git a/dnsapi/dns_ipprojects.sh b/dnsapi/dns_ipprojects.sh
new file mode 100644
index 00000000..dadd05f0
--- /dev/null
+++ b/dnsapi/dns_ipprojects.sh
@@ -0,0 +1,91 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+dns_ipprojects_info='IP-Projects DNS
+Site: ip-projects.de/
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_ipprojects
+Options:
+ IPP_Apikey API Key
+Issues: github.com/acmesh-official/acme.sh/issues/6958
+Author: Markus Ebner
+'
+
+IPP_Apikey="${IPP_Apikey:-$(_readaccountconf_mutable IPP_Apikey)}"
+IPP_API="https://api.ip-projects.de/v1/dns/acme"
+
+######## Public functions ########
+
+dns_ipprojects_add() {
+ fulldomain="$1"
+ txtvalue="$2"
+
+ _info "Using IP-Projects DNS API to add record"
+ _debug fulldomain "$fulldomain"
+ _debug txtvalue "$txtvalue"
+
+ if ! _IPP_load_credentials; then
+ return 1
+ fi
+
+ _IPP_api_request "add" "$fulldomain" "$txtvalue"
+}
+
+dns_ipprojects_rm() {
+ fulldomain="$1"
+ txtvalue="$2"
+
+ _info "Using IP-Projects DNS API to remove record"
+ _debug fulldomain "$fulldomain"
+ _debug txtvalue "$txtvalue"
+
+ if ! _IPP_load_credentials; then
+ return 1
+ fi
+
+ _IPP_api_request "remove" "$fulldomain" "$txtvalue"
+}
+
+######## Private helpers ########
+
+_IPP_load_credentials() {
+ IPP_Apikey="${IPP_Apikey:-$(_readaccountconf_mutable IPP_Apikey)}"
+
+ if [ -z "$IPP_Apikey" ]; then
+ _err "You must export IPP_Apikey"
+ _err "e.g.: export IPP_Apikey=\"your_api_key\""
+ return 1
+ fi
+
+ _saveaccountconf_mutable IPP_Apikey "$IPP_Apikey"
+ return 0
+}
+
+_IPP_api_request() {
+ action="$1"
+ domain="$2"
+ value="$3"
+
+ url="$IPP_API/$action"
+
+ data="{\"domain\":\"$domain\",\"key\":\"$domain\",\"value\":\"$value\"}"
+ _debug url "$url"
+ _debug data "$data"
+ export _H1="X-API-Key: $IPP_Apikey"
+
+ response="$(_post "$data" "$url" "" "POST" "application/json")"
+ ret="$?"
+ _ipprojects_last_http_code=$(grep "^HTTP" "${HTTP_HEADER}" | _tail_n 1 | cut -d " " -f 2 | tr -d '\r\n')
+
+ _debug response "$response"
+
+ if [ "$ret" != "0" ]; then
+ _err "HTTP request failed"
+ return 1
+ fi
+
+ if [ "$_ipprojects_last_http_code" != "200" ]; then
+ _err "API returned an error [code: ${_ipprojects_last_http_code}]"
+ return 1
+ fi
+
+ return 0
+}
From d9ce7fefa1f3106b39101b7864980f90d2616d9a Mon Sep 17 00:00:00 2001
From: "Simon V." <218359733+sim0n-v@users.noreply.github.com>
Date: Sun, 24 May 2026 22:25:52 +0200
Subject: [PATCH 03/18] ARI - Add support for switching ACME Server during
renewal (#6983)
* ARI - Add support for switching ACME Server during renewal
https://github.com/acmesh-official/acme.sh/issues/6964
* Restore old condition while adding malformed
---
acme.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/acme.sh b/acme.sh
index aa9d34a0..f67749c0 100755
--- a/acme.sh
+++ b/acme.sh
@@ -4895,7 +4895,7 @@ issue() {
# (Let's Encrypt) may also reject with a malformed error if the prior cert
# was issued by a different issuer / different CA. Retry without "replaces"
# whenever the failure mentions ARI or the replaces field.
- if [ "$_replaces_certID" ] && { _contains "$response" "alreadyReplaced" || _contains "$response" "'replaces'" || _contains "$response" "ARI"; }; then
+ if [ "$_replaces_certID" ] && { _contains "$response" "alreadyReplaced" || _contains "$response" "urn:ietf:params:acme:error:malformed" || _contains "$response" "'replaces'" || _contains "$response" "ARI"; }; then
_info "ARI 'replaces' rejected by CA, retrying newOrder without 'replaces'."
if ! _send_signed_request "$ACME_NEW_ORDER" "$_newOrderObj}"; then
_err "Error creating new order."
From 206f4494ac5977c359e33382c23575deab5c8cef Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Jakob=20N=C3=A6ss?=
<102598494+InvisibleDuck@users.noreply.github.com>
Date: Sun, 24 May 2026 22:34:56 +0200
Subject: [PATCH 04/18] Add Poweradmin DNS API plugin (dns_poweradmin) (#6943)
* Add Poweradmin DNS API plugin (dns_poweradmin)
---
dnsapi/dns_poweradmin.sh | 238 +++++++++++++++++++++++++++++++++++++++
1 file changed, 238 insertions(+)
create mode 100644 dnsapi/dns_poweradmin.sh
diff --git a/dnsapi/dns_poweradmin.sh b/dnsapi/dns_poweradmin.sh
new file mode 100644
index 00000000..db31fa4f
--- /dev/null
+++ b/dnsapi/dns_poweradmin.sh
@@ -0,0 +1,238 @@
+#!/usr/bin/env sh
+
+# shellcheck disable=SC2034
+
+# Credits to the authors of dnsapi/dns_pdns.sh as this reuses much of that code.
+
+dns_poweradmin_info='Poweradmin API
+Site: https://www.poweradmin.org/
+Docs: https://github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_poweradmin
+Options:
+POWERADMIN_URL API URL (with scheme). E.g. "https://poweradmin.example.com" or "http://192.168.0.10:8080"
+POWERADMIN_API_KEY API Token "pwa_xxxx"
+POWERADMIN_API_VERSION Optionally override Poweradmin API version.
+Issues: https://github.com/acmesh-official/acme.sh/issues/6912
+Author: Jakob Næss
+'
+
+######## Public functions ####################
+
+# Usage: dns_poweradmin_add _acme-challenge.www.domain.com "123456789ABCDEF"
+# fulldomain
+# txtvalue
+dns_poweradmin_add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ POWERADMIN_URL="${POWERADMIN_URL:-$(_readaccountconf_mutable POWERADMIN_URL)}"
+ POWERADMIN_API_KEY="${POWERADMIN_API_KEY:-$(_readaccountconf_mutable POWERADMIN_API_KEY)}"
+ POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-$(_readaccountconf_mutable POWERADMIN_API_VERSION)}"
+ POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-2}"
+
+ if [ -z "$POWERADMIN_URL" ]; then
+ POWERADMIN_URL=""
+ _err "You didn't specify Poweradmin URL."
+ _err "Please set POWERADMIN_URL and try again."
+ return 1
+ fi
+
+ if [ -z "$POWERADMIN_API_KEY" ]; then
+ POWERADMIN_API_KEY=""
+ _err "You didn't specify Poweradmin token."
+ _err "Please set POWERADMIN_API_KEY and try again."
+ return 1
+ fi
+
+ # Save the api addr, key, and version to the account conf file.
+ _saveaccountconf_mutable POWERADMIN_URL "$POWERADMIN_URL"
+ _saveaccountconf_mutable POWERADMIN_API_KEY "$POWERADMIN_API_KEY"
+ _saveaccountconf_mutable POWERADMIN_API_VERSION "$POWERADMIN_API_VERSION"
+
+ _debug "Detect root zone"
+ if ! _get_root "$fulldomain"; then
+ _err "invalid domain"
+ return 1
+ fi
+
+ _debug _domain "$_domain"
+ _debug _zone_id "$_zone_id"
+
+ if ! _set_record "$fulldomain" "$txtvalue"; then
+ return 1
+ fi
+
+ return 0
+}
+
+# Usage: dns_poweradmin_rm _acme-challenge.www.domain.com "123456789ABCDEF"
+# fulldomain
+# txtvalue
+dns_poweradmin_rm() {
+ fulldomain=$1
+ txtvalue=$2
+
+ POWERADMIN_URL="${POWERADMIN_URL:-$(_readaccountconf_mutable POWERADMIN_URL)}"
+ POWERADMIN_API_KEY="${POWERADMIN_API_KEY:-$(_readaccountconf_mutable POWERADMIN_API_KEY)}"
+ POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-$(_readaccountconf_mutable POWERADMIN_API_VERSION)}"
+ POWERADMIN_API_VERSION="${POWERADMIN_API_VERSION:-2}"
+
+ _debug "Detect root zone"
+ if ! _get_root "$fulldomain"; then
+ _err "invalid domain"
+ return 1
+ fi
+
+ _debug _domain "$_domain"
+ _debug _zone_id "$_zone_id"
+
+ if ! _rm_record "$fulldomain" "$txtvalue"; then
+ return 1
+ fi
+
+ return 0
+}
+
+######## Private functions below #####################
+
+_set_record() {
+ _info "Adding TXT record"
+ full=$1
+ new_challenge=$2
+
+ data='{"name":"'$full'","type":"TXT","content":"'$new_challenge'","ttl":60}'
+
+ if ! _poweradmin_rest "POST" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records" "$data" "application/json"; then
+ _err "Failed to add TXT record"
+ return 1
+ fi
+
+ return 0
+}
+
+_rm_record() {
+ _info "Remove TXT record"
+ full=$1
+ txtvalue=$2
+
+ if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records"; then
+ _err "Failed to retrieve records"
+ return 1
+ fi
+
+ # The API returns: {"success":true,"data":[{"id":..., "name":"...", "type":"TXT", "content":"...", ...}]}
+ _txt_record_obj=$(
+ printf '%s\n' "$response" |
+ sed 's/^.*"data":\[//; s/\],"message":.*$//' |
+ awk '{ gsub(/},{/, "}\n{"); print }' |
+ grep -F "\"name\":\"$full\"" |
+ grep -F "\"type\":\"TXT\"" |
+ grep -F "\"content\":\"$txtvalue\"" |
+ _head_n 1
+ )
+
+ if [ -z "$_txt_record_obj" ]; then
+ _info "TXT record not found for $full with content $txtvalue"
+ return 0
+ fi
+
+ record_id=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"id":\([0-9][0-9]*\).*/\1/p' | _head_n 1)
+ record_type=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"type":"\([^"]*\)".*/\1/p' | _head_n 1)
+ record_name=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"name":"\([^"]*\)".*/\1/p' | _head_n 1)
+ record_content=$(printf '%s\n' "$_txt_record_obj" | sed -n 's/.*"content":"\([^"]*\)".*/\1/p' | _head_n 1)
+
+ _debug2 "_txt_record_obj=$_txt_record_obj"
+ _debug2 "record id: $record_id"
+ _debug2 "record type: $record_type"
+ _debug2 "record name: $record_name"
+ _debug2 "record content: $record_content"
+
+ if [ "$record_type" != "TXT" ]; then
+ _err "Refusing to delete non-TXT record id=$record_id type=$record_type name=$full"
+ return 1
+ fi
+
+ if ! _poweradmin_rest "DELETE" "/api/v${POWERADMIN_API_VERSION}/zones/$_zone_id/records/$record_id"; then
+ _err "Failed to delete TXT record"
+ return 1
+ fi
+
+ _info "Record deleted successfully"
+ return 0
+}
+
+# _acme-challenge.www.domain.com
+# returns
+# _domain=domain.com
+# _zone_id=220
+_get_root() {
+ domain=$1
+ i=1
+
+ if ! _poweradmin_rest "GET" "/api/v${POWERADMIN_API_VERSION}/zones"; then
+ _err "Failed to retrieve zones"
+ return 1
+ fi
+
+ _zones_response="$response"
+
+ while true; do
+ h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
+
+ if [ -z "$h" ]; then
+ _debug "Root domain not found for $domain"
+ return 1
+ fi
+
+ zone_obj=$(
+ printf '%s' "$_zones_response" |
+ sed 's/},{/}\n{/g' |
+ grep -F "\"name\":\"$h\"" |
+ _head_n 1
+ )
+
+ if [ -n "$zone_obj" ]; then
+ _zone_id=$(printf '%s' "$zone_obj" | _egrep_o '"id":[0-9][0-9]*' | _head_n 1 | cut -d: -f2)
+ _domain="$h"
+ _debug "Found zone: $_domain with id: $_zone_id"
+ return 0
+ fi
+
+ i=$(_math "$i" + 1)
+ done
+}
+
+_poweradmin_rest() {
+ method=$1
+ ep=$2
+ data=$3
+ ct=$4
+
+ export _H1="X-API-Key: $POWERADMIN_API_KEY"
+
+ if [ "$method" = "GET" ]; then
+ response="$(_get "$POWERADMIN_URL$ep")"
+ else
+ _debug "API call: $method $ep"
+ _debug "Content-Type: $ct"
+ _debug "Payload: $data"
+ response="$(_post "$data" "$POWERADMIN_URL$ep" "" "$method" "$ct")"
+ fi
+
+ # Clear _H1 variable
+ unset -v _H1
+
+ if [ "$?" != "0" ]; then
+ _err "API error on $method $ep"
+ _debug "Response: $response"
+ return 1
+ fi
+
+ if printf '%s' "$response" | grep -q '"success"[[:space:]]*:[[:space:]]*false'; then
+ _err "API reported failure on $method $ep"
+ _debug "Response: $response"
+ return 1
+ fi
+
+ _debug2 "API Response: $response"
+ return 0
+}
From b7e9214e2d65b1099f6cd0dfc321ffa7b06159b0 Mon Sep 17 00:00:00 2001
From: neil
Date: Fri, 8 May 2026 20:44:48 +0200
Subject: [PATCH 05/18] minor
---
acme.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/acme.sh b/acme.sh
index f67749c0..6ab2d8d8 100755
--- a/acme.sh
+++ b/acme.sh
@@ -5785,7 +5785,7 @@ renew() {
_debug "_renewServer" "$_renewServer"
_initpath "$Le_Domain" "$_isEcc"
-
+ _info "Renew: $Le_Domain"
_set_level=${NOTIFY_LEVEL:-$NOTIFY_LEVEL_DEFAULT}
_info "$(__green "Renewing: '$Le_Domain'")"
if [ ! -f "$DOMAIN_CONF" ]; then
From 5713c1d39d3dd89f6b6698aaf26181ea9bb8c382 Mon Sep 17 00:00:00 2001
From: neil
Date: Sat, 30 May 2026 11:48:06 +0200
Subject: [PATCH 06/18] remove dns_hetzner.sh
https://github.com/acmesh-official/acme.sh/issues/6990#issuecomment-4576551997
---
dnsapi/dns_hetzner.sh | 256 ------------------------------------------
1 file changed, 256 deletions(-)
delete mode 100755 dnsapi/dns_hetzner.sh
diff --git a/dnsapi/dns_hetzner.sh b/dnsapi/dns_hetzner.sh
deleted file mode 100755
index f1bddc61..00000000
--- a/dnsapi/dns_hetzner.sh
+++ /dev/null
@@ -1,256 +0,0 @@
-#!/usr/bin/env sh
-# shellcheck disable=SC2034
-dns_hetzner_info='Hetzner.com
-Site: Hetzner.com
-Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_hetzner
-Options:
- HETZNER_Token API Token
-Issues: github.com/acmesh-official/acme.sh/issues/2943
-'
-
-HETZNER_Api="https://dns.hetzner.com/api/v1"
-
-######## Public functions #####################
-
-# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
-# Used to add txt record
-# Ref: https://dns.hetzner.com/api-docs/
-dns_hetzner_add() {
- full_domain=$1
- txt_value=$2
-
- HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}"
-
- if [ -z "$HETZNER_Token" ]; then
- HETZNER_Token=""
- _err "You didn't specify a Hetzner api token."
- _err "You can get yours from here https://dns.hetzner.com/settings/api-token."
- return 1
- fi
-
- #save the api key and email to the account conf file.
- _saveaccountconf_mutable HETZNER_Token "$HETZNER_Token"
-
- _debug "First detect the root zone"
-
- if ! _get_root "$full_domain"; then
- _err "Invalid domain"
- return 1
- fi
- _debug _domain_id "$_domain_id"
- _debug _sub_domain "$_sub_domain"
- _debug _domain "$_domain"
-
- _debug "Getting TXT records"
- if ! _find_record "$_sub_domain" "$txt_value"; then
- return 1
- fi
-
- if [ -z "$_record_id" ]; then
- _info "Adding record"
- if _hetzner_rest POST "records" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then
- if _contains "$response" "$txt_value"; then
- _info "Record added, OK"
- _sleep 2
- return 0
- fi
- fi
- _err "Add txt record error${_response_error}"
- return 1
- else
- _info "Found record id: $_record_id."
- _info "Record found, do nothing."
- return 0
- # we could modify a record, if the names for txt records for *.example.com and example.com would be not the same
- #if _hetzner_rest PUT "records/${_record_id}" "{\"zone_id\":\"${HETZNER_Zone_ID}\",\"type\":\"TXT\",\"name\":\"$full_domain\",\"value\":\"$txt_value\",\"ttl\":120}"; then
- # if _contains "$response" "$txt_value"; then
- # _info "Modified, OK"
- # return 0
- # fi
- #fi
- #_err "Add txt record error (modify)."
- #return 1
- fi
-}
-
-# Usage: full_domain txt_value
-# Used to remove the txt record after validation
-dns_hetzner_rm() {
- full_domain=$1
- txt_value=$2
-
- HETZNER_Token="${HETZNER_Token:-$(_readaccountconf_mutable HETZNER_Token)}"
-
- _debug "First detect the root zone"
- if ! _get_root "$full_domain"; then
- _err "Invalid domain"
- return 1
- fi
- _debug _domain_id "$_domain_id"
- _debug _sub_domain "$_sub_domain"
- _debug _domain "$_domain"
-
- _debug "Getting TXT records"
- if ! _find_record "$_sub_domain" "$txt_value"; then
- return 1
- fi
-
- if [ -z "$_record_id" ]; then
- _info "Remove not needed. Record not found."
- else
- if ! _hetzner_rest DELETE "records/$_record_id"; then
- _err "Delete record error${_response_error}"
- return 1
- fi
- _sleep 2
- _info "Record deleted"
- fi
-}
-
-#################### Private functions below ##################################
-#returns
-# _record_id=a8d58f22d6931bf830eaa0ec6464bf81 if found; or 1 if error
-_find_record() {
- unset _record_id
- _record_name=$1
- _record_value=$2
-
- if [ -z "$_record_value" ]; then
- _record_value='[^"]*'
- fi
-
- _debug "Getting all records"
- _hetzner_rest GET "records?zone_id=${_domain_id}"
-
- if _response_has_error; then
- _err "Error${_response_error}"
- return 1
- else
- _record_id=$(
- echo "$response" |
- grep -o "{[^\{\}]*\"name\":\"$_record_name\"[^\}]*}" |
- grep "\"value\":\"$_record_value\"" |
- while read -r record; do
- # test for type and
- if [ -n "$(echo "$record" | _egrep_o '"type":"TXT"')" ]; then
- echo "$record" | _egrep_o '"id":"[^"]*"' | cut -d : -f 2 | tr -d \"
- break
- fi
- done
- )
- fi
-}
-
-#_acme-challenge.www.domain.com
-#returns
-# _sub_domain=_acme-challenge.www
-# _domain=domain.com
-# _domain_id=sdjkglgdfewsdfg
-_get_root() {
- domain=$1
- i=1
- p=1
-
- domain_without_acme=$(echo "$domain" | cut -d . -f 2-)
- domain_param_name=$(echo "HETZNER_Zone_ID_for_${domain_without_acme}" | sed 's/[\.\-]/_/g')
-
- _debug "Reading zone_id for '$domain_without_acme' from config..."
- HETZNER_Zone_ID=$(_readdomainconf "$domain_param_name")
- if [ "$HETZNER_Zone_ID" ]; then
- _debug "Found, using: $HETZNER_Zone_ID"
- if ! _hetzner_rest GET "zones/${HETZNER_Zone_ID}"; then
- _debug "Zone with id '$HETZNER_Zone_ID' does not exist."
- _cleardomainconf "$domain_param_name"
- unset HETZNER_Zone_ID
- else
- if _contains "$response" "\"id\":\"$HETZNER_Zone_ID\""; then
- _domain=$(printf "%s\n" "$response" | _egrep_o '"name":"[^"]*"' | cut -d : -f 2 | tr -d \" | head -n 1)
- if [ "$_domain" ]; then
- _cut_length=$((${#domain} - ${#_domain} - 1))
- _sub_domain=$(printf "%s" "$domain" | cut -c "1-$_cut_length")
- _domain_id="$HETZNER_Zone_ID"
- return 0
- else
- return 1
- fi
- else
- return 1
- fi
- fi
- fi
-
- _debug "Trying to get zone id by domain name for '$domain_without_acme'."
- while true; do
- h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
- if [ -z "$h" ]; then
- #not valid
- return 1
- fi
- _debug h "$h"
-
- _hetzner_rest GET "zones?name=$h"
-
- if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_entries":1'; then
- _domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
- if [ "$_domain_id" ]; then
- _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
- _domain=$h
- HETZNER_Zone_ID=$_domain_id
- _savedomainconf "$domain_param_name" "$HETZNER_Zone_ID"
- return 0
- fi
- return 1
- fi
- p=$i
- i=$(_math "$i" + 1)
- done
- return 1
-}
-
-#returns
-# _response_error
-_response_has_error() {
- unset _response_error
-
- err_part="$(echo "$response" | _egrep_o '"error":\{[^\}]*\}')"
-
- if [ -n "$err_part" ]; then
- err_code=$(echo "$err_part" | _egrep_o '"code":[0-9]+' | cut -d : -f 2)
- err_message=$(echo "$err_part" | _egrep_o '"message":"[^"]+"' | cut -d : -f 2 | tr -d \")
-
- if [ -n "$err_code" ] && [ -n "$err_message" ]; then
- _response_error=" - message: ${err_message}, code: ${err_code}"
- return 0
- fi
- fi
-
- return 1
-}
-
-#returns
-# response
-_hetzner_rest() {
- m=$1
- ep="$2"
- data="$3"
- _debug "$ep"
-
- key_trimmed=$(echo "$HETZNER_Token" | tr -d \")
-
- export _H1="Content-TType: application/json"
- export _H2="Auth-API-Token: $key_trimmed"
-
- if [ "$m" != "GET" ]; then
- _debug data "$data"
- response="$(_post "$data" "$HETZNER_Api/$ep" "" "$m")"
- else
- response="$(_get "$HETZNER_Api/$ep")"
- fi
-
- if [ "$?" != "0" ] || _response_has_error; then
- _debug "Error$_response_error"
- return 1
- fi
- _debug2 response "$response"
- return 0
-}
From dfbe2c5bff139a89b7a782d365870aee8b9eaa50 Mon Sep 17 00:00:00 2001
From: Thomas
Date: Thu, 4 Jun 2026 20:15:12 +0100
Subject: [PATCH 07/18] fix _getAKI() on OpenBSD (#7007)
The order of the arguments does matter for OpenBSD's grep (bug or
feature).
---
acme.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/acme.sh b/acme.sh
index 6ab2d8d8..00192868 100755
--- a/acme.sh
+++ b/acme.sh
@@ -6865,7 +6865,7 @@ deactivate() {
#cert
_getAKI() {
_cert="$1"
- ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -text -noout | grep "X509v3 Authority Key Identifier" -A 1 | _tail_n 1 | tr -d ': ' | sed "s/keyid//"
+ ${ACME_OPENSSL_BIN:-openssl} x509 -in "$_cert" -text -noout | grep -A 1 "X509v3 Authority Key Identifier" | _tail_n 1 | tr -d ': ' | sed "s/keyid//"
}
#cert
From c7c903fba3188ac2c9063a9fa9b14a2f991bbe25 Mon Sep 17 00:00:00 2001
From: terafin
Date: Thu, 4 Jun 2026 12:25:38 -0700
Subject: [PATCH 08/18] ci: add GitHub Container Registry (ghcr.io) publishing
(#7005)
---
.github/workflows/dockerhub.yml | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml
index 0e7ba748..d10c17a8 100644
--- a/.github/workflows/dockerhub.yml
+++ b/.github/workflows/dockerhub.yml
@@ -41,6 +41,9 @@ jobs:
runs-on: ubuntu-latest
needs: CheckToken
if: "contains(needs.CheckToken.outputs.hasToken, 'true')"
+ permissions:
+ contents: read
+ packages: write
steps:
- name: checkout code
uses: actions/checkout@v6
@@ -58,6 +61,9 @@ jobs:
- name: login to docker hub
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
+ - name: login to ghcr
+ run: |
+ echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${{ github.actor }}" --password-stdin
- name: build and push the image
run: |
if [[ $GITHUB_REF == refs/tags/* ]]; then
@@ -73,6 +79,8 @@ jobs:
fi
fi
+ echo "DOCKER_IMAGE_TAG=${DOCKER_IMAGE_TAG}" >>"$GITHUB_ENV"
+
DOCKER_LABELS=()
while read -r label; do
DOCKER_LABELS+=(--label "${label}")
@@ -84,3 +92,9 @@ jobs:
--output "type=image,push=true" \
--build-arg AUTO_UPGRADE=${AUTO_UPGRADE} \
--platform linux/arm64/v8,linux/amd64,linux/arm/v6,linux/arm/v7,linux/386,linux/ppc64le,linux/s390x .
+ - name: mirror the image to ghcr (best-effort)
+ run: |
+ docker buildx imagetools create \
+ --tag ghcr.io/${{ github.repository }}:${DOCKER_IMAGE_TAG} \
+ ${DOCKER_IMAGE}:${DOCKER_IMAGE_TAG} \
+ || echo "::warning::GHCR mirror failed; Docker Hub publish unaffected"
From 2e4e5d7955530932673e446f4be41e32e2204c6b Mon Sep 17 00:00:00 2001
From: neil
Date: Thu, 4 Jun 2026 22:34:24 +0200
Subject: [PATCH 09/18] add tribblix
---
.github/workflows/DNS.yml | 58 ++++++++++++++++++++++++-
.github/workflows/Tribblix.yml | 79 ++++++++++++++++++++++++++++++++++
README.md | 2 +
3 files changed, 138 insertions(+), 1 deletion(-)
create mode 100644 .github/workflows/Tribblix.yml
diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml
index 00d180b9..232c9b0f 100644
--- a/.github/workflows/DNS.yml
+++ b/.github/workflows/DNS.yml
@@ -661,9 +661,65 @@ jobs:
- Haiku:
+ Tribblix:
runs-on: ubuntu-latest
needs: OpenIndiana
+ env:
+ TEST_DNS : ${{ secrets.TEST_DNS }}
+ TestingDomain: ${{ secrets.TestingDomain }}
+ TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
+ TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
+ TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
+ CASE: le_test_dnsapi
+ TEST_LOCAL: 1
+ DEBUG: ${{ secrets.DEBUG }}
+ http_proxy: ${{ secrets.http_proxy }}
+ https_proxy: ${{ secrets.https_proxy }}
+ HTTPS_INSECURE: 1 # always set to 1 to ignore https error, since Tribblix doesn't accept the expired ISRG X1 root
+ TokenName1: ${{ secrets.TokenName1}}
+ TokenName2: ${{ secrets.TokenName2}}
+ TokenName3: ${{ secrets.TokenName3}}
+ TokenName4: ${{ secrets.TokenName4}}
+ TokenName5: ${{ secrets.TokenName5}}
+ steps:
+ - 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/tribblix-vm@v1
+ with:
+ debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy HTTPS_INSECURE TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
+ sync: nfs
+ prepare: zap install socat
+ run: |
+ if [ "${{ secrets.TokenName1}}" ] ; then
+ export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
+ fi
+ if [ "${{ secrets.TokenName2}}" ] ; then
+ export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
+ fi
+ if [ "${{ secrets.TokenName3}}" ] ; then
+ export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
+ fi
+ if [ "${{ secrets.TokenName4}}" ] ; then
+ export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
+ fi
+ if [ "${{ secrets.TokenName5}}" ] ; then
+ export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
+ fi
+ cd ../acmetest
+ ./letest.sh
+ - name: DebugOnError
+ if: ${{ failure() }}
+ run: |
+ echo "See how to debug in VM:"
+ echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
+
+
+
+ Haiku:
+ runs-on: ubuntu-latest
+ needs: Tribblix
env:
TEST_DNS : ${{ secrets.TEST_DNS }}
TestingDomain: ${{ secrets.TestingDomain }}
diff --git a/.github/workflows/Tribblix.yml b/.github/workflows/Tribblix.yml
new file mode 100644
index 00000000..cd43e0e3
--- /dev/null
+++ b/.github/workflows/Tribblix.yml
@@ -0,0 +1,79 @@
+name: Tribblix
+on:
+ push:
+ branches:
+ - '*'
+ paths:
+ - '*.sh'
+ - '.github/workflows/Tribblix.yml'
+
+ pull_request:
+ branches:
+ - dev
+ paths:
+ - '*.sh'
+ - '.github/workflows/Tribblix.yml'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+
+
+jobs:
+ Tribblix:
+ strategy:
+ matrix:
+ include:
+ - TEST_ACME_Server: "LetsEncrypt.org_test"
+ CA_ECDSA: ""
+ CA: ""
+ CA_EMAIL: ""
+ TEST_PREFERRED_CHAIN: (STAGING)
+ - TEST_ACME_Server: "LetsEncrypt.org_test"
+ CA_ECDSA: ""
+ CA: ""
+ CA_EMAIL: ""
+ TEST_PREFERRED_CHAIN: (STAGING)
+ ACME_USE_WGET: 1
+ #- TEST_ACME_Server: "ZeroSSL.com"
+ # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
+ # CA: "ZeroSSL RSA DV SSL CA 2"
+ # CA_EMAIL: "githubtest@acme.sh"
+ # TEST_PREFERRED_CHAIN: ""
+ runs-on: ubuntu-latest
+ env:
+ TEST_LOCAL: 1
+ TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
+ CA_ECDSA: ${{ matrix.CA_ECDSA }}
+ CA: ${{ matrix.CA }}
+ CA_EMAIL: ${{ matrix.CA_EMAIL }}
+ TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
+ ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
+ steps:
+ - uses: actions/checkout@v6
+ - uses: anyvm-org/cf-tunnel@v0
+ id: tunnel
+ with:
+ protocol: http
+ port: 8080
+ - name: Set envs
+ run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
+ - name: Clone acmetest
+ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
+ - uses: vmactions/tribblix-vm@v1
+ with:
+ debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
+ nat: |
+ "8080": "80"
+ prepare: zap install socat curl wget
+ sync: nfs
+ run: |
+ cd ../acmetest \
+ && ./letest.sh
+ - name: DebugOnError
+ if: ${{ failure() }}
+ run: |
+ echo "See how to debug in VM:"
+ echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
diff --git a/README.md b/README.md
index ba5d3591..22700b4c 100644
--- a/README.md
+++ b/README.md
@@ -19,6 +19,7 @@
+
@@ -112,6 +113,7 @@
|23|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/acmesh-official/acme.sh/wiki/How-to-run-on-OpenWRT)
|24|[](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management)
|25|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS
+|26|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
From 0e5f1518aab3937eb690aa421ed48078c79b2ecd Mon Sep 17 00:00:00 2001
From: neil
Date: Thu, 4 Jun 2026 22:57:50 +0200
Subject: [PATCH 10/18] upgrade
---
.github/workflows/dockerhub.yml | 4 ++--
.github/workflows/issue.yml | 2 +-
.github/workflows/pr_dns.yml | 2 +-
.github/workflows/pr_notify.yml | 2 +-
.github/workflows/wiki-monitor.yml | 2 +-
5 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/dockerhub.yml b/.github/workflows/dockerhub.yml
index d10c17a8..7dc42290 100644
--- a/.github/workflows/dockerhub.yml
+++ b/.github/workflows/dockerhub.yml
@@ -50,14 +50,14 @@ jobs:
with:
persist-credentials: false
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ uses: docker/setup-qemu-action@v4
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v6
with:
images: ${DOCKER_IMAGE}
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@v4
- name: login to docker hub
run: |
echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin
diff --git a/.github/workflows/issue.yml b/.github/workflows/issue.yml
index e92b0411..c659fce5 100644
--- a/.github/workflows/issue.yml
+++ b/.github/workflows/issue.yml
@@ -7,7 +7,7 @@ jobs:
comment:
runs-on: ubuntu-latest
steps:
- - uses: actions/github-script@v6
+ - uses: actions/github-script@v9
with:
script: |
github.rest.issues.createComment({
diff --git a/.github/workflows/pr_dns.yml b/.github/workflows/pr_dns.yml
index 558ebf48..19763a15 100644
--- a/.github/workflows/pr_dns.yml
+++ b/.github/workflows/pr_dns.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- - uses: actions/github-script@v6
+ - uses: actions/github-script@v9
with:
script: |
await github.rest.issues.createComment({
diff --git a/.github/workflows/pr_notify.yml b/.github/workflows/pr_notify.yml
index 416ed721..76ae76f6 100644
--- a/.github/workflows/pr_notify.yml
+++ b/.github/workflows/pr_notify.yml
@@ -15,7 +15,7 @@ jobs:
runs-on: ubuntu-latest
if: github.actor != 'neilpang'
steps:
- - uses: actions/github-script@v6
+ - uses: actions/github-script@v9
with:
script: |
await github.rest.issues.createComment({
diff --git a/.github/workflows/wiki-monitor.yml b/.github/workflows/wiki-monitor.yml
index a706529a..7e5d7ca3 100644
--- a/.github/workflows/wiki-monitor.yml
+++ b/.github/workflows/wiki-monitor.yml
@@ -51,7 +51,7 @@ jobs:
} > wiki-change-msg.txt
- name: Create issue to notify Neilpang
- uses: peter-evans/create-issue-from-file@v5
+ uses: peter-evans/create-issue-from-file@v6
with:
title: "Wiki edited"
content-filepath: ./wiki-change-msg.txt
From b4634719514509d2485aff87c23800fee116ac59 Mon Sep 17 00:00:00 2001
From: neil
Date: Fri, 5 Jun 2026 19:22:25 +0200
Subject: [PATCH 11/18] fix localaddress
https://github.com/acmesh-official/acme.sh/issues/7009#issuecomment-4633681701
---
acme.sh | 2 ++
1 file changed, 2 insertions(+)
diff --git a/acme.sh b/acme.sh
index 00192868..e6272c9f 100755
--- a/acme.sh
+++ b/acme.sh
@@ -5312,6 +5312,8 @@ $_authorizations_map"
fi
fi
elif [ "$vtype" = "$VTYPE_ALPN" ]; then
+ _ncaddr="$(_getfield "$_local_addr" "$_ncIndex")"
+ _ncIndex="$(_math $_ncIndex + 1)"
acmevalidationv1="$(printf "%s" "$keyauthorization" | _digest "sha256" "hex")"
_debug acmevalidationv1 "$acmevalidationv1"
if ! _starttlsserver "$d" "" "$Le_TLSPort" "$keyauthorization" "$_ncaddr" "$acmevalidationv1"; then
From a2f046306e5089b2ab82b247a319218b3aa30278 Mon Sep 17 00:00:00 2001
From: Adrian Fedoreanu
Date: Fri, 5 Jun 2026 19:28:08 +0200
Subject: [PATCH 12/18] dns_1984hosting: cleanup, memoize zone id (#6978)
* dns_1984hosting: cleanup, memoize zone id, optional OTP
---
dnsapi/dns_1984hosting.sh | 51 +++++++++++++++++++++++++++++++--------
1 file changed, 41 insertions(+), 10 deletions(-)
diff --git a/dnsapi/dns_1984hosting.sh b/dnsapi/dns_1984hosting.sh
index 8d9676ac..8ed9b8ef 100755
--- a/dnsapi/dns_1984hosting.sh
+++ b/dnsapi/dns_1984hosting.sh
@@ -7,6 +7,7 @@ Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi2#dns_1984hosting
Options:
One984HOSTING_Username Username
One984HOSTING_Password Password
+ One984HOSTING_TOTP_Secret Base32 TOTP shared secret. Required only if the account has 2FA enabled. Requires oathtool. Used to mint the OTP code automatically at login so cron renewals keep working.
Issues: github.com/acmesh-official/acme.sh/issues/2851
Author: Adrian Fedoreanu
'
@@ -124,11 +125,28 @@ _1984hosting_login() {
_debug "Login to 1984Hosting as user $One984HOSTING_Username."
username=$(printf '%s' "$One984HOSTING_Username" | _url_encode)
password=$(printf '%s' "$One984HOSTING_Password" | _url_encode)
- url="https://1984.hosting/api/auth/"
- _get "https://1984.hosting/accounts/login/" | grep "csrfmiddlewaretoken"
- csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
- sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
+ # When 2FA is enabled, mint a fresh TOTP code from the stored shared secret.
+ # Empty otpkey is accepted by the server when 2FA is off.
+ otpkey=""
+ if [ -n "$One984HOSTING_TOTP_Secret" ]; then
+ if ! _exists oathtool; then
+ _err "oathtool is required to use One984HOSTING_TOTP_Secret for 2FA. Please install it."
+ return 1
+ fi
+ otpcode="$(oathtool --base32 --totp "$One984HOSTING_TOTP_Secret" 2>/dev/null)"
+ if [ -z "$otpcode" ]; then
+ _err "Failed to generate TOTP code from One984HOSTING_TOTP_Secret."
+ return 1
+ fi
+ otpkey="$(printf '%s' "$otpcode" | _url_encode)"
+ fi
+
+ # Fetch the login page to obtain CSRF and session cookies.
+ # Note: _get sets the global 'url', so assign the auth URL afterwards.
+ _get "https://1984.hosting/accounts/login/" >/dev/null
+ csrftoken="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | _head_n 1 | tr -d ';')"
+ sessionid="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | _head_n 1 | tr -d ';')"
if [ -z "$csrftoken" ] || [ -z "$sessionid" ]; then
_err "One or more cookies are empty: '$csrftoken', '$sessionid'."
@@ -140,17 +158,23 @@ _1984hosting_login() {
csrf_header=$(echo "$csrftoken" | sed 's/csrftoken=//' | _head_n 1)
export _H3="X-CSRFToken: $csrf_header"
- response="$(_post "username=$username&password=$password&otpkey=" $url)"
+ url="https://1984.hosting/api/auth/"
+ response="$(_post "username=$username&password=$password&otpkey=$otpkey" "$url")"
response="$(echo "$response" | _normalizeJson)"
_debug2 response "$response"
if _contains "$response" '"loggedin": true'; then
- One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | tr -d ';')"
- One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | tr -d ';')"
+ One984HOSTING_SESSIONID_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'cookie1984nammnamm=[^;]*;' | _head_n 1 | tr -d ';')"
+ One984HOSTING_CSRFTOKEN_COOKIE="$(grep -i '^set-cookie:' "$HTTP_HEADER" | _egrep_o 'csrftoken=[^;]*;' | _head_n 1 | tr -d ';')"
export One984HOSTING_SESSIONID_COOKIE
export One984HOSTING_CSRFTOKEN_COOKIE
_saveaccountconf_mutable One984HOSTING_Username "$One984HOSTING_Username"
_saveaccountconf_mutable One984HOSTING_Password "$One984HOSTING_Password"
+ if [ -n "$One984HOSTING_TOTP_Secret" ]; then
+ _saveaccountconf_mutable One984HOSTING_TOTP_Secret "$One984HOSTING_TOTP_Secret"
+ else
+ _clearaccountconf_mutable One984HOSTING_TOTP_Secret
+ fi
_saveaccountconf_mutable One984HOSTING_SESSIONID_COOKIE "$One984HOSTING_SESSIONID_COOKIE"
_saveaccountconf_mutable One984HOSTING_CSRFTOKEN_COOKIE "$One984HOSTING_CSRFTOKEN_COOKIE"
return 0
@@ -161,6 +185,7 @@ _1984hosting_login() {
_check_credentials() {
One984HOSTING_Username="${One984HOSTING_Username:-$(_readaccountconf_mutable One984HOSTING_Username)}"
One984HOSTING_Password="${One984HOSTING_Password:-$(_readaccountconf_mutable One984HOSTING_Password)}"
+ One984HOSTING_TOTP_Secret="${One984HOSTING_TOTP_Secret:-$(_readaccountconf_mutable One984HOSTING_TOTP_Secret)}"
if [ -z "$One984HOSTING_Username" ] || [ -z "$One984HOSTING_Password" ]; then
One984HOSTING_Username=""
One984HOSTING_Password=""
@@ -225,9 +250,15 @@ _get_root() {
# Usage: _get_zone_id url domain.com
# Returns zone id for domain.com
+# Memoized per-domain so add/rm don't re-fetch the same zone list within a run.
+# Keyed on domain (not url) since the url is always the domains listing.
_get_zone_id() {
url=$1
domain=$2
+ if [ "$_zone_id_for" = "$domain" ] && [ -n "$_zone_id" ]; then
+ _debug2 _zone_id "$_zone_id (cached)"
+ return 0
+ fi
_htmlget "$url" "$domain"
_zone_id="$(echo "$_response" | _egrep_o 'zone\/[0-9]+' | _head_n 1)"
_debug2 _zone_id "$_zone_id"
@@ -235,6 +266,7 @@ _get_zone_id() {
_err "Error getting _zone_id for $2."
return 1
fi
+ _zone_id_for="$domain"
return 0
}
@@ -257,9 +289,8 @@ _htmlget() {
# Add extra headers to request
_authpost() {
- url="https://1984.hosting/domains"
- _get_zone_id "$url" "$_domain"
- csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | _egrep_o "=[^=][0-9a-zA-Z]*" | tr -d "=")"
+ _get_zone_id "https://1984.hosting/domains" "$_domain"
+ csrf_header="$(echo "$One984HOSTING_CSRFTOKEN_COOKIE" | sed 's/csrftoken=//' | _head_n 1)"
export _H1="Cookie: $One984HOSTING_CSRFTOKEN_COOKIE; $One984HOSTING_SESSIONID_COOKIE"
export _H2="Referer: https://1984.hosting/domains/$_zone_id"
export _H3="X-CSRFToken: $csrf_header"
From 58d9c8d7f613c1975f694df417d78dda805d5629 Mon Sep 17 00:00:00 2001
From: rajcz
Date: Fri, 5 Jun 2026 19:38:50 +0200
Subject: [PATCH 13/18] acme.sh: validate cert response before writing .cer
(#7006)
---
acme.sh | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/acme.sh b/acme.sh
index e6272c9f..a7397be1 100755
--- a/acme.sh
+++ b/acme.sh
@@ -5544,6 +5544,13 @@ $_authorizations_map"
return 1
fi
+ if ! _contains "$response" "$BEGIN_CERT"; then
+ response="$(echo "$response" | _dbase64 "multiline" | tr -d '\0' | _normalizeJson)"
+ _err "Signing failed: $(echo "$response" | _egrep_o '"detail":"[^"]*"')"
+ _on_issue_err "$_post_hook"
+ return 1
+ fi
+
echo "$response" >"$CERT_PATH"
_split_cert_chain "$CERT_PATH" "$CERT_FULLCHAIN_PATH" "$CA_CERT_PATH"
if [ -z "$_preferred_chain" ]; then
@@ -5563,6 +5570,11 @@ $_authorizations_map"
_err "$response"
continue
fi
+
+ if ! _contains "$response" "$BEGIN_CERT"; then
+ _debug2 "Skipping alternate cert link due to unexpected response format."
+ continue
+ fi
_relcert="$CERT_PATH.alt"
_relfullchain="$CERT_FULLCHAIN_PATH.alt"
_relca="$CA_CERT_PATH.alt"
From 9b597b3f1bb424b2b782c54b74955bd9efa37c8e Mon Sep 17 00:00:00 2001
From: aitor422
Date: Fri, 5 Jun 2026 20:02:14 +0000
Subject: [PATCH 14/18] Add CDMON Api (#6984)
* Added CDMon DNS API
---
dnsapi/dns_cdmon.sh | 137 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 137 insertions(+)
create mode 100644 dnsapi/dns_cdmon.sh
diff --git a/dnsapi/dns_cdmon.sh b/dnsapi/dns_cdmon.sh
new file mode 100644
index 00000000..470fb5fe
--- /dev/null
+++ b/dnsapi/dns_cdmon.sh
@@ -0,0 +1,137 @@
+#!/usr/bin/env sh
+# shellcheck disable=SC2034
+
+dns_cdmon_info='cdmon
+Site: www.cdmon.com
+Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_cdmon
+Options:
+ CDMON_Key API Key
+'
+
+CDMON_Api="https://api-domains.cdmon.services/api-domains"
+
+######## Public functions #####################
+# Usage: add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+# Used to add txt record
+dns_cdmon_add() {
+ fulldomain=$1
+ txtvalue=$2
+
+ CDMON_Key="${CDMON_Key:-$(_readaccountconf_mutable CDMON_Key)}"
+
+ if [ -z "$CDMON_Key" ]; then
+ CDMON_Key=""
+ _err "You didn't specify your cdmon api key yet."
+ _err "Please create your key and try again."
+ return 1
+ fi
+
+ _saveaccountconf_mutable CDMON_Key "$CDMON_Key"
+
+ _debug "First, we detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ _err "invalid domain"
+ return 1
+ fi
+
+ _debug _sub_domain "$_sub_domain"
+ _debug _domain "$_domain"
+ _info "Adding record"
+ if _cdmon_rest "dnsrecords/create" "{\"data\":{\"type\":\"TXT\",\"domain\":\"$_domain\",\"value\":\"$txtvalue\",\"ttl\":120,\"host\":\"$_sub_domain\"}}"; then
+ if _contains "$response" "\"status\":\"ok\""; then
+ _info "Added, OK"
+ return 0
+ else
+ _err "Add txt record error."
+ return 1
+ fi
+ fi
+ _err "Add txt record error."
+ return 1
+}
+
+# Usage: fulldomain txtvalue
+# Used to remove the txt record after validation
+dns_cdmon_rm() {
+ fulldomain=$1
+ txtvalue=$2
+
+ CDMON_Key="${CDMON_Key:-$(_readaccountconf_mutable CDMON_Key)}"
+ _debug "First, we detect the root zone"
+ if ! _get_root "$fulldomain"; then
+ _err "invalid domain"
+ return 1
+ fi
+
+ _debug _sub_domain "$_sub_domain"
+ _debug _domain "$_domain"
+
+ _info "Removing record"
+ if _cdmon_rest "dnsrecords/delete" "{\"data\":{\"value\":\"$txtvalue\",\"type\":\"TXT\",\"domain\":\"$_domain\",\"host\":\"$_sub_domain\"}}"; then
+ if _contains "$response" "\"status\":\"ok\""; then
+ _info "Deleted, OK"
+ return 0
+ else
+ _err "Delete txt record error."
+ return 1
+ fi
+ fi
+ _err "Delete txt record error."
+ return 1
+}
+
+#################### Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+_get_root() {
+ domain=$1
+ i=1
+ p=1
+
+ if ! _cdmon_rest "domains/list"; then
+ return 1
+ fi
+
+ while true; do
+ h=$(printf "%s" "$domain" | cut -d . -f "$i"-100)
+ _debug h "$h"
+ if [ -z "$h" ]; then
+ #not valid
+ return 1
+ fi
+ if _contains "$response" "\"domain\":\"$h\""; then
+ _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-"$p")
+ _domain=$h
+ return 0
+ fi
+ p=$i
+ i=$(_math "$i" + 1)
+ done
+ return 1
+}
+
+_cdmon_rest() {
+ ep="$1"
+ data="$2"
+ _debug "$ep"
+
+ key_trimmed=$(echo "$CDMON_Key" | tr -d '"')
+
+ export _H1="Content-Type: application/json"
+ export _H2="apikey: $key_trimmed"
+
+ _debug data "$data"
+ response="$(_post "$data" "$CDMON_Api/$ep")"
+ _ret="$?"
+
+ unset _H1 _H2
+
+ if [ "$_ret" != "0" ]; then
+ _err "error $ep"
+ return 1
+ fi
+ _debug2 response "$response"
+ return 0
+}
From d98fa53f627ab08e17616a8037cea86e496aa917 Mon Sep 17 00:00:00 2001
From: Bill <80264737+billzee@users.noreply.github.com>
Date: Fri, 5 Jun 2026 16:06:49 -0400
Subject: [PATCH 15/18] Updated AWS Route53 service endpoint to the dual-stack
endpoint (#6994)
* Update to dual-stack service endpoint
---
dnsapi/dns_aws.sh | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/dnsapi/dns_aws.sh b/dnsapi/dns_aws.sh
index b76d69c2..1face1c8 100755
--- a/dnsapi/dns_aws.sh
+++ b/dnsapi/dns_aws.sh
@@ -11,7 +11,8 @@ Options:
# All `_sleep` commands are included to avoid Route53 throttling, see
# https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html#limits-api-requests
-AWS_HOST="route53.amazonaws.com"
+# Updated from "route53.amazonaws.com"
+AWS_HOST="route53.global.api.aws"
AWS_URL="https://$AWS_HOST"
AWS_WIKI="https://github.com/acmesh-official/acme.sh/wiki/How-to-use-Amazon-Route53-API"
From 4575877d48643494d6e5769c0468386ea6a80a77 Mon Sep 17 00:00:00 2001
From: neil
Date: Fri, 5 Jun 2026 23:01:31 +0200
Subject: [PATCH 16/18] add GhostBSD
---
.github/workflows/DNS.yml | 58 +++++++++++++++++++++++-
.github/workflows/GhostBSD.yml | 80 ++++++++++++++++++++++++++++++++++
README.md | 2 +
3 files changed, 139 insertions(+), 1 deletion(-)
create mode 100644 .github/workflows/GhostBSD.yml
diff --git a/.github/workflows/DNS.yml b/.github/workflows/DNS.yml
index 232c9b0f..06dd29ac 100644
--- a/.github/workflows/DNS.yml
+++ b/.github/workflows/DNS.yml
@@ -260,7 +260,7 @@ jobs:
- OpenBSD:
+ GhostBSD:
runs-on: ubuntu-latest
needs: FreeBSD
env:
@@ -281,6 +281,62 @@ jobs:
TokenName5: ${{ secrets.TokenName5}}
steps:
- 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/ghostbsd-vm@v1
+ with:
+ debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_NO_SUBDOMAIN TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG http_proxy https_proxy TokenName1 TokenName2 TokenName3 TokenName4 TokenName5 ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'
+ prepare: pkg install -y socat curl
+ usesh: true
+ sync: nfs
+ run: |
+ if [ "${{ secrets.TokenName1}}" ] ; then
+ export ${{ secrets.TokenName1}}="${{ secrets.TokenValue1}}"
+ fi
+ if [ "${{ secrets.TokenName2}}" ] ; then
+ export ${{ secrets.TokenName2}}="${{ secrets.TokenValue2}}"
+ fi
+ if [ "${{ secrets.TokenName3}}" ] ; then
+ export ${{ secrets.TokenName3}}="${{ secrets.TokenValue3}}"
+ fi
+ if [ "${{ secrets.TokenName4}}" ] ; then
+ export ${{ secrets.TokenName4}}="${{ secrets.TokenValue4}}"
+ fi
+ if [ "${{ secrets.TokenName5}}" ] ; then
+ export ${{ secrets.TokenName5}}="${{ secrets.TokenValue5}}"
+ fi
+ cd ../acmetest
+ ./letest.sh
+ - name: DebugOnError
+ if: ${{ failure() }}
+ run: |
+ echo "See how to debug in VM:"
+ echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
+
+
+
+ OpenBSD:
+ runs-on: ubuntu-latest
+ needs: GhostBSD
+ env:
+ TEST_DNS : ${{ secrets.TEST_DNS }}
+ TestingDomain: ${{ secrets.TestingDomain }}
+ TEST_DNS_NO_WILDCARD: ${{ secrets.TEST_DNS_NO_WILDCARD }}
+ TEST_DNS_NO_SUBDOMAIN: ${{ secrets.TEST_DNS_NO_SUBDOMAIN }}
+ TEST_DNS_SLEEP: ${{ secrets.TEST_DNS_SLEEP }}
+ CASE: le_test_dnsapi
+ TEST_LOCAL: 1
+ DEBUG: ${{ secrets.DEBUG }}
+ http_proxy: ${{ secrets.http_proxy }}
+ https_proxy: ${{ secrets.https_proxy }}
+ TokenName1: ${{ secrets.TokenName1}}
+ TokenName2: ${{ secrets.TokenName2}}
+ TokenName3: ${{ secrets.TokenName3}}
+ TokenName4: ${{ secrets.TokenName4}}
+ TokenName5: ${{ secrets.TokenName5}}
+ steps:
+ - 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
diff --git a/.github/workflows/GhostBSD.yml b/.github/workflows/GhostBSD.yml
new file mode 100644
index 00000000..2dd2412b
--- /dev/null
+++ b/.github/workflows/GhostBSD.yml
@@ -0,0 +1,80 @@
+name: GhostBSD
+on:
+ push:
+ branches:
+ - '*'
+ paths:
+ - '*.sh'
+ - '.github/workflows/GhostBSD.yml'
+
+ pull_request:
+ branches:
+ - dev
+ paths:
+ - '*.sh'
+ - '.github/workflows/GhostBSD.yml'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+
+
+jobs:
+ GhostBSD:
+ strategy:
+ matrix:
+ include:
+ - TEST_ACME_Server: "LetsEncrypt.org_test"
+ CA_ECDSA: ""
+ CA: ""
+ CA_EMAIL: ""
+ TEST_PREFERRED_CHAIN: (STAGING)
+ - TEST_ACME_Server: "LetsEncrypt.org_test"
+ CA_ECDSA: ""
+ CA: ""
+ CA_EMAIL: ""
+ TEST_PREFERRED_CHAIN: (STAGING)
+ ACME_USE_WGET: 1
+ #- TEST_ACME_Server: "ZeroSSL.com"
+ # CA_ECDSA: "ZeroSSL ECC DV SSL CA 2"
+ # CA: "ZeroSSL RSA DV SSL CA 2"
+ # CA_EMAIL: "githubtest@acme.sh"
+ # TEST_PREFERRED_CHAIN: ""
+ runs-on: ubuntu-latest
+ env:
+ TEST_LOCAL: 1
+ TEST_ACME_Server: ${{ matrix.TEST_ACME_Server }}
+ CA_ECDSA: ${{ matrix.CA_ECDSA }}
+ CA: ${{ matrix.CA }}
+ CA_EMAIL: ${{ matrix.CA_EMAIL }}
+ TEST_PREFERRED_CHAIN: ${{ matrix.TEST_PREFERRED_CHAIN }}
+ ACME_USE_WGET: ${{ matrix.ACME_USE_WGET }}
+ steps:
+ - uses: actions/checkout@v6
+ - uses: anyvm-org/cf-tunnel@v0
+ id: tunnel
+ with:
+ protocol: http
+ port: 8080
+ - name: Set envs
+ run: echo "TestingDomain=${{steps.tunnel.outputs.server}}" >> $GITHUB_ENV
+ - name: Clone acmetest
+ run: cd .. && git clone --depth=1 https://github.com/acmesh-official/acmetest.git && cp -r acme.sh acmetest/
+ - uses: vmactions/ghostbsd-vm@v1
+ with:
+ debug-on-error: ${{ vars.DEBUG_ON_ERROR }}
+ envs: 'TEST_LOCAL TestingDomain TEST_ACME_Server CA_ECDSA CA CA_EMAIL TEST_PREFERRED_CHAIN ACME_USE_WGET'
+ nat: |
+ "8080": "80"
+ prepare: pkg install -y socat curl wget
+ usesh: true
+ sync: nfs
+ run: |
+ cd ../acmetest \
+ && ./letest.sh
+ - name: DebugOnError
+ if: ${{ failure() }}
+ run: |
+ echo "See how to debug in VM:"
+ echo "https://github.com/acmesh-official/acme.sh/wiki/debug-in-VM"
diff --git a/README.md b/README.md
index 22700b4c..3a697691 100644
--- a/README.md
+++ b/README.md
@@ -17,6 +17,7 @@
+
@@ -114,6 +115,7 @@
|24|[](https://github.com/acmesh-official/letest#here-are-the-latest-status)| Proxmox: See Proxmox VE Wiki. Version [4.x, 5.0, 5.1](https://pve.proxmox.com/wiki/HTTPS_Certificate_Configuration_(Version_4.x,_5.0_and_5.1)#Let.27s_Encrypt_using_acme.sh), version [5.2 and up](https://pve.proxmox.com/wiki/Certificate_Management)
|25|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Haiku.yml)|Haiku OS
|26|[](https://github.com/acmesh-official/acme.sh/actions/workflows/Tribblix.yml)|Tribblix
+|27|[](https://github.com/acmesh-official/acme.sh/actions/workflows/GhostBSD.yml)|GhostBSD
> 🧪 Check our [testing project](https://github.com/acmesh-official/acmetest)
From db098055de3ea8012190db7e71d02a0e229a0e75 Mon Sep 17 00:00:00 2001
From: SpeedGriffon <5631890+SpeedGriffon@users.noreply.github.com>
Date: Fri, 19 Jun 2026 14:23:03 +0200
Subject: [PATCH 17/18] Fix RouterOS deploy (#7034)
* routeros: save ROUTER_OS_ADDITIONAL_SERVICES as base64
* routeros: remove cer_3
---
deploy/routeros.sh | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/deploy/routeros.sh b/deploy/routeros.sh
index ef9c6954..328fabbd 100644
--- a/deploy/routeros.sh
+++ b/deploy/routeros.sh
@@ -125,7 +125,7 @@ routeros_deploy() {
_savedeployconf ROUTER_OS_PORT "$ROUTER_OS_PORT"
_savedeployconf ROUTER_OS_SSH_CMD "$ROUTER_OS_SSH_CMD"
_savedeployconf ROUTER_OS_SCP_CMD "$ROUTER_OS_SCP_CMD"
- _savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES"
+ _savedeployconf ROUTER_OS_ADDITIONAL_SERVICES "$ROUTER_OS_ADDITIONAL_SERVICES" "base64"
# push key to routeros
if ! _scp_certificate "$_ckey" "$ROUTER_OS_USERNAME@$ROUTER_OS_HOST:$_cdomain.key"; then
@@ -143,6 +143,7 @@ comment=\"generated by routeros deploy script in acme.sh\" \
source=\"/certificate remove [ find name=$_cdomain.cer_0 ];\
\n/certificate remove [ find name=$_cdomain.cer_1 ];\
\n/certificate remove [ find name=$_cdomain.cer_2 ];\
+\n/certificate remove [ find name=$_cdomain.cer_3 ];\
\ndelay 1;\
\n/certificate import file-name=\\\"$_cdomain.cer\\\" passphrase=\\\"\\\";\
\n/certificate import file-name=\\\"$_cdomain.key\\\" passphrase=\\\"\\\";\
From 365d2d10f3d5e170d6e9b92d2f79b2c8b86bdd75 Mon Sep 17 00:00:00 2001
From: regisvidal-bitmapz
Date: Fri, 19 Jun 2026 14:24:20 +0200
Subject: [PATCH 18/18] Fix dns_namesilo_rm failing to remove TXT record
(#6969)
* Fixes #6907
---
dnsapi/dns_namesilo.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/dnsapi/dns_namesilo.sh b/dnsapi/dns_namesilo.sh
index 5d47a59a..df5871cf 100755
--- a/dnsapi/dns_namesilo.sh
+++ b/dnsapi/dns_namesilo.sh
@@ -65,7 +65,7 @@ dns_namesilo_rm() {
if _namesilo_rest GET "dnsListRecords?version=1&type=xml&key=$Namesilo_Key&domain=$_domain"; then
retcode=$(printf "%s\n" "$response" | _egrep_o "300")
if [ "$retcode" ]; then
- _record_id=$(echo "$response" | _egrep_o "([^<]*)TXT$fulldomain" | _egrep_o "([^<]*)" | sed -r "s/([^<]*)<\/record_id>/\1/" | tail -n 1)
+ _record_id=$(echo "$response" | _egrep_o "([^<]*)TXT$_sub_domain$txtvalue" | _egrep_o "([^<]*)" | sed -r "s/([^<]*)<\/record_id>/\1/" | tail -n 1)
_debug _record_id "$_record_id"
if [ "$_record_id" ]; then
_info "Successfully retrieved the record id for ACME challenge."