Commit Graph
6858 Commits
  • 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
  • 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.
  • Mirror the tag object, not the commit, in vtag.yml
    The v-prefixed mirror was created from github.sha, so for an annotated or
    signed tag it would point at the commit and drop the signature: "git
    verify-tag v3.1.3" fails with "cannot verify a non-tag object of type
    commit" while "git verify-tag 3.1.3" succeeds. Resolve refs/tags/<tag>
    and mirror whatever object it points at instead, which keeps the current
    behaviour for lightweight tags. Also move the workflow expressions into
    env instead of interpolating them into the shell command.
  • Add UniFi OS Server deploy hook (#7184)
    * Add UniFi OS Server deploy hook
    
    Uses UniFi OS Server's local REST API (login, list, upload, activate,
    remove superseded) since it stores certificates in its own Postgres
    database rather than flat config files, unlike the Cloud Key/UDM
    hardware covered by the existing unifi deploy hook. Tested against
    real instances on both macOS and Ubuntu 26.04 (self-hosted, remote).
    
    * Address review: portable sed/grep, scoped HTTPS_INSECURE, fingerprint matching
    
    - Replace GNU-only \n in sed replacement with a portable literal newline
      (matches dnsapi/dns_cpanel_uapi.sh, dnsapi/dns_glesys.sh); pipe the
      list response through _normalizeJson first for consistent formatting.
    - Use grep -F for the domain-name match instead of an unescaped BRE --
      a wildcard cert name (*.example.com) broke the regex.
    - Drop \W (undocumented, GNU-only) from the cookie lookup in favor of
      an anchored `^Set-Cookie: *NAME=` match.
    - Scope HTTPS_INSECURE=1 inside the hook (matches deploy/proxmoxve.sh,
      deploy/fritzbox.sh) instead of requiring the caller to export it for
      the whole acme.sh run, which would also disable verification for the
      connection to the ACME CA.
    - On a duplicate-certificate response, match the existing entry by
      fingerprint instead of taking the first name match -- with more than
      one stale entry for a domain, the wrong one could get activated.
    - Check the list endpoint's response code before proceeding.
    - Save username/password with the "base64" flag (matches
      deploy/synology_dsm.sh) since _save_conf wraps values in unescaped
      single quotes.
    
    * Rework certificate handling: unique names per upload, drop cleanup
    
    Testing against a real UniFi OS Server showed the server enforces name
    uniqueness independently of fingerprint uniqueness, and that activation is
    exclusive server-wide regardless of name/domain. A unique name per upload
    avoids the name-collision path entirely (previously only handled as a
    retry-of-identical-content edge case), and removes the need for the
    post-hoc cleanup loop, which risked deleting the wrong entry.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * Shorten generated certificate name to Unix epoch seconds
    
    Real-hardware testing showed the UniFi OS Server certificate list's name
    column is fixed-width and doesn't wrap, so a full human-readable timestamp
    overlaps the Expires column and makes both unreadable. Epoch seconds are
    still short enough to fit while remaining unique.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    * Add scoped cleanup of old certificate entries, use _time helper
    
    Per review: dropping cleanup entirely went further than the original bug
    required, and left old entries (each holding a private key) accumulating
    indefinitely. Since every upload now gets a name unique to its domain and
    run, cleanup can safely target only entries whose name starts with that
    domain -- entries this hook itself created -- excluding the one just
    activated. Also swaps date +%s for the core _time helper, and rewrote the
    design comments to make them clearer and match the current behavior
    instead of the pre-redesign one.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
  • add deploy hook support for ikuai (#6456)
    * add deploy hook support for ikuai
    
    * fix shellcheck warn and shfmt the code
    
    * 1.fix config load 2.use _secre_debug2 to log password 3.use fullchain to deploy 4.fix hardcode id 5.change shebang
    
    * fix miss ; after cookie
    
    * 1.fix shfmt ; 2.fix IKUAI_CERT_ID conf load; 3.correct IKUAI_CERT_ID description
    
    * fix some log msg
    
    * fix shfmt
  • Fix dns_netcup reporting a bogus 4013 instead of the real zone error
    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.
  • Listen on both IPv4 and IPv6 in standalone mode by default
    socat binds a single family unless told which one: up to 1.7.x the
    default IP version for TCP-LISTEN is 4, and 1.8.0 made it "no
    preference", which resolves to whatever getaddrinfo and bindv6only
    happen to give. So an order carrying both an IPv4 and an IPv6
    identifier could never pass both http-01 challenges.
    
    Bind one socket per family instead, with ipv6only on the IPv6 one so
    the two do not collide. IPv4-mapped IPv6 addresses are not a portable
    alternative, OpenBSD does not support them at all. The IPv6 listener
    is best effort, a host without IPv6 still gets the IPv4 one. The
    python fallback does the same. --listen-v4 and --listen-v6 keep
    forcing a single family, and passing both now means both.
    
    Le_Listen_V4 and Le_Listen_V6 were mutually exclusive in the domain
    conf, which silently dropped one of them on renewal, and
    _starttlsserver let -4 win when both were set.
    
    Fixes #7185
  • feat: added nexdns dnsapi (#7170)
    * 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
  • Fix dns_namecheap ignoring IsOurDNS when matching the root zone
    _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
  • Fix multideploy MULTIDEPLOY_FILENAME conf read and allow an absolute path
    _getdeployconf assigns and exports the variable, it does not print the
    value, so wrapping it in a command substitution ran it in a subshell and
    always yielded an empty string. A MULTIDEPLOY_FILENAME saved by an
    earlier run was therefore never restored on renewal and the hook
    silently fell back to multideploy.yml. Call it the same way every other
    deploy hook does.
    
    Also treat a MULTIDEPLOY_FILENAME starting with '/' as an absolute path
    instead of always resolving it under DOMAIN_PATH, so one deploy file can
    live outside the certificate directory and be shared by all domains.
    Names without a leading '/' keep resolving under DOMAIN_PATH as before.
  • Fix synology_dsm logging out after the temp admin is already deleted
    _temp_admin_cleanup ran before _logout, so the logout request carried
    the session id of an account synouser had already removed and DSM kept
    the orphaned entry in Connected Users. Swap the order in both terminal
    branches, and add the missing _logout to the two post-login error paths
    (CRT list failure, certificate not found without SYNO_CREATE).
    
    _logout overwrites the global $response, so the upload-failure branch
    prints its error message before calling it.
    
    Reported by @Bertl75 in #7174
  • Fix empty finalize URL when resuming a saved DNS-manual order
    The decision to resume a pending order is keyed on Le_Vlist, but the
    decision to keep Le_OrderFinalize/Le_LinkOrder was keyed on the webroot
    being exactly "dns". Any other webroot with a saved Le_Vlist skipped
    newOrder and then finalized against an empty URL.
    
    Key both on Le_Vlist, and always clear Le_LinkCert, which is per-run
    state that is never read back from the saved domain conf.
    
    Fixes #7177
  • Fix dns_cyon cleanup failing on FreeBSD
    _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
  • Fix --make-dns-persist-value printing a wildcard TXT record name
    For -d '*.example.com' the printed record name kept the literal '*' label
    (_validation-persist.*.example.com). The CA never queries that name, so
    issuance fails with "No TXT record found for DNS-PERSIST-01 challenge".
    
    Per draft-ietf-acme-dns-persist-01 sec 4 and 10.2 the record is published at
    the base domain's Validation Domain Name; the wildcard scope comes from
    'policy=wildcard' in the record value (sec 5.1), not from a '*' label in the
    record name. Strip the leading "*." in a new _dns_persist_txt_name helper,
    and imply --dns-persist-wildcard for a wildcard -d, since without
    policy=wildcard the printed record can never authorize the wildcard.
    
    Fixes #7168
  • Refactor dns_freemyip.sh for enhanced compatibility (#7166)
    * 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: fix TXT record removal failing with "Unknown key file format" (#7150)
    * 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.
  • fix: grep -A is not portable, breaks ARI on Solaris
    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
  • fix dns_yc: avoid empty-matchable _egrep_o pattern that hangs OmniOS
    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.