Add DNSMint DNS API

DNSMint mints hostnames on domains it operates and serves from its own
authoritative nameservers, so records are published through its API
rather than a zone you run.

An ACME challenge goes to the DNS-01 endpoint, which derives the hostname
from the challenge name: no root zone to detect, no record id to track,
and the value published is the value removed. Wildcards work because the
API keeps the two newest values for a name.

Any other TXT name is an ordinary record under a hostname and goes to the
records endpoint instead, which is why the add and rm functions branch on
the _acme-challenge label. Issuing certificates needs only the dns01:write
scope; the record endpoint needs hostnames:read and hostnames:write.

Tested against Let's Encrypt staging for a wildcard plus its apex, and
the non-challenge TXT path against the live API.
This commit is contained in:
karthik
2026-09-06 22:10:00 -04:00 Unverified
parent 6f738157ac
commit 70923c738a
+126 -19
View File
@@ -2,17 +2,17 @@
# shellcheck disable=SC2034
dns_dnsmint_info='DNSMint.com
DNSMint mints hostnames on domains it operates and serves from its own
authoritative nameservers, so the challenge is published through its API
rather than a zone you run.
authoritative nameservers, so records are published through its API rather
than a zone you run.
Site: dnsmint.com
Docs: github.com/acmesh-official/acme.sh/wiki/dnsapi#dns_dnsmint
Options:
DNSMINT_API_KEY API key carrying the dns01:write scope
DNSMINT_API_KEY API key. dns01:write is enough to issue certificates.
Issues: github.com/dnsmint/acme.sh
Author: DNSMint
'
DNSMint_Api="https://dnsmint.com/api/httpreq"
DNSMint_Api="https://dnsmint.com/api"
######## Public functions #####################
@@ -29,7 +29,18 @@ dns_dnsmint_add() {
return 1
fi
if _dnsmint_rest present "$fulldomain" "$txtvalue"; then
# An ACME challenge goes to the DNS-01 endpoint, which derives the hostname
# itself and needs only dns01:write. Any other name is an ordinary record
# under a hostname, which is a different endpoint and a wider scope.
if _startswith "$fulldomain" "_acme-challenge."; then
if _dnsmint_challenge present "$fulldomain" "$txtvalue"; then
_info "Added, OK"
return 0
fi
return 1
fi
if _dnsmint_record_add "$fulldomain" "$txtvalue"; then
_info "Added, OK"
return 0
fi
@@ -49,7 +60,15 @@ dns_dnsmint_rm() {
return 1
fi
if _dnsmint_rest cleanup "$fulldomain" "$txtvalue"; then
if _startswith "$fulldomain" "_acme-challenge."; then
if _dnsmint_challenge cleanup "$fulldomain" "$txtvalue"; then
_info "Removed, OK"
return 0
fi
return 1
fi
if _dnsmint_record_rm "$fulldomain" "$txtvalue"; then
_info "Removed, OK"
return 0
fi
@@ -72,28 +91,31 @@ _dnsmint_key() {
return 0
}
# DNSMint derives the hostname from the challenge name, so there is no zone to
# look up and no record id to track: the value published is the value removed.
_dnsmint_rest() {
action="$1"
fqdn="$2"
value="$3"
_dnsmint_headers() {
export _H1="Authorization: Bearer $DNSMINT_API_KEY"
export _H2="Accept: application/json"
export _H3="Content-Type: application/json"
}
data="{\"fqdn\":\"$fqdn\",\"value\":\"$value\"}"
_secure_debug2 data "$data"
# One request. Sets $response and $_code; returns non-zero on a transport error.
_dnsmint_rest() {
_m="$1"
_ep="$2"
_data="$3"
response="$(_post "$data" "$DNSMint_Api/$action" "" "POST")"
_dnsmint_headers
if [ "$_m" = "GET" ]; then
response="$(_get "$DNSMint_Api$_ep")"
else
_secure_debug2 _data "$_data"
response="$(_post "$_data" "$DNSMint_Api$_ep" "" "$_m")"
fi
_ret="$?"
_code="$(grep "^HTTP" "$HTTP_HEADER" | _tail_n 1 | cut -d " " -f 2 | tr -d "\\r\\n")"
_debug "http response code $_code"
_debug2 response "$response"
if [ "$_ret" != "0" ]; then
_err "error $action"
_err "error $_ep"
return 1
fi
case "$_code" in
@@ -101,8 +123,93 @@ _dnsmint_rest() {
*)
# The API says why in the body - a key narrowed to another hostname, a
# name that is not live - and that is more use than the status alone.
_err "error $action: HTTP $_code $response"
_err "error $_ep: HTTP $_code $response"
return 1
;;
esac
}
# The DNS-01 endpoint. It derives the hostname from the challenge name, so
# there is no zone to look up and no record id to track: the value published
# is the value removed.
_dnsmint_challenge() {
_action="$1"
_fqdn="$2"
_value="$3"
_dnsmint_rest POST "/httpreq/$_action" "{\"fqdn\":\"$_fqdn\",\"value\":\"$_value\"}"
}
# Everything below here is for names that are not ACME challenges. A record
# under a hostname is addressed by the hostname's id and a name relative to
# it, so the hostname has to be found first.
_dnsmint_host() {
_name="$1"
_host_id=""
_host_sub=""
if ! _dnsmint_rest GET "/v1/hostnames?limit=500"; then
return 1
fi
for _h in $(echo "$response" | _egrep_o '"hostname":"[^"]*"' | cut -d'"' -f4); do
case "$_name" in
*".$_h")
# Longest suffix wins, so a.b.example.dev prefers b.example.dev over
# example.dev when both are hostnames on the account.
if [ "${#_h}" -gt "${#_host_sub}" ]; then
_host_sub="$_h"
fi
;;
esac
done
if [ -z "$_host_sub" ]; then
_err "$_name is not under a hostname on this account"
return 1
fi
# The id sits next to the hostname in the same object.
_host_id="$(echo "$response" | _egrep_o "\"id\":\"[^\"]*\",\"hostname\":\"$_host_sub\"" | cut -d'"' -f4)"
if [ -z "$_host_id" ]; then
_err "could not read the id for $_host_sub"
return 1
fi
_record_name="${_name%".$_host_sub"}"
_debug _host_sub "$_host_sub"
_debug _record_name "$_record_name"
return 0
}
_dnsmint_record_add() {
_name="$1"
_value="$2"
if ! _dnsmint_host "$_name"; then
return 1
fi
_dnsmint_rest POST "/v1/hostnames/$_host_id/records" \
"{\"name\":\"$_record_name\",\"type\":\"TXT\",\"text\":\"$_value\"}"
}
_dnsmint_record_rm() {
_name="$1"
_value="$2"
if ! _dnsmint_host "$_name"; then
return 1
fi
if ! _dnsmint_rest GET "/v1/hostnames/$_host_id/records"; then
return 1
fi
# Records come back as {"id":...,"name":"<fqdn>","type":"TXT","ttl":...,
# "data":{...,"text":["<value>"]}}. Match on the value so a name holding
# several TXT records loses only the one that was added.
_rid="$(echo "$response" | sed 's/},{/}\n{/g' | grep -F "\"$_value\"" | _egrep_o '"id":"[^"]*"' | cut -d'"' -f4 | _head_n 1)"
if [ -z "$_rid" ]; then
_info "Record already gone, nothing to remove"
return 0
fi
_dnsmint_rest DELETE "/v1/hostnames/$_host_id/records/$_rid" ""
}