From 954d0275e9dcdf1d29a91597d66434ff11e6b3e7 Mon Sep 17 00:00:00 2001 From: Andrea Ferro Date: Sat, 17 Jan 2026 16:48:07 +0100 Subject: [PATCH 1/2] feat(dnsapi): add ApertoDNS DNS plugin Add dns_apertodns.sh for ApertoDNS dynamic DNS service. Features: - DNS-01 challenge support for Let's Encrypt - Works with standard (*.apertodns.com) and custom domains - Uses ApertoDNS Protocol API v1.2 Usage: export APERTODNS_API_KEY="your-api-key" acme.sh --issue --dns dns_apertodns -d example.apertodns.com Documentation: https://github.com/apertodns/acme-dns-apertodns --- dnsapi/dns_apertodns.sh | 201 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 dnsapi/dns_apertodns.sh diff --git a/dnsapi/dns_apertodns.sh b/dnsapi/dns_apertodns.sh new file mode 100644 index 00000000..21b8658a --- /dev/null +++ b/dnsapi/dns_apertodns.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env sh +# shellcheck disable=SC2034 +dns_apertodns_info='ApertoDNS +Site: www.apertodns.com +Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_apertodns +Options: + APERTODNS_API_KEY API Key + APERTODNS_API_URL API URL (optional, default: https://api.apertodns.com) +Author: Andrea Ferro +' + +APERTODNS_API_DEFAULT="https://api.apertodns.com" + +######## Public functions ##################### + +# Usage: dns_apertodns_add _acme-challenge.myhost.apertodns.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_apertodns_add() { + fulldomain=$1 + txtvalue=$2 + + _info "Using ApertoDNS API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + APERTODNS_API_KEY="${APERTODNS_API_KEY:-$(_readaccountconf_mutable APERTODNS_API_KEY)}" + APERTODNS_API_URL="${APERTODNS_API_URL:-$(_readaccountconf_mutable APERTODNS_API_URL)}" + + if [ -z "$APERTODNS_API_KEY" ]; then + APERTODNS_API_KEY="" + _err "You did not specify APERTODNS_API_KEY yet." + _err "Please create your API key at https://www.apertodns.com/dashboard and try again." + _err "e.g." + _err "export APERTODNS_API_KEY=apertodns_live_xxxxxxxx" + return 1 + fi + + if [ -z "$APERTODNS_API_URL" ]; then + APERTODNS_API_URL="$APERTODNS_API_DEFAULT" + fi + + # Save the credentials + _saveaccountconf_mutable APERTODNS_API_KEY "$APERTODNS_API_KEY" + if [ "$APERTODNS_API_URL" != "$APERTODNS_API_DEFAULT" ]; then + _saveaccountconf_mutable APERTODNS_API_URL "$APERTODNS_API_URL" + fi + + # Extract hostname and TXT name from fulldomain + # fulldomain: _acme-challenge.myhost.apertodns.com + # hostname: myhost.apertodns.com + # txtname: _acme-challenge + if ! _apertodns_parse_domain "$fulldomain"; then + return 1 + fi + + _debug _hostname "$_hostname" + _debug _txtname "$_txtname" + + # Build JSON payload + _info "Adding TXT record for $_hostname" + _body="{\"hostname\":\"$_hostname\",\"txt\":{\"name\":\"$_txtname\",\"value\":\"$txtvalue\",\"action\":\"set\"}}" + + if _apertodns_rest POST "/.well-known/apertodns/v1/update" "$_body"; then + if _contains "$response" "\"success\":true" || _contains "$response" "\"status\":\"good\"" || _contains "$response" "\"status\":\"nochg\""; then + _info "TXT record added successfully" + return 0 + else + _err "Failed to add TXT record: $response" + return 1 + fi + fi + + _err "Error adding TXT record" + return 1 +} + +# Usage: dns_apertodns_rm _acme-challenge.myhost.apertodns.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_apertodns_rm() { + fulldomain=$1 + txtvalue=$2 + + _info "Using ApertoDNS API" + _debug fulldomain "$fulldomain" + _debug txtvalue "$txtvalue" + + APERTODNS_API_KEY="${APERTODNS_API_KEY:-$(_readaccountconf_mutable APERTODNS_API_KEY)}" + APERTODNS_API_URL="${APERTODNS_API_URL:-$(_readaccountconf_mutable APERTODNS_API_URL)}" + + if [ -z "$APERTODNS_API_KEY" ]; then + APERTODNS_API_KEY="" + _err "You did not specify APERTODNS_API_KEY yet." + return 1 + fi + + if [ -z "$APERTODNS_API_URL" ]; then + APERTODNS_API_URL="$APERTODNS_API_DEFAULT" + fi + + # Extract hostname and TXT name from fulldomain + if ! _apertodns_parse_domain "$fulldomain"; then + return 1 + fi + + _debug _hostname "$_hostname" + _debug _txtname "$_txtname" + + # Build JSON payload + _info "Removing TXT record for $_hostname" + _body="{\"hostname\":\"$_hostname\",\"txt\":{\"name\":\"$_txtname\",\"action\":\"delete\"}}" + + if _apertodns_rest POST "/.well-known/apertodns/v1/update" "$_body"; then + if _contains "$response" "\"success\":true" || _contains "$response" "\"status\":\"good\"" || _contains "$response" "\"status\":\"nochg\""; then + _info "TXT record removed successfully" + return 0 + else + _err "Failed to remove TXT record: $response" + return 1 + fi + fi + + _err "Error removing TXT record" + return 1 +} + +#################### Private functions below ################################## + +# Parse fulldomain to extract hostname and txtname +# Input: _acme-challenge.myhost.apertodns.com +# Output: _hostname=myhost.apertodns.com, _txtname=_acme-challenge +_apertodns_parse_domain() { + domain="$1" + + # Check if domain ends with .apertodns.com + if ! _contains "$domain" ".apertodns.com"; then + _err "Domain must be under apertodns.com" + return 1 + fi + + # Extract the TXT name (first part before the hostname) + # For _acme-challenge.myhost.apertodns.com: + # - _txtname = _acme-challenge + # - _hostname = myhost.apertodns.com + + # Count dots to determine structure + # _acme-challenge.myhost.apertodns.com has 4 parts + # myhost.apertodns.com has 3 parts + + # Get everything after the first dot + _rest="$(printf "%s" "$domain" | cut -d . -f 2-)" + + # Check if _rest is a valid apertodns hostname (X.apertodns.com) + if _contains "$_rest" ".apertodns.com"; then + # The first part is the TXT name + _txtname="$(printf "%s" "$domain" | cut -d . -f 1)" + _hostname="$_rest" + else + # No subdomain prefix, use the full domain + _txtname="_acme-challenge" + _hostname="$domain" + fi + + # Validate hostname format + if [ -z "$_hostname" ] || [ -z "$_txtname" ]; then + _err "Could not parse domain: $domain" + return 1 + fi + + return 0 +} + +# REST API call +# Usage: _apertodns_rest METHOD ENDPOINT [DATA] +_apertodns_rest() { + method="$1" + endpoint="$2" + data="$3" + + url="$APERTODNS_API_URL$endpoint" + + export _H1="Authorization: Bearer $APERTODNS_API_KEY" + export _H2="Content-Type: application/json" + export _H3="Accept: application/json" + + _debug url "$url" + + if [ "$method" = "POST" ]; then + _secure_debug2 data "$data" + response="$(_post "$data" "$url" "" "POST")" + else + response="$(_get "$url")" + fi + + _ret="$?" + _debug2 response "$response" + + if [ "$_ret" != "0" ]; then + _err "API request failed" + return 1 + fi + + return 0 +} From 7258dc87dbb47bcddf6b91414a693039000bb672 Mon Sep 17 00:00:00 2001 From: Andrea Ferro Date: Sat, 17 Jan 2026 18:50:55 +0100 Subject: [PATCH 2/2] fix: add value field to delete request for wildcard certificate support --- dnsapi/dns_apertodns.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dnsapi/dns_apertodns.sh b/dnsapi/dns_apertodns.sh index 21b8658a..402ed601 100644 --- a/dnsapi/dns_apertodns.sh +++ b/dnsapi/dns_apertodns.sh @@ -103,9 +103,9 @@ dns_apertodns_rm() { _debug _hostname "$_hostname" _debug _txtname "$_txtname" - # Build JSON payload + # Build JSON payload (include value for multi-TXT support) _info "Removing TXT record for $_hostname" - _body="{\"hostname\":\"$_hostname\",\"txt\":{\"name\":\"$_txtname\",\"action\":\"delete\"}}" + _body="{\"hostname\":\"$_hostname\",\"txt\":{\"name\":\"$_txtname\",\"value\":\"$txtvalue\",\"action\":\"delete\"}}" if _apertodns_rest POST "/.well-known/apertodns/v1/update" "$_body"; then if _contains "$response" "\"success\":true" || _contains "$response" "\"status\":\"good\"" || _contains "$response" "\"status\":\"nochg\""; then