I can confirm this behavior
This section allows you to view all posts made by this member. Note that you can only see posts made in areas you currently have access to.
Show posts Menu#!/bin/sh
# ============================================================
# TrueNAS SCALE ACME Shell Authenticator for easyDNS
#
# Script:
# /mnt/Pool/Scripts/ACME/acmedns.sh
#
#:
#
# TrueNAS-Format:
# acmedns.sh set scale.domain.com _acme-challenge.scale.domain.com TOKEN
# acmedns.sh unset scale.domain.com _acme-challenge.scale.domain.com TOKEN
#
# Fallback-Format:
# acmedns.sh set _acme-challenge.scale.domain.com TOKEN
# acmedns.sh unset _acme-challenge.scale.domain.com TOKEN
# ============================================================
set -eu
LOG_DIR="/mnt/Pool/Scripts/ACME"
LOG_FILE="${LOG_DIR}/acmedns.log"
MAX_LOG_BYTES=$((1024 * 1024))
API_BASE="https://rest.easydns.net"
EASYDNS_DOMAIN="domain.com"
EASYDNS_TTL=300
EASYDNS_TOKEN_FILE="/mnt/Pool/Scripts/ACME/easydns.token"
EASYDNS_KEY_FILE="/mnt/Pool/Scripts/ACME/easydns.key"
PROPAGATION_SLEEP=60
mkdir -p "$LOG_DIR"
rotate_log_if_needed() {
if [ -f "$LOG_FILE" ]; then
size="$(stat -c%s "$LOG_FILE" 2>/dev/null || echo 0)"
if [ "$size" -gt "$MAX_LOG_BYTES" ]; then
: > "$LOG_FILE"
fi
fi
}
log() {
echo "[$(date '+%F %T')] $*" >> "$LOG_FILE"
}
fail() {
log "ERROR: $*"
exit 1
}
rotate_log_if_needed
[ -f "$EASYDNS_TOKEN_FILE" ] || fail "Missing EasyDNS token file: $EASYDNS_TOKEN_FILE"
[ -f "$EASYDNS_KEY_FILE" ] || fail "Missing EasyDNS key file: $EASYDNS_KEY_FILE"
EASYDNS_TOKEN="$(tr -d '\r\n' < "$EASYDNS_TOKEN_FILE")"
EASYDNS_KEY="$(tr -d '\r\n' < "$EASYDNS_KEY_FILE")"
[ -n "$EASYDNS_TOKEN" ] || fail "EasyDNS token file is empty"
[ -n "$EASYDNS_KEY" ] || fail "EasyDNS key file is empty"
record_to_host() {
record="$1"
# trailing dot entfernen und lowercase
record="$(printf '%s' "$record" | sed 's/\.$//' | tr '[:upper:]' '[:lower:]')"
case "$record" in
*."$EASYDNS_DOMAIN")
printf '%s\n' "${record%.$EASYDNS_DOMAIN}"
;;
"$EASYDNS_DOMAIN")
printf '%s\n' "@"
;;
*)
fail "Challenge record '$record' is not inside zone '$EASYDNS_DOMAIN'"
;;
esac
}
json_body_for_add() {
host="$1"
token="$2"
python3 - "$host" "$EASYDNS_DOMAIN" "$EASYDNS_TTL" "$token" <<'PY'
import sys, json
host = sys.argv[1]
domain = sys.argv[2]
ttl = int(sys.argv[3])
token = sys.argv[4]
body = {
"host": host,
"domain": domain,
"ttl": ttl,
"prio": 0,
"type": "txt",
"rdata": token,
}
print(json.dumps(body, separators=(",", ":")))
PY
}
api_get_records() {
curl -fsS \
-u "${EASYDNS_TOKEN}:${EASYDNS_KEY}" \
--connect-timeout 15 \
--max-time 60 \
"${API_BASE}/zones/records/all/${EASYDNS_DOMAIN}?format=json"
}
api_add_txt() {
body="$1"
curl -fsS \
-u "${EASYDNS_TOKEN}:${EASYDNS_KEY}" \
-X PUT \
--connect-timeout 15 \
--max-time 60 \
-H "Content-Type: application/json" \
-d "$body" \
"${API_BASE}/zones/records/add/${EASYDNS_DOMAIN}/txt?format=json"
}
api_delete_record() {
id="$1"
log "Trying DELETE with confirm body for id=$id"
if curl -fsS \
-u "${EASYDNS_TOKEN}:${EASYDNS_KEY}" \
-X DELETE \
--connect-timeout 15 \
--max-time 60 \
-H "Content-Type: application/json" \
-d '{"confirm":"DELETE"}' \
"${API_BASE}/zones/records/${EASYDNS_DOMAIN}/${id}?format=json" >> "$LOG_FILE" 2>&1; then
log "DELETE with confirm body succeeded for id=$id"
return 0
fi
log "DELETE with confirm body failed for id=$id, trying fallback DELETE without body"
curl -fsS \
-u "${EASYDNS_TOKEN}:${EASYDNS_KEY}" \
-X DELETE \
--connect-timeout 15 \
--max-time 60 \
"${API_BASE}/zones/records/${EASYDNS_DOMAIN}/${id}?format=json" >> "$LOG_FILE" 2>&1
log "Fallback DELETE succeeded for id=$id"
}
find_txt_record_ids() {
host="$1"
token="${2:-}"
records_json="$3"
printf '%s' "$records_json" | python3 - "$host" "$token" <<'PY'
import sys, json
host = sys.argv[1].lower()
token = sys.argv[2]
try:
data = json.load(sys.stdin)
except Exception as e:
print(f"JSON_ERROR:{e}", file=sys.stderr)
sys.exit(2)
for r in data.get("data", []):
rtype = str(r.get("type", "")).lower()
rhost = str(r.get("host", "")).lower()
rrdata = r.get("rdata", r.get("rData", ""))
rrdata = str(rrdata).strip()
rrdata_unquoted = rrdata.strip("\"")
if rtype == "txt" and rhost == host:
if not token or rrdata == token or rrdata_unquoted == token:
rid = r.get("id")
if rid is not None:
print(rid)
PY
}
delete_txt_record() {
host="$1"
token="${2:-}"
log "Searching TXT records for host=$host token=${token:-none}"
records_json="$(api_get_records)"
log "easyDNS records fetched"
ids="$(find_txt_record_ids "$host" "$token" "$records_json" || true)"
if [ -z "$ids" ] && [ -n "$token" ]; then
log "No TXT record found with exact token. Searching again by host only."
ids="$(find_txt_record_ids "$host" "" "$records_json" || true)"
fi
if [ -z "$ids" ]; then
log "No matching TXT record found for host=$host"
return 0
fi
echo "$ids" | while IFS= read -r id; do
[ -z "$id" ] && continue
log "Deleting TXT record host=$host id=$id"
api_delete_record "$id"
log "Delete completed for TXT record id=$id"
sleep 2
done
}
set_record() {
challenge_fqdn="$1"
token="$2"
host="$(record_to_host "$challenge_fqdn")"
log "SET requested"
log "Challenge FQDN=$challenge_fqdn"
log "Zone=$EASYDNS_DOMAIN"
log "Host=$host"
delete_txt_record "$host" "$token" || true
body="$(json_body_for_add "$host" "$token")"
log "Adding TXT record host=$host ttl=$EASYDNS_TTL"
api_add_txt "$body" >> "$LOG_FILE" 2>&1
log "TXT record added for host=$host"
log "Waiting ${PROPAGATION_SLEEP}s for DNS propagation"
sleep "$PROPAGATION_SLEEP"
log "SET completed"
}
unset_record() {
challenge_fqdn="$1"
token="${2:-}"
host="$(record_to_host "$challenge_fqdn")"
log "UNSET requested"
log "Challenge FQDN=$challenge_fqdn"
log "Zone=$EASYDNS_DOMAIN"
log "Host=$host"
log "Token=${token:-none}"
delete_txt_record "$host" "$token"
log "UNSET completed"
}
# ------------------------------------------------------------
# Parameter Parsing
# ------------------------------------------------------------
mode="${1:-}"
arg2="${2:-}"
arg3="${3:-}"
arg4="${4:-}"
domain_fqdn=""
challenge_fqdn=""
txt_token=""
log "============================================================"
log "Raw args: mode='${mode:-}' arg2='${arg2:-}' arg3='${arg3:-}' arg4='${arg4:-}'"
case "$mode" in
set)
case "$arg2" in
_acme-challenge*)
# Fallback-Format:
# set challenge token
challenge_fqdn="$arg2"
txt_token="$arg3"
;;
*)
# TrueNAS-Format:
# set domain challenge token
domain_fqdn="$arg2"
challenge_fqdn="$arg3"
txt_token="$arg4"
;;
esac
[ -n "$challenge_fqdn" ] || fail "Missing challenge FQDN argument"
[ -n "$txt_token" ] || fail "Missing TXT token argument"
set_record "$challenge_fqdn" "$txt_token"
;;
unset)
case "$arg2" in
_acme-challenge*)
# Fallback-Format:
# unset challenge token
challenge_fqdn="$arg2"
txt_token="$arg3"
;;
*)
# TrueNAS-Format:
# unset domain challenge token
domain_fqdn="$arg2"
challenge_fqdn="$arg3"
txt_token="$arg4"
;;
esac
[ -n "$challenge_fqdn" ] || fail "Missing challenge FQDN argument"
unset_record "$challenge_fqdn" "$txt_token"
;;
*)
fail "Unknown mode '$mode'. Expected 'set' or 'unset'."
;;
esac
exit 0
#!/bin/sh
set -eu
LOG_DIR="/mnt/Pool/Scripts/ACME"
LOG_FILE="${LOG_DIR}/cleanup-acme-txt.log"
API_BASE="https://rest.easydns.net"
EASYDNS_DOMAIN="domain.com"
EASYDNS_TOKEN_FILE="/mnt/Pool/Scripts/ACME/easydns.token"
EASYDNS_KEY_FILE="/mnt/Pool/Scripts/ACME/easydns.key"
DRY_RUN=0
if [ "${1:-}" = "--dry-run" ]; then
DRY_RUN=1
fi
mkdir -p "$LOG_DIR"
log() {
echo "[$(date '+%F %T')] $*" | tee -a "$LOG_FILE"
}
fail() {
log "ERROR: $*"
exit 1
}
[ -f "$EASYDNS_TOKEN_FILE" ] || fail "Token file missing: $EASYDNS_TOKEN_FILE"
[ -f "$EASYDNS_KEY_FILE" ] || fail "Key file missing: $EASYDNS_KEY_FILE"
EASYDNS_TOKEN="$(tr -d '\r\n' < "$EASYDNS_TOKEN_FILE")"
EASYDNS_KEY="$(tr -d '\r\n' < "$EASYDNS_KEY_FILE")"
[ -n "$EASYDNS_TOKEN" ] || fail "Token file is empty"
[ -n "$EASYDNS_KEY" ] || fail "Key file is empty"
TMP_RECORDS="$(mktemp)"
TMP_IDS="$(mktemp)"
cleanup_tmp() {
rm -f "$TMP_RECORDS" "$TMP_IDS"
}
trap cleanup_tmp EXIT INT TERM
log "============================================================"
log "Starting ACME TXT cleanup for zone: $EASYDNS_DOMAIN"
if [ "$DRY_RUN" -eq 1 ]; then
log "Mode: DRY-RUN - nothing will be deleted"
else
log "Mode: DELETE - matching records will be deleted"
fi
to run this once a week to clean up the old records. It is not as smart as centralizing the task from opnsense but it does the job.