Commit Graph
6897 Commits
Author SHA1 Message Date
neil f5f2242d60 deploy/ssh: stop the batched remote command at the first failed step
In single-call mode (the default) all steps were joined with ";", so a
failed write did not stop the batch: the remaining files were still
overwritten, DEPLOY_SSH_REMOTE_CMD still ran on the broken file, and the
deploy reported success because only the last exit code counted.

With a POSIX remote shell, steps are now wrapped in { } and joined with
&&, matching multi-call mode. Non-POSIX remote shells (powershell -c,
cmd /c) keep the old ";" join byte for byte.

See #7328
2026-10-14 14:13:21 +08:00
AmadeusAbionandstelar7 d73d68ced4 Add Abion.com DNS API (#7311)
* 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>
2026-10-14 12:39:44 +08:00
Eyal Kalderon 228a140094 Add deploy hook for Redfish-compatible BMCs (#7276)
* Add `deploy/redfish.sh` and implement Redfish session log in/out

Since `acme.sh` intends to keep third-party dependencies to a minimum,
this implementation will not rely on `redfishtool`.

* Detect BMC manager and replace existing cert with ACME cert

* Reuse `_toPkcs8` defined in `acme.sh`

* Return early if unsupported key algorithms or bit lengths are used

* Improve robustness and UX a bit

* Support opt-in automatic restart of BMC

* Improve log output and inline docs some more

* Remove `test-redfish-deploy-hook.sh` test harness

The business logic has been implemented, so it should be safe to delete
this test harness now and just interact with `acme.sh` directly.

* Trim `_redfish_` prefixes from local variables

Since each deploy hook is executed within its own subshell, there's no
risk of polluting the outer `acme.sh` environment nor any other deploy
hooks that might follow.

* Save deploy configuration changes to disk

* Tweak deploy hook log messages a bit

* Add `DEPLOY_REDFISH_MANAGER` environment variable

* Add function for checking HTTP status code

* Add more helper functions, clean up log output a bit more

* Return early from non-zero `curl` exit codes

* Only upload leaf cert in `--sign-csr` mode

* Fix bug with RSA key compatibility error message

* Remove `_redfish_load_deploy_conf` helper, use consistent var naming

* Add several more helper functions

* Check whether server supports Certificate schema >=1.4.0

Redfish servers with a schema version older than this don't support
uploading private keys + full certificate chains directly. Instead, the
CSR must be generated on the server and signed with `acme.sh --sign-csr`
prior to deployment.

* Clarify environment variable naming a bit

* Don't implicitly opt into `HTTPS_INSECURE`

* Feedback: Fix lower-bound RSA key length check

* Feedback: Verify file existence with `-f` flag

* Feedback: Correctly map `TPM_ECC_CURVE` key length values

* Feedback: Replace exact HTTP 204 checks with 2xx instead

* Feedback: Use POSIX-compliant alternative to `\+`

* Feedback: Get session token from body instead of HTTP header

* Feedback: Save PKCS#8 private key directly to variable

* Feedback: Drop `jq` dependency, handle missing action info

* Fix regression in `_redfish_rest()` error reporting

* Simplify construction of expected certificate JSON string

Confirmed with a real-world test that the value of `_certificate_str`
is unchanged before and after applying this commit. See below:

diff --git a/deploy/redfish.sh b/deploy/redfish.sh
index e485fd60..3459b71a 100644
--- a/deploy/redfish.sh
+++ b/deploy/redfish.sh
@@ -131,6 +131,10 @@ redfish_deploy() {
       printf '%s\n' "${_ckey_pkcs8}"
       cat "${_cfullchain}"
     } | _json_encode)"
+
+    _new_certificate_str="$(echo "${_ckey_pkcs8}" | cat - "${_cfullchain}" | _json_encode)"
+    [ "${_certificate_str}" = "${_new_certificate_str}" ] || return 123
+
     _certificate_type='PEMchain'
   else
     _info "Private key and full certificate chain not available; we must be using a server-generated CSR."

* Include link to `redfish-generate-csr.sh` helper script in error message

* Feedback: Drop use of `paste` and `expr`

* Feedback: Remove stale `_err` invocation

* Feedback: Correctly deal with whitespace after colon

* Feedback: Handle `\n` in username and password in Dash shell

It seems that `echo` in Dash automatically converts `\n` into a line
break, causing usernames and/or passwords containing a literal
backslash-n to reach the BMC with a newline, breaking login. It
seems that `printf '%s'` doesn't have the same issue, so we use those to
format `_username` and `_password` instead of `echo`.

* Feedback: Link to acme.sh wiki instead of Gist

* Feedback: Fix `Certificate` schema version check

* Feedback: Better handle missing key algo/length/curve info

This commit changes how the deploy hook detects which cryptographic
algorithms are supported by the server if not explicitly declared in
`KeyPairAlgorithm`. Instead of blindly assuming RSA, we instead check
for the presence of `KeyBitLength` and/or `KeyCurveId` fields and try to
make an educated guess. If not enough info exists to determine key
compatibility, we fail-open instead of failing closed; the server will
yell at us anyway if `ReplaceCertificate` doesn't support something.

* Feedback: Fix character escaping of username/password

* Feedback: Correctly parse inline `KeyBitLength@Redfish.AllowableValues`

This metadatum is represented as a JSON array of allowed values, not a
numeric range with min/max. Make sure to treat the element in the list
as the maximum, and not assume the list is at most 2 elements long.

* Feedback: Add "\n" to format string for Solaris compatibility

* Feedback: Use POSIX parameter expansion instead of `rev`
2026-10-14 12:38:33 +08:00
Kevin Winter f5aeed1752 dns_cf: support IDN domains (#7322)
* dns_cf: support IDN domains (punycode record lookup, URL-encoded zone lookup)

* dns_cf: convert IDN once at the top of dns_cf_add/dns_cf_rm
2026-10-14 12:37:08 +08:00
Lee 81fd2d2126 Add Option to Control HTTP Scheme (#7318)
* Add Option to Control HTTP Scheme

Once certificates are installed renewal should always use HTTPS.
HTTP is only required if there have never been any certificates on
the device.

This also fixes an authentication issue where HTTPS is forced
by the device. Before this patch authentication failed for
devices which forced HTTPS because the POST request used
for request did not follow the HTTP to HTTPS redirect.

* Use http as Default Scheme

Using https as default for SHELLY_SCHEME breaks existing deployments
as they may not have a SHELLY_HOST which is identical to a name in
the certificate.

SHELLY_SCHEME is now a parameter as any other without the automatic
upgrade to https. This will always need to be done manually.
2026-10-13 09:43:28 +08:00
Tom BeardandTom Beard 772ed2aa93 Add Public Internet DNS API (dns_pblin) (#7316)
Co-authored-by: Tom Beard <12370027+tombeard@users.noreply.github.com>
2026-10-12 17:37:00 +08:00
Marvo2011andneil ee38f28e13 Update SelfHost DNS API (#7307)
* 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>
2026-10-12 17:33:00 +08:00
Matteo Gaggiano 9d09edcd69 [dnsapi] add MVMNET dns api (#6606)
* 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
2026-10-12 17:30:59 +08:00
neil cf51ad22d1 dns_baidu, dns_bhosted: pipe input to _lower_case, not an argument
_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
2026-10-12 17:02:10 +08:00
neil 3e6d4835e5 Retry signed requests on transport errors
_send_signed_request tested $? after _CACHED_NONCE="" had reset it, so a
POST that never got an answer (curl 7, 28, 35) returned success with a
stale or empty code, and callers blamed the CA ("Finalize code was not
200"). Keep the _post status and retry with backoff and a new nonce, like
a gateway error. Drop the nonce-fetch $? check, which was dead the same
way.

Fixes #7320
2026-10-12 16:58:32 +08:00
neil 0e4d75325b dns_yc: accept compact JSON responses (#7300)
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.
2026-10-09 17:40:42 +02:00
fhaala 04c3e64ea7 Add optional SYNO_USE_TOKEN switch for Synology SRM compatibility (#7290)
* Add optional SYNO_USE_TOKEN switch for Synology SRM compatibility

* Fix Synology SRM token switch review comments

* Fix SYNO_USE_TOKEN handling
2026-10-09 17:28:32 +02:00
invario 2f8a5535dc Add deploy hook for nginx proxy manager and NPM plus (#7267)
* Add deploy hook for nginx proxy manager and npmplus

Signed-off-by: invario <67800603+invario@users.noreply.github.com>

* rename to nginx-proxy-manager
use _post instead of raw curl
support both npmplus and nginx API
create certificate if it doesn't already exist
json_encode values
evaluate and report server return code

Signed-off-by: invario <67800603+invario@users.noreply.github.com>

* - removed jq dependency, replaced with _egrep_o
- fix _secure_debug output for _npm_token
- remove newline from _npm_certname_json
- remove unused variables
- escape backslashes in passwords
- unset _H1 before returning
- tested on nginx proxy manager (image jc21/nginx-proxy-manager:latest)
- tested on NPMplus (image zoeyvid/npmplus:2026-07-24-r1)

Signed-off-by: invario <67800603+invario@users.noreply.github.com>

* add "requiresTotp" check for 2FA enablement (npmplus:develop branch)

Signed-off-by: invario <67800603+invario@users.noreply.github.com>

* remove api/schema detection and infer API based on token location

Signed-off-by: invario <67800603+invario@users.noreply.github.com>

* replaced `[[:space:]]`
evaluate `requires_2fa` and `requires_Totp` separately
unset `_H1` on additional return for failure
prefix `nl` `_boundary` `_payload`
add `_npm_payload_format` to strip backslashes, encode for JSON, and remove newlines

Signed-off-by: invario <67800603+invario@users.noreply.github.com>

* remove backslash handling from _npm_payload_format
use _utc_date instead of date

Signed-off-by: invario <67800603+invario@users.noreply.github.com>

---------

Signed-off-by: invario <67800603+invario@users.noreply.github.com>
2026-10-09 17:20:39 +02:00
b2e6173b1a fix: poweradmin dnsapi leaves hanging txt records (#7232)
* 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>
2026-10-03 07:48:58 +02:00
neil acbe85b7e9 Support wget2 in the wget code path
Fedora 40 and later ship wget2 as wget. Under -q it prints nothing for -S,
so $HTTP_HEADER stayed empty in wget mode (ACME_USE_WGET=1, or no curl):
no status code, no Replay-Nonce, no Location, and every ACME request
failed. Without -q its own "HTTP response 200 OK [url]" line would be read
as the status line, and a HEAD response gets no headers at all.

Detect wget2 by its --version line and run it without -q (and without -d,
whose demultiplexing expects wget 1.x output). _wget2_headers keeps only
the header blocks of its stderr; for a response without a body, where
wget2 prints no block, it rebuilds the status line from the "HTTP
response" line. HEAD goes through --method HEAD --save-headers, which
writes the headers into $HTTP_HEADER. wget 1.x and curl are unchanged.
2026-09-27 19:32:38 +02:00
Tao AnandClaude Opus 5.5 6467ca9764 fix: keep the conf intact when writing it fails (#7247) (#7278)
* fix: keep the conf intact when writing it fails

_setopt() and _clear_conf() redirected the new content straight into the
conf file. The redirect truncates the file before anything is written,
so a failed write (no space left on device, quota, I/O error) left a
0-byte conf, _save_conf still returned 0, and the cert could not be
renewed any more even after space was freed.

Write the new content to a temp file next to the conf, verify it, then
rename it over the conf. The temp file is created with cp -p so the mode
and owner are kept. A symlinked or bind mounted conf cannot be renamed
over, so it is written in place once the temp copy is known to be good.
A failed write now returns 1 with an error and the conf untouched.

Fixes #7247

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RL9G1DHE5yNVtkm3FFPPNX

* fix: route the append through _write_conf and keep the temp file on a failed in-place write

The append branch of _setopt() still wrote into the conf with >>, so a
write cut short (full disk, quota) left a half written line while
_setopt returned 0, and sourcing the conf failed afterwards. Build the
new content in all three branches and write it once through _write_conf.
This also drops the separate trailing newline append.

In the symlink / bind mount fallback, a failed in-place write leaves the
conf truncated. Keep the temp file in that case, since it is the only
complete copy, and name it in the error.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G7ohY7h4RUSNJ2ES14NMmY

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-27 10:02:42 +02:00
ngehrsitz a1de626512 Use UTF-16LE encoding in Fritzbox script (#7288)
instead of the glibc specifc UTF16LE alias that does not exist libiconv
2026-09-27 09:53:49 +02:00
Marc-Pascal ClementandClaude Opus 5.5 84a8d2a752 Add Healthchecks.io notify hook (#7282)
* Add Healthchecks.io notify hook

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* notify/healthchecks: require an exact "OK" response

hc-ping.com answers HTTP 200 "OK (not found)" for an unknown UUID, so the
prefix match reported success for a mistyped or deleted check, including
the --set-notify test ping. Only treat an exact "OK" body as success.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 09:51:04 +02:00
neil 6ef16ffe63 Save CA_EMAIL only when the CA took the contact
_regAccount wrote CA_EMAIL before sending newAccount, so it recorded an
address the CA never stored: for an account key it already knows, the CA
answers 200 and ignores the contact of the request. A --register-account
-m on an existing account then left the local config claiming an email
the account does not have.

Save it in the 201 branch, and in the 200/409 branch say that the email
was not changed and point at --update-account -m. Nothing is printed
when the given address already matches the saved one.
2026-09-26 12:48:52 +02:00
neil 427787cb8f Guard the account email in __initHome too
_process() calls __initHome directly once the option loop is over, and
that sourcing of account.conf overwrote the address -m had just
exported -- before any command function reaches _initpath. Guarding
only _initpath was not enough: by then ACCOUNT_EMAIL already held the
saved address, so it was saved and restored unchanged.

Keep the live value across this sourcing as well.
2026-09-26 12:38:43 +02:00
neil 31f06a4ecf Keep the command line account email out of account.conf's way
_initpath sources account.conf (twice, counting __initHome), and the
assignment overwrote the ACCOUNT_EMAIL that -m had just exported, so
--update-account -m sent the saved address instead of the given one.
It also short-circuited _getAccountEmail, whose first branch is meant
for the live value: the saved global address then won over the per-CA
CA_EMAIL as well.

Save the live value before the sourcing and restore it after. The saved
address is still reached, by the _readaccountconf at the end of
_getAccountEmail, which is where it belongs in the order.
2026-09-26 12:14:17 +02:00
neil 6618d55bfc Decide the ARI replaces retry on the HTTP status, not on the CA's wording
The retry was keyed on the message text and missed ZeroSSL, which answers
HTTP 401 with 'The "replaces" field does not identify a certificate that
belongs to this ACME account', so switching the ACME server failed every
renewal.

RFC 9773 Section 5 mandates a problem type only for the 409
"alreadyReplaced" case and leaves its other checks (same account, shared
identifier) to server policy, so the wording cannot be enumerated. Judge
the status code instead: _isARIReplacesRejected treats only a non-2xx
response as a rejection. That also removes a false positive on the
success path, where the server "MUST reflect that field in the response"
and the echoed base64url certID can itself contain "ARI".

fix #7280
2026-09-26 11:50:22 +02:00
neil 879c1519bb Merge pull request #7263 from kekropp/dns_bergdns
Add bergdns.at DNS API
2026-09-26 11:41:07 +02:00
Kenny Kropp d51d853ad4 fix default value handling 2026-09-21 11:53:09 +02:00
neil ceed0e0228 start 3.1.7 2026-09-20 16:56:43 +02:00
neil aafc4efe1f 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
2026-09-20 13:13:20 +02:00
neil 138e0dff84 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
2026-09-20 13:06:37 +02:00
neil ccb376dde3 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.
2026-09-19 19:20:45 +02:00
neil 8f56e8fe04 add _post_file 2026-09-19 16:40:40 +02:00
48966ba481 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>
2026-09-19 15:45:02 +02:00
c58d25eb90 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>
2026-09-19 15:44:32 +02:00
daemonhornandClaude Sonnet 5 a5e683546f 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>
2026-09-19 15:43:23 +02:00
Kenny Kropp 84d6b5841e fix pr comment issues 2026-09-19 11:29:08 +02:00
Kenny Kropp 0ee2720d73 add bergdns.at provider info block values 2026-09-16 08:30:13 +02:00
Kenny Kropp e4c0a4f5f5 add bergdns.at support 2026-09-15 14:56:59 +02:00
neil 8e5ba60bee 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.
2026-09-13 07:50:39 +02:00
neil 666bf75589 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.
2026-09-11 14:56:58 +08:00
neil efecf2a308 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.
2026-09-11 14:23:38 +08:00
neil cbd68ddc02 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.
2026-09-11 12:42:47 +08:00
Bill WeissandBill Weiss 98276668fd 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>
2026-09-11 11:55:38 +08:00
neil 9249c89221 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.
2026-09-11 11:51:07 +08:00
Jens Ott 2074493c7c 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.
2026-09-11 11:49:47 +08:00
Eduardo LangnerandClaude Opus 5 cb0ef1c30c 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>
2026-09-11 11:48:31 +08:00
neil 15bed65ce1 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.
2026-09-10 21:56:59 +08:00
José M. Requena Plens 0617ae6696 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
2026-09-10 21:56:24 +08:00
Mashiro 75afdcc1fb Fix dns_hw: prevent record-set lookup from hanging (#7236) 2026-09-10 21:41:00 +08:00
Tok-Ra85 0fa785be2a 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
2026-09-10 21:39:13 +08:00
jduss 162fbde328 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.
2026-09-10 21:37:03 +08:00
neil 63ee7cb16e ci: tolerate 422 when the latest release tag has no workflow_dispatch 2026-09-07 12:33:21 +08:00
neil 94d2f62626 fix 2026-09-04 15:51:12 +08:00