6880 Commits
  • Merge pull request #7275 from acmesh-official/dev
    allowed_signers: point the examples at 3.1.6
  • allowed_signers: point the examples at 3.1.6
    The comment block told the reader to run "git verify-tag 3.1.5", which is
    exactly the tag that fails: 3.1.5 was tagged by the GitHub release form as
    a lightweight ref and carries no signature. Use 3.1.6 in the examples and
    state the baseline, so the file that teaches verification does not hand out
    a command that cannot work.
    
    https://github.com/acmesh-official/acme.sh/issues/7273
  • Merge pull request #7274 from acmesh-official/dev
    Move the release signing baseline to 3.1.6 and catch lightweight tags…
  • Move the release signing baseline to 3.1.6 and catch lightweight tags in CI
    The 3.1.5 tag was created server-side by publishing the GitHub release,
    which can only produce a lightweight ref, so it carries no signature and
    git verify-tag fails on it. Rather than force-move a published tag, state
    that signing starts at 3.1.6 and cut that tag locally with git tag -s
    before the release is published.
    
    vtag.yml now fails the run when the pushed tag is not a tag object, so the
    same mistake shows up as a red check on the release instead of arriving as
    a user report. The mirror step runs first, so v<tag> is still created; its
    early "already exists" return became an else branch so the check is never
    skipped.
    
    https://github.com/acmesh-official/acme.sh/issues/7273
  • deploy/ruckus: rename _post_file to avoid clashing with the core helper
    Core acme.sh now defines a _post_file() function. Functions and variables
    live in separate namespaces in POSIX sh, so this was not a real conflict,
    but the identical name is confusing to read. Use _post_upfile, matching the
    _post_action / _post_boundary / _post_data locals already in this function.
  • Add DNSMint DNS API (#7238)
    * 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>
  • Add support for Private DNS zones on Azure (#7227)
    * 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>
  • Add JetKVM SSH deploy hook (#7254)
    * Add JetKVM SSH deploy hook
    
    Adds deploy/jetkvm.sh to deploy a certificate to a JetKVM
    (https://jetkvm.com) KVM-over-IP device over plain SSH, writing the
    cert/key via a small POSIX shell script piped to the remote "sh" (JetKVM
    has no scp/SFTP server), staged under temp names and atomically renamed
    into place so a dropped connection can't leave the device with a
    mismatched cert/key pair for its own HTTPS listener. Defaults target
    JetKVM's confirmed "Custom" TLS storage path/filenames and default the
    post-upload command to "reboot", since JetKVM has no hot-reload for a new
    certificate.
    
    This factors out the SSH upload logic originally proposed in
    opnsense/plugins#5621 (an OPNsense ACME Client plugin automation) per
    maintainer feedback there, so it can be reused as a small config instead
    of plugin-specific code: https://github.com/opnsense/plugins/pull/5621#issuecomment-5570647257
    
    * Fix restart-command exit-code handling in jetkvm.sh
    
    The default restart command ("reboot") tears down the very SSH
    connection running it, which real hardware testing shows makes ssh's
    own exit code unreliable: it can come back as either a clean 0 or a
    connection-reset 255 for the exact same successful reboot depending on
    timing. The hook previously trusted that single exit code directly, so
    a fully successful, unattended cron renewal could be reported as a
    failed deploy.
    
    Split into two SSH calls: the first uploads and stages the cert/key and
    its exit code is trusted as-is (no reboot risk there). The second runs
    the restart command and is judged by whether a marker line printed
    *before* that command shows up in the captured output -- if the marker
    is missing, the call never really ran (real failure); if it's present,
    only a clean exit or 255 (connection dropped, expected) counts as
    success, while any other exit code is treated as the restart command's
    own genuine failure (e.g. 127 = command not found).
    
    Also: a blank DEPLOY_JETKVM_RESTART_CMD now falls back to "reboot"
    rather than silently skipping the restart -- previously there was no
    way to actually configure "no restart command", since the hook coerced
    any blank value (including one the user deliberately set) back to
    "reboot" on every run. Skipping it now requires the explicit sentinel
    DEPLOY_JETKVM_RESTART_CMD="none".
    
    * Verify JetKVM HTTPS Mode is "Custom" before uploading
    
    Uploading a certificate that the device's active HTTPS Mode won't even
    serve was previously a silent no-op -- the write would succeed but never
    take effect until a human noticed and fixed the mode themselves.
    
    JetKVM's own JSON-RPC getTLSState/setTLSState calls require an
    authenticated WebRTC session (see jetkvm/kvm#1240 and the still-open
    jetkvm/kvm#1515), so there's no documented/headless way to query this.
    Its firmware (web_tls.go / config.go in jetkvm/kvm) does persist the
    mode as a plain JSON field, "tls_mode" (values "", "self-signed", or
    "custom"), in /userdata/kvm_config.json -- confirmed against a real
    device, including that its busybox grep handles the -E/[[:space:]]
    regex used here.
    
    The check runs as the first step of the existing upload SSH call (no
    extra round trip), exits a dedicated code (3) if "tls_mode" isn't
    "custom", and the hook surfaces that as a specific, actionable error
    pointing at the device's web UI setting, distinct from a generic upload
    failure. Since the underlying config file/field is just as undocumented
    as everything else this hook depends on, DEPLOY_JETKVM_REQUIRE_CUSTOM_MODE=no
    opts out entirely in case a future firmware version changes the format.
    
    Also tightens two things noticed while adding this: the "Uploading
    certificate..." info log no longer prints before a call that might
    immediately fail the mode check, and the remote script's own error
    echo (redundant with the local hook's more detailed _err message) is
    dropped.
    
    Confirmed end-to-end against a real JetKVM device: the regex correctly
    matched the device's actual tls_mode=custom, and a full run of the
    updated hook (mode check included) succeeded.
    
    * Address maintainer review: hardcode firmware constants, drop local temp files, fix POSIX portability
    
    Per @neilpang's review (acmesh-official/acme.sh#7254):
    
    1. Drop [[:space:]]/-E from the tls_mode grep -- not portable (Solaris
       sed/grep read it as a literal bracket set); the compact and indented
       JSON forms are both covered by a plain space with '*'.
    2. Use the core _time() wrapper instead of `date +%s || echo 0` -- the
       fallback was dead code (a date binary that doesn't understand %s
       still exits 0), and _time() is the idiom every other hook/dnsapi
       script already uses for this.
    3. Drop the local temp files entirely for both the upload and restart
       SSH calls. The upload script is now built in a variable and piped
       directly into `ssh ... sh` (same pattern as deploy/windows_rdp.sh);
       $? after the pipeline is still ssh's own exit code. This keeps the
       private key off local disk and removes the _mktemp/chmod/rm dance.
    4. Refuse to deploy when the key or fullchain file is empty (e.g. a
       --signcsr-only run) instead of uploading an empty key file and
       rebooting the device.
    5. Use `printf '%s\n'`, not `echo`, for every generated script line --
       dash's echo interprets backslash escapes, so the remote script's
       content would otherwise depend on which /bin/sh happens to run
       acme.sh.
    6. Save DEPLOY_JETKVM_SSH_CMD and DEPLOY_JETKVM_RESTART_CMD with
       _savedeployconf's "base64" flag (as deploy/docker.sh does for its
       own reload command), since a value containing a single quote would
       otherwise break the saved domain.conf line.
    7. Distinguish "config file missing/unreadable" from "HTTPS Mode isn't
       Custom" with separate exit codes -- grep's own exit 2 for a missing
       file was previously funneled into the same "not custom" error,
       misdiagnosing the actual problem. Also stopped suggesting
       REQUIRE_CUSTOM_MODE=no in that error message: following it silently
       turns every future deploy into a no-op once persisted to domain.conf.
    8. Hardcode the remote path, filenames, chmod values and config file
       path as constants instead of DEPLOY_JETKVM_* variables. They're
       firmware facts on a single-purpose, single-root appliance, not user
       configuration -- and since _savedeployconf pins whatever value is
       first used into domain.conf, a firmware-side correction to one of
       these later would never reach anyone who'd already deployed once.
       Only USER/HOST/PORT/SSH_CMD/RESTART_CMD/REQUIRE_CUSTOM_MODE remain.
    9. Run the restart command detached (nohup sh -c 'sleep N; $CMD' &) so
       the ssh call returns as soon as it's launched, before the device
       actually reboots, instead of racing the connection teardown. This
       also removes the marker/case-based "0 or 255" exit-code logic
       entirely, along with the bug it had: a connection dropping after the
       marker printed but before the restart command actually ran was
       previously reported as a successful deploy. The tradeoff (also
       called out inline and in the PR description): a restart command that
       fails after being launched can no longer be detected, only a failure
       to launch it at all.
    10. Use fixed temp filenames for the staged cert/key (not one new name
        per run) plus a `trap ... EXIT` in the generated script, so any
        abort (the mode check, a write failure under `set -e`) cleans up
        instead of leaving another stray key-bearing file on the device.
    11. Trimmed the header: removed the marker/0|255 rationale (obsoleted by
        #9), corrected the "typically overnight" claim about when the
        restart actually runs, and added a wiki reference.
    
    Not yet done: a deployhooks wiki entry (acmesh-official/acme.sh#7254's
    point 12) -- flagged in the PR thread since only a repo collaborator can
    edit that wiki.
    
    Verified: shellcheck (no exclusions needed anymore) and shfmt -i 2
    clean; a local smoke-test harness (stubbed acme.sh core, a fake ssh
    that redirects the hardcoded device paths into a scratch directory)
    covering a clean deploy with byte-exact content/permissions and no
    leftover staged files, RESTART_CMD=none, HTTPS Mode not "custom",
    the config file missing entirely (now a distinct error),
    REQUIRE_CUSTOM_MODE=no, an empty key file (--signcsr case), and a
    failed restart-command launch.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01M7rTpUF3btoBXSZ95Psjh7
    
    * Fix restart-command quoting and correct the failure-detection comment
    
    Per @neilpang's second review round:
    
    1. DEPLOY_JETKVM_RESTART_CMD was interpolated unescaped inside the
       detached command's own single-quoted "sh -c '...'" wrapper. A value
       containing a single quote (e.g. "sh -c 'sync; reboot'") broke that
       quoting, splitting the string so only part of the intended command
       ran, un-detached. Escape embedded single quotes (the standard
       '\'' substitution) before nesting the value, matching how a value
       with no quotes at all still behaves identically. Verified against
       sh and dash directly, and with a new local smoke-test case that
       actually executes the generated detached command and confirms both
       halves of a quoted restart command run intact.
    
    2. The comment claiming "only a failure to launch it at all is caught
       below" was wrong: since the restart command runs as an unwaited
       background job (nohup ... &), the remote sh returns 0 as soon as
       that job is launched, regardless of whether nohup, sh, or the
       restart command itself actually exist or succeed -- measured 0 in
       both cases. Reworded so the comment describes what's actually
       caught (an outright SSH connection failure) instead of implying a
       guarantee the code doesn't provide. No behavior change from this
       half of the fix, comment-only.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01M7rTpUF3btoBXSZ95Psjh7
    
    ---------
    
    Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
  • deploy/ssh: create the backup directory only when there is a file to back up
    Follow-up to 9249c892 (#7249). The mkdir -p ran unconditionally before
    the guarded cp calls, so a first deploy left an empty backup directory
    behind. Move the mkdir into each guarded block.
  • Honor an HTTP-date Retry-After when polling an order
    HARICA answers a processing order with "Retry-After: <HTTP-date>" set
    about two minutes after finalize (discussion 7190). Skipping the date
    left the poll loop on its 2s fallback, and 30 rounds ran out 21 seconds
    before the time the CA had named, so the issue failed although the
    certificate was about to be signed.
    
    Add _httpdate2time, which converts the IMF-fixdate form in shell
    arithmetic: GNU, BSD and busybox date each want a different invocation
    for it, and %a/%b are locale lookups. _retryafter_seconds prints
    delay-seconds as before and turns a date into the seconds left until
    then; a date in the past or an unparseable value still prints nothing.
  • ci: run Pebble with PEBBLE_AUTHZREUSE=0 in PebbleStrict
    Pebble reuses a valid authorization in a new order 50% of the time by
    default, so the dns manual mode case was a coin flip: a reused
    authorization left nothing for the TXT record to answer.
  • Ignore an HTTP-date Retry-After when polling an order
    Pebble answers a processing order with "Retry-After: <HTTP-date>". The
    poll loop cut the value at the first colon and fed "Fri,11Sep202604" to
    "[ -gt 0 ]", which errored with "integer expression expected" on every
    round. Add _retryafter_seconds, which prints the header only in its
    delay-seconds form, and use it at all four Retry-After sites. The two
    sites that already filtered on digits used "[0-9]\+", which Solaris grep
    does not support.
  • Truenas 26 deploy fixes (#7205)
    * Works with TrueNAS 26.0.0-BETA3 now
    
    * Updated to work with new and old versions of TrueNAS
    
    * shfmt fix for my changes
    
    ---------
    
    Co-authored-by: Bill Weiss <github@e.billweiss.net>
  • Both confirmed and fixed in dev.
    1. The four backup cp calls (KEYFILE/CERTFILE/CAFILE/FULLCHAIN) ran
       unguarded. With USE_SCP=yes MULTI_CALL is implicit, so each cp is its
       own ssh call and a missing source aborted the deploy. In batched mode
       it was masked because the exit code is that of the last command.
       Each cp is now wrapped in a remote [ -f ] test.
    2. deploy/ssh.sh tested DEPLOY_SSH_FULLCHAIN = "yes" instead of
       DEPLOY_SSH_MULTI_CALL (since 2017). Effect was only that the
       fullchain backup got deferred to the next batch. Fixed as well.
    
    Please upgrade with acme.sh --upgrade -b dev and retest.
  • Add Opteamax DNS API (#7244)
    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) (#7243)
    * 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>
  • ci: enable the dns manual mode case in PebbleStrict
    TEST_DNS_MANUAL=1 turns on le_test_dns_manual_renew, which answers the
    dns-01 challenge through the pebble-challtestsrv that the compose setup
    already runs.
  • Poll the order this run created, not the one the previous cert came from (#7237)
    A renewal in dns manual mode writes the previous certificate again. The
    second invocation resumes from the domain conf, which carries
    Le_LinkOrder and Le_LinkCert from the last successful issuance. newOrder
    saves only Le_OrderFinalize, so after finalizing the new order the
    `[ -z "$Le_LinkOrder" ]` guard keeps the stale link, the poll reads the
    old order, and its certificate URL is the old certificate.
    
    The same gap breaks a first issuance in dns manual mode outright: there
    is no stale link to fall back on, and a finalize that answers while the
    order is still processing carries no Location header, so the run dies
    with "could not get order link location header".
    
    Save the order link where the order is created, next to Le_OrderFinalize,
    and drop the certificate link that belongs to the order just replaced.
    
    Fixes #7105
  • TrueNAS deploy script witch websocat instead of Python midclt internal command (#7216)
    * Add files via upload
    
    TrueNAS deploy script for SCALE/CORE using websocket (websocat binary)
    It is recommend to use a wildcard certificate
    
    Tested with TrueNAS SCALE 25.10 (API "wss://host/api/current", JSON-RPC 2.0).
    Unlike "truenas_ws" hook, this script does NOT use midclt, the truenas_api_client Python package.
    It only depends on:
    - jq
    - websocat  (a static binary you deploy)
    
    Why: avoids installing a Python environment / TrueNAS package on OPNsense just to push a certificate.
    
    IMPORTANT: This script is written in pure POSIX sh (no coproc, no bash arrays).
    
    * Update truenas_websocat.sh
    
    Mistake on port and procotol.
    
    * Update truenas_websocat.sh
    
    Adjustment on the "Why"
    
    * Add files via upload
    
    * Update truenas_websocat.sh
    
    * Update truenas_websocat.sh
    
    Apply shellcheck disable=SC2016 to avoid false positive.
    
    * Update truenas_websocat.sh
  • Update netcup DNS API to support new API (#7214)
    * 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.
  • fix
  • sync (#7234)
    * Merge pull request #7220 from moezx/dev
    
    Add AK & SK based Huawei Cloud DNS API
    
    * fix: dynv6 record parsing (#7197)
    
    * unifios: extract JSON-split helper, document RSA/ECC name collision (#7200)
    
    * Extract _uos_split_json helper, document RSA/ECC name-prefix collision
    
    Per neilpang's non-blocking review notes on #7184: the _normalizeJson +
    split-into-lines block was duplicated at both call sites, now shared via
    _uos_split_json(). Also documents (without changing behavior, since it's
    harmless today) that an RSA and ECC deploy of the same domain share the
    generated name's prefix, each removing the other's entry on cleanup --
    citing haproxy.sh/lighttpd.sh's existing .rsa/.ecdsa suffix pattern as
    the fix if this ever needs addressing.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * Replace grep -F with a portable matcher, fix RSA/ECC name collision
    
    grep -F isn't on Solaris, and dropping it naively breaks matching:
    wildcard domains and dots collide as regex. _uos_grep_literal replaces
    both call sites with a case-based literal match instead.
    
    _uos_name now includes the key type, so RSA and ECC deploys of the
    same domain no longer share a cleanup scope.
    
    Per neilpang's review on #7200.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * Fix echo's \n handling in _uos_grep_literal, drop unneeded Le_Keylength guard
    
    echo does not behave consistently across different environments. dash
    interprets literal \n in a line, splitting it.  printf '%s\n' does not and matches
    _uos_split_json's existing pattern. printf behaves more consistently across
    environments and is generally preferred over echo.
    
    Le_Keylength guard was a no-op and didn't help under set -u either;
    _isEccKey already handles empty. Kept the shellcheck warning suppressed
    inline instead of assigning to a core Le_* var.
    
    Per neilpang's review on #7200.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
    
    * add trigger
    
    * minor
    
    ---------
    
    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>
  • unifios: extract JSON-split helper, document RSA/ECC name collision (#7200)
    * Extract _uos_split_json helper, document RSA/ECC name-prefix collision
    
    Per neilpang's non-blocking review notes on #7184: the _normalizeJson +
    split-into-lines block was duplicated at both call sites, now shared via
    _uos_split_json(). Also documents (without changing behavior, since it's
    harmless today) that an RSA and ECC deploy of the same domain share the
    generated name's prefix, each removing the other's entry on cleanup --
    citing haproxy.sh/lighttpd.sh's existing .rsa/.ecdsa suffix pattern as
    the fix if this ever needs addressing.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * Replace grep -F with a portable matcher, fix RSA/ECC name collision
    
    grep -F isn't on Solaris, and dropping it naively breaks matching:
    wildcard domains and dots collide as regex. _uos_grep_literal replaces
    both call sites with a case-based literal match instead.
    
    _uos_name now includes the key type, so RSA and ECC deploys of the
    same domain no longer share a cleanup scope.
    
    Per neilpang's review on #7200.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * Fix echo's \n handling in _uos_grep_literal, drop unneeded Le_Keylength guard
    
    echo does not behave consistently across different environments. dash
    interprets literal \n in a line, splitting it.  printf '%s\n' does not and matches
    _uos_split_json's existing pattern. printf behaves more consistently across
    environments and is generally preferred over echo.
    
    Le_Keylength guard was a no-op and didn't help under set -u either;
    _isEccKey already handles empty. Kept the shellcheck warning suppressed
    inline instead of assigning to a core Le_* var.
    
    Per neilpang's review on #7200.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
  • Merge pull request #7220 from moezx/dev
    Add AK & SK based Huawei Cloud DNS API
  • Let an explicit --days or --valid-to outrank the ARI window
    ARI has overridden Le_NextRenewTime unconditionally since 3.1.4, so a user
    who passed --days never got the schedule they asked for, and --valid-to was
    guarded at issue time but not on the renewal check: the guard survived one
    run before the next cron rewrote it and saved it back.
    
    An explicit --days or --valid-to now pins the schedule. The window is still
    taken when it is earlier than what the user asked for, so a CA can pull an
    urgent renewal forward but can never push a pinned renewal back.
    Le_RenewalDays is only written to the domain conf when --days was actually
    passed, so its presence there is what marks a schedule as pinned.
    
    A fixed-date --valid-to opts out of ARI entirely: that cert is not renewed
    automatically at all, so pulling it forward would change what it does, not
    just when it renews.
    
    Both call sites go through the new _calc_ari_renew_time.
  • Never truncate the conf file when a saved value breaks the rewrite sed
    A value holding a backslash-digit sequence (a backreference to sed) or an
    embedded line break made _setopt's replace command fail after the shell
    had already truncated the conf file, wiping the whole domain conf; the
    next renewal then fails with an empty Le_API and no validation method.
    Same class as #2426, which escaped only '&' and '|'.
    
    Escape the backslash too, write the sed output back only when sed
    succeeds, reject values holding a line break, and rewrite the file with
    printf instead of echo in the append path and in _clear_conf: dash's
    builtin echo interprets backslash escapes and corrupted such values on
    every rewrite.
    
    https://github.com/acmesh-official/acme.sh/issues/7213
  • dns_azure: never read or persist AZUREDNS_BEARERTOKEN from account.conf
    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: match the TXT record by its rdata when removing (#7199)
    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>
  • Merge pull request #5194 from flesniak/myloc
    Add dnsapi script for myloc.de/webtropia.com
  • deploy/unifios: document UniFi OS hardware support, not just self-hosted
    The certificate REST API this hook drives is UniFi OS's own, not
    specific to the self-hosted UniFi OS Server: user reports confirm it on
    a UDM Pro (UniFi OS 5.1.26) and a UCG Fiber (5.0.16). Reframe the scope
    around the endpoint rather than the product line, state that the choice
    between unifi and unifios is local/SSH file access vs remote REST API,
    and note that the management port is 11443 on UniFi OS Server but 443
    on hardware, so DEPLOY_UNIFIOS_HOST must be set there.