Since 329dab9a (3.1.3) the token/key is saved with
_saveaccountconf_mutable, but only _add read it back. rm runs in its
own subshell, so on cron renewal it called the API unauthenticated;
the 401 body has no "code": 401, so _get_root took the first probe as
the zone and the TXT record was never removed.
Fixes#7347
Record names were sent relative to the zone, derived from _sub_domain.
That has no valid form when the TXT name is the zone apex, which
happens with --domain-alias pointing at a dedicated zone:
- YC_Zone_ID: _sub_domain ends up empty and the API rejects the
request ("Length must be in the range of 1 to 254").
- YC_Folder_ID: _get_root returns the first label as _sub_domain, so
the record lands at <alias>.<alias>. and validation never succeeds.
The API accepts absolute names (trailing dot) everywhere a relative one
is accepted, so send $fulldomain, which is already lowercased and
dot-terminated. Ordinary _acme-challenge.<sub> names keep working.
Supersedes #6862.
* handle OAuth response
* revert comment removal
* handle lowercased domain value
* expand error detection logic for all non-expected responses
* shell-independent lowercasing
* revert; domain value already lowercased
* add missing double-quotes
* use zone records endpoint fallback when zone cannot be identified
* add logic missing from previous
---------
Co-authored-by: neil <github@neilpang.com>
Co-authored-by: Mashiro <adadam@qq.com>
Co-authored-by: MBWhitestone <25477219+MBWhitestone@users.noreply.github.com>
Co-authored-by: Pablo <Pablo1@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* Add Abion.com DNS API
* Update dns_abion.sh
* dns_abion: keep existing TXT records on add and rm
Continues acmesh-official/acme.sh#6885 by @stelar7 and addresses
the review comments there.
- add: append to the existing TXT values at the name instead of
replacing them, so wildcard + base certificates work
- rm: remove only the matching value, keep the TTL and comments of
the other records, and clear the name with null when it is empty
- only read the TXT records of the target name; the old parser also
picked up TXT records from other names in the zone
- find the zone with GET zones/<name> instead of the paginated zone
list, and stop with the API error on anything but "Zone not found"
- add Docs line and progress messages, fix shfmt
Co-authored-by: stelar7 <dudedbz@gmail.com>
* dns_abion: lowercase the domain so rm finds the record
* dns_abion: add tracking issue to info block
* dns_abion: align info block with the structural info format
---------
Co-authored-by: stelar7 <dudedbz@gmail.com>
* Changes for new selfhost api
* Skip acmetestXyzRandomName test, selfhost does not longer support dns updates to other recods than _acme-challenge.
* Add selfhost migration hint and instructions
---------
Co-authored-by: neil <github@neilpang.com>
* feat: add initial version of MVMNET dns api
* fix: adjust shell format with shfmt
* feat: add initial version of MVMNET dns api
* fix: adjust shell format with shfmt
* Change runner from ubuntu-latest to self-hosted
* Revert "Change runner from ubuntu-latest to self-hosted"
This reverts commit 55eb217fa1.
* fix(dns_mvmnet): use configurable API endpoint
* fix(dns_mvmnet): extract records portably
* fix(dns_mvmnet): namespace private helpers
* fix(dns_mvmnet): stop on missing credentials
* fix(dns_mvmnet): reuse normalized subdomain
* fix(dns_mvmnet): reuse validated credentials
* docs(dns_mvmnet): update API and issue links
_lower_case reads stdin only. Called as _lower_case "$x", the argument
was dropped and tr read the inherited stdin. On an interactive tty that
never reaches EOF, so dns_baidu hung forever on every add/rm (#7319).
Under cron stdin is /dev/null and the value came back empty: dns_baidu
fell into the default engine order by luck, and the dns_bhosted
fallback error checks could never match.
Fixes#7319
The Yandex Cloud DNS API now returns compact JSON ("done":true,
"zone":"..."), so the add/rm success checks and the folder zone lookup
never matched. Also fix the zone id lookup when a zone has labels, and
strip the trailing dot from the record name in YC_Zone_ID mode.
* fix: improved record txt record matching
matching txt records sometimes failed if they contained certain chars causing leftover challenges in dns zone
* fix(dns_poweradmin): use portable record and zone matching
grep -o and grep -F are not supported by Solaris /usr/bin/grep.
Split JSON objects with the core _egrep_o wrapper and match name,
type and content with case, which is literal by construction. Loop
over a here-doc since case inside $(...) breaks bash 3.2 and
Solaris sh. Apply the same to _get_root.
Drop _json_escape: hostnames and base64url challenge values cannot
contain quotes or backslashes, so escaping was a no-op.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: neil <github@neilpang.com>
Co-authored-by: Jakob Næss <jakob@itsjefen.no>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* Add DNSMint DNS API
DNSMint mints hostnames on domains it operates and serves from its own
authoritative nameservers, so a customer has no zone of their own and the
challenge is published through its API.
The hostname is derived server-side from the challenge name, so there is
no root zone to detect and no record id to track: the value published is
the value removed. Wildcards work because the API keeps the two newest
values for a name, which is what the apex and wildcard pair needs.
Tested against Let's Encrypt staging for a wildcard plus its apex.
* 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.
* dnsmint: honour DNSMint_Api instead of overwriting it
The assignment was unconditional, so exporting DNSMint_Api did nothing - the
plugin reset it to the default every time it was sourced. Every neighbouring
plugin with an _Api variable treats it as an override; this one only looked
like it did.
Found while writing the dnsapi2 entry: the sentence documenting the override
would have been false.
* dnsmint: format case blocks with shfmt -i 2
* dnsmint: portable record lookup on removal
grep -F is not on Solaris /usr/bin/grep, and "\n" in a sed replacement is a
GNU extension that BSD sed writes as a literal n. The second is the one that
loses data: without the split the whole reply stays on one line, so the match
succeeds whatever the value is and the id taken is the first record under the
hostname rather than the one holding the challenge. Removal then deletes
somebody else's TXT record.
Literal newline in the replacement, as dns_glesys.sh does it, and a case
pattern per line with the case outside a command substitution.
Docs and Issues now point at dnsapi2 and at the tracking issue upstream.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* dnsmint: echo into sed, and keep _head_n 1 on the id
Both from review on #7238.
The reply arrives with no trailing newline, so printf "%s" hands sed an
incomplete final line. Solaris /usr/bin/sed discards one, and here that line
is the whole reply: the loop then sees nothing and every removal reports the
record already gone, leaving the challenge TXT behind. echo, as lines 154 and
172 of this file already do.
_head_n 1 was dropped in 61ef816c. A record object carrying a nested id under
"data" makes _rid two lines and the DELETE URL is then malformed.
---------
Co-authored-by: kxbnb <hello@dnsmint.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Enhance Azure DNS script for private zones support
Added support for Azure Private DNS Zones and updated API versions accordingly.
* Update README.md
* Update Bearer Token description in dns_azure.sh
Clarified the usage of the Bearer Token in the script.
* Update dns_azure.sh
* Update dns_azure.sh
* Update README.md
* implement optimization based on input from maintainer
* Update dns_azure.sh
* create reusable function
* optimize function usage and make less verbose
---------
Co-authored-by: neil <github@neilpang.com>
Co-authored-by: Mashiro <adadam@qq.com>
Co-authored-by: MBWhitestone <25477219+MBWhitestone@users.noreply.github.com>
Co-authored-by: Pablo <Pablo1@users.noreply.github.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Adds dns_opteamax.sh, solving dns-01 challenges through the Opteamax
customer API (api.opteam.ax). Authentication is a Bearer token created in
the customer panel; the zone is detected by walking the name up against the
account's zone list, so subzones and DNS alias mode both work, and rm only
removes the value it was given so a wildcard's two TXT records survive each
other.
* Add Optidata Cloud DNS API (dns_optidata)
* dns_optidata: portable TXT match, drop undocumented "internal" case
Review feedback on #7243:
1. grep -F is not portable: Solaris /usr/bin/grep has neither -F nor --.
The DNS test only passed there because the CI job prepends
/usr/gnu/bin to PATH. txtvalue is base64url ([A-Za-z0-9_-]), so it
needs no JSON escaping and a case pattern matches it with a shell
builtin, without depending on any grep extension.
2. Dropped the undocumented "internal" exception from the zone status
notice in _get_root. It only silenced an _info message, and a dns-01
challenge has to resolve publicly anyway, so an internal zone can
never validate through this hook.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* dns_netcup: add support for the new netcup REST API
Domains managed by the new DNS backend can be handled through the new
REST API at api.netcup.com. The API is selected by the length of
NC_Apikey: new REST API keys are 64 characters long, legacy CCP API
keys are 50.
With a REST API key the domain is looked up via GET /v1/domain and the
challenge record is managed through the dedicated ACME challenge
endpoints. After adding a record, the script waits 20 seconds and then
polls until the record reports the deployed status.
Domains whose DNS cannot be managed via the REST API yet fall back to
the legacy CCP API when NC_Apikey_Legacy, NC_Apipw and NC_CID are
configured.
* dns_netcup: treat non-challenge records as a no-op on the REST API
The REST API can only manage _acme-challenge records, records with
other names cannot exist behind it. The DNS-API-Test adds and removes
a TXT record outside _acme-challenge and expects both calls to
succeed, so treat such records as a successful no-op with an info
message instead of failing.
* dns_netcup: address review feedback for the REST API support
- Only skip the synthetic DNS-API-Test record: real records without
the _acme-challenge prefix (e.g. a challenge alias in the "=" form)
now fail loudly, or use the legacy CCP API when legacy credentials
are configured. The zone walk starts at the full name for them, so
an apex alias is found.
- Blank _H2..._H5 for REST API calls and clear all header slots before
legacy CCP API calls so no auth headers leak between endpoints or
dns hooks.
- Stop walking the zone lookup when the API reports success:false and
surface the response instead of a misleading "no zone found".
- Split the response before extracting id/isDnsManaged so the egrep
and sed implementations of _egrep_o cannot pick different matches.
- Fall back to the legacy CCP API only on a literal isDnsManaged
false; error distinctly on an unparsable value.
- Poll the deploy status right away and sleep between retries instead
of an unconditional 20 second sleep.
- Use ${#NC_Apikey} for the key length and rename internal state to
_nc_apikey/_nc_endrest.
* dns_netcup: walk on when the REST API reports resourceDoesNotExist
Querying /domain?fqdn= for a name that is not a domain of the account
does not return an empty result: the API answers with success:false
and the error code resourceDoesNotExist. Treat exactly that as "not
found" during the zone walk and keep failing hard on everything else,
e.g. an invalid API key.
Versions up to 3.0.9 cached the internally-acquired access token as
SAVED_AZUREDNS_BEARERTOKEN. 3.1.0 repurposed that variable for
user-supplied bearer tokens, so after an upgrade the stale cached token
was read back as if user-supplied, skipped the refresh path, and failed
renewals with 401 forever once expired.
A bearer token is short-lived, so persisting it is never useful: take it
from the environment only, and clear any stale saved value on the next
run.
fix https://github.com/acmesh-official/acme.sh/issues/7218
dns_easydns_rm() picked the first id in the search response and ignored
$txtvalue. When two challenge records exist under the same host - for
example when example.com and *.example.com are issued as separate
certificates - a concurrent run's record could be deleted instead of
our own.
Select the record by its rdata instead, following the dns_cf.sh
convention of matching name + value. tr '{' '\n' puts one record per
line, so both _egrep_o branches - egrep -o and the BRE sed fallback -
return the same single id. Without it the sed fallback would return
only the last match, since .* is greedy.
An empty record_id is now treated as "nothing to remove" and returns 0,
rather than being reported as an error.
Also add the credential check that _rm was missing. It deliberately
does not call _saveaccountconf_mutable, as _add already does that.
Co-authored-by: wurzelpanzer <wurzelpanzer@maximolider.net>
The zone lookup walked the challenge name from the right and ended up
asking netcup for the full "_acme-challenge.<domain>" as a zone name.
That can never be a zone, so netcup answered 4013 "Validation Error",
which replaced the real 5028 "The zone <domain> could not be found" as
the error shown to the user.
Stop one label short of the full name, and fail explicitly when no zone
matched, reporting the last API response plus what to check. Before, a
run where every candidate returned 5028 fell through to logout and
returned success.
* feat: added nexdns dnsapi
Adds a DNS-01 hook for NexDNS, an authoritative DNS service with a REST API.
dns_nexdns_add walks the label list to find the zone that owns the challenge
name and creates the TXT record in it. dns_nexdns_rm lists the TXT records at
that name, picks the one carrying exactly this challenge value and deletes it
by id, so a wildcard and its base domain do not remove each other's record.
A 429 is waited out and the request retried, in the shape dns_hetznercloud.sh
and dns_bunny.sh already use.
* dns_nexdns: cap the rate-limit wait, judge success by status, add the tracking issue
_get_root_by_getList() matched the candidate suffix as an unanchored
substring of the whole domains.getList response and never looked at the
IsOurDNS attribute. A domain parked on Namecheap's webhosting DNS is
listed with IsOurDNS="false", yet it was still accepted as the root zone,
so _get_root() returned success and the domains.dns.getHosts probe that
would have found the real zone never ran. Every following getHosts call
was then refused with error 2030288 "not using proper DNS servers" and
the challenge failed with "invalid tld".
Match the exact <Domain Name="..."> entry instead and require
IsOurDNS="true", so a subdomain delegated to Namecheap BasicDNS/FreeDNS
under a parent that is not on Namecheap DNS now resolves to its own zone.
Matching the entry exactly also drops the old substring/regex match, in
which the dots of a domain matched any character.
Fixes#7178
_cyon_delete_txt relied on `printf "%b"` to convert a sed-injected literal
`\n` into a real newline, but `%b` also processes the `\"` escapes that the
JSON response is full of. glibc/bash/dash keep the backslash of such an
undefined escape, FreeBSD's printf (sh builtin and /usr/bin/printf alike)
drops it -- so `data-hash=\"..\"` became `data-hash=".."`, the extraction
regex matched nothing, _dns_entries stayed empty and no TXT record was ever
deleted.
Drop the newline injection and use _egrep_o, which already yields one match
per line, then parse each line with sed.
Also feed the read loop a newline-terminated list: `printf "%s"` left the
last line unterminated, so `read` returned non-zero at EOF and the loop
skipped the final entry on every platform.
Verified identical output on FreeBSD 14.3, Linux/bash and Linux/dash.
Fixes#7169
* Refactor dns_freemyip.sh for clarity and compatibility
Updated dns_freemyip.sh for better readability and compatibility with ASUSWRT-Merlin. Improved error handling and response logging.
* Update author information in dns_freemyip.sh
* replace both loops with POSIX shell counters
replace both loops with POSIX shell counters
* Typo
Typo
* Fix error message for freemyip API request failure
Remove existing token leak. Not my regression.
* Refactor retry logic and improve error handling
* Remove unnecessary blank lines in dns_freemyip.sh
* Clean up dns_freemyip.sh by removing blank lines
Removed unnecessary blank lines in the script to improve readability.
* dns_yc: restore YC_SA_Key_File in dns_yc_rm before signing the JWT
dns_yc_rm() never rebuilt YC_SA_Key_File from YC_SA_Key_File_PEM_b64 /
YC_SA_Key_File_Path like dns_yc_add() does. Per the DNS API dev guide,
add()/rm() run in separate subshells, so rm() must repeat add()'s setup
steps rather than rely on variables set during add().
Without it, when _yc_login() needs a fresh JWT during removal (the IAM
token from the add phase isn't available), it signs with an empty/unset
key path, and openssl fails with "Unknown key file format". The
resulting auth failure then surfaces misleadingly as "invalid domain" in
_get_root, and the TXT record is never deleted.
Verified against a real Yandex Cloud account/zone with --staging: before
the fix, removal failed with the same errors reported in the issue;
after adding the missing key-restoration block, add + remove both
succeed and the TXT record is actually deleted.
* dns_yc: preserve other TXT values when removing one at the same name
dns_yc_rm previously sent the full current data array (all existing
TXT values at the name) to the deletions API, wiping out the whole
rrset instead of only the value being removed. This breaks wildcard +
base domain issuance, where both share the same _acme-challenge name
with two different values: removing the first one deleted both,
leaving nothing for the second removal to find.
* dns_yc: read persisted config from domain conf before account conf
YC_Zone_ID, YC_Folder_ID, YC_SA_ID, YC_SA_Key_ID (zone-ID mode) and
YC_SA_Key_File_PEM_b64/Path were always saved via _savedomainconf
(domain.conf), but only ever read back via _readaccountconf_mutable
(account.conf). Once the env vars were unset, none of these could be
recovered from the saved config, so dns_yc_add/dns_yc_rm failed with
"You didn't specify a YC_SA_ID or YC_SA_Key_ID or YC_SA_Key_File."
even though the values had been persisted correctly on the prior run.
* dns_yc: replace grep -Fxv/sed with a portable loop in dns_yc_rm
Solaris's /usr/bin/grep supports neither -F nor -x, so
_remaining_txtvalue was always empty there and the preserve-other-
values logic silently fell back to deleting the whole rrset (with a
grep usage error on stderr on every rm). The sed trailing-comma strip
had a matching issue on Solaris, whose sed drops an unterminated last
line. CI didn't catch this because the fallback path also returns
"done: true". Use a plain for-loop with word splitting instead.
* dns_yc: use upsertRecordSets.deletions to remove a single TXT value
updateRecordSets has no "merges" field (only deletions/additions), so
the previous preserve-other-values logic silently did nothing -- the
TXT record was never actually removed, a regression from before that
change (which at least deleted the whole rrset). CI didn't catch it
because _clearupdns runs dns_yc_rm in a subshell and ignores its exit
code.
upsertRecordSets.deletions removes only the specified value from the
rrset directly, so the getRecordSet read and the remaining-value
recomputation are no longer needed at all.
Verified against a real zone (base + wildcard domain sharing one
_acme-challenge name): adding both values then removing one leaves
the other in place, and removing the second cleans up fully.
* dns_yc: don't delete the user's own key file in YC_SA_Key_File_Path mode
_yc_login unconditionally rm'd $YC_SA_Key_File after signing. That's
fine for the PEM_b64 path, where it's a decoded temp file, but in
YC_SA_Key_File_Path mode it's the user's own persistent key file --
the first successful login permanently deleted it, so every
subsequent dns_yc_rm/renewal hit "Unknown key file format" (the exact
symptom this PR is about, just from a different cause). Track whether
the key file is our own temp copy and only delete it in that case.
Verified with a stubbed _yc_login: a temp-mode key gets removed after
login, a path-mode key survives.
* dns_yc: clear both domain and account conf on invalid config
The failure branch in dns_yc_add only ever called _clearaccountconf,
but YC_Zone_ID/YC_Folder_ID/YC_SA_Key_File_PEM_b64/Path are persisted
via _savedomainconf, and YC_SA_ID/YC_SA_Key_ID may have been saved via
_saveaccountconf_mutable (Folder_ID mode, which stores under a
SAVED_ prefix read back by _readaccountconf_mutable). Clearing only
one store left stale values behind in whichever one wasn't touched.
Verified by seeding both domain.conf and account.conf with leftover
values, then triggering this branch and confirming both config files
end up empty.
Solaris /usr/bin/grep has no -A ("illegal option -- A"), so _getAKI
printed an error to stderr on every cron renewal and returned empty.
The empty AKI silently corrupts the RFC 9773 ARI certID, so ARI is
never available and renewal falls back to the fixed schedule.
Split the pipeline into a testable stdin filter _extractAKI and select
the value line with a portable sed range instead.
Same fix for the two hooks that still used grep -A: dns_world4you.sh
(also replaces the GNU-only "\s" in the same expression) and
deploy/keyhelp.sh (the -A 2 window could truncate the div range that
follows it, so it is just dropped).
https://github.com/acmesh-official/acme.sh/issues/7159
OmniOS native egrep -o infinite-loops emitting empty lines when the
pattern can match the empty string, so `_egrep_o "[^:]*$"` never lets
the pipeline finish and dns_yc hangs until the CI timeout. Require at
least one character instead. `+` is not usable because the sed fallback
in _egrep_o parses BRE.
dnsapi/deploy: remove POSIX character classes from sed/grep patterns
Solaris /usr/bin/sed and /usr/bin/grep parse [[:space:]] etc. as a
literal bracket set and silently mis-match. Replace with [ ]* for
JSON matching, a printf-tab bracket for user-input trimming, and
[0-9] for digits; also drop GNU-only sed -r/-E in rage4, selfhost
and selectel, and reuse _strip_blank_lines in byteplus_alb.
The OCI DNS plugin cached the tenancy, user, region and signing key into
acme.sh's account.conf at issuance and then, on subsequent runs, read those
cached values before consulting ~/.oci/config. A value cached at issuance
therefore permanently shadowed the config file, so editing ~/.oci/config
afterwards (most visibly rotating the API signing key) had no effect and
renewals kept using stale credentials, failing authentication.
Resolve each field in the order: explicit environment variable, then
~/.oci/config when it exists, then the cached account.conf value as a
fallback for env-only installs that have no config file. The signing key
likewise prefers the key_file resolved from the environment or ~/.oci/config
over any cached key.
Signed-off-by: Avi Miller <me@dje.li>
The sed in _get_zone passed the raw JSON through when the response
contained no [{"fqdn":", so an API error turned the zone into "{" and
the failure only surfaced later as POST /2/zones/{/records
"method_not_found". Log the response at debug2, error out on
non-success results, and parse fqdn position-independently.
https://github.com/acmesh-official/acme.sh/issues/6851
The zone cannot be derived from the record name when the Knot server
is only authoritative for a delegated subdomain; let the user name it
explicitly, like NSUPDATE_ZONE.
fixes https://github.com/acmesh-official/acme.sh/issues/2881
The legacy plain _saveaccountconf copy in account.conf is sourced at
startup and silently overrides a newly exported DNSimple_OAUTH_TOKEN,
so rotated tokens never took effect.
fixes https://github.com/acmesh-official/acme.sh/issues/3422