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 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>
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.
* 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>
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 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
* 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>
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.
* 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>
_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.
_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
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
* feat: add Shelly Gen3+ deploy hook with RFC 7616 HTTP Digest auth
Adds deploy/shelly.sh for deploying Let's Encrypt HTTPS server certificates
to Shelly Gen3+ devices (Gen4 tested) via JSON-RPC over HTTP.
- RFC 7616 SHA-256 HTTP Digest authentication (Authorization header)
- Uploads fullchain.pem and private key via Shelly.PutHTTPServerCert / PutHTTPServerKey
- Auto-reboot support (SHELLY_REBOOT to disable)
- Auth auto-detection: no password = no auth, password = Digest
- Nonce counter (nc) increments per request per RFC 7616
- Tested against Shelly 2PM Gen4 (firmware 2.0.0)
Also adds deploy/test_shelly.sh for self-testing the hook logic without
a real device (mocked _post).
* fix: address review feedback on shelly deploy hook
- Fix _secure_debug calls to use two arguments (label + value)
- Remove bash-only $RANDOM cnonce fallback; openssl always available
- Parse $HTTP_HEADER directly instead of raw curl re-request
- Detect auth via HTTP 401 status line, not empty response body
- Route reboot through _shelly_rpc to rebuild auth header with correct nc
- Remove export HTTPS_INSECURE=1 (no-op for http://, leaks to other hooks)
- Clear _H1 before returning from shelly_deploy
- Prefix all helper variables with _shelly_ to avoid namespace collisions
- Delete deploy/test_shelly.sh (deploy/ files become hook names)
- Fix missing trailing newline
* fix: validate shelly JSON-RPC responses are valid JSON
Non-JSON responses like HTTP 429 'Too Many Requests' would pass
the empty-response and '"error"' checks and be reported as success.
Now reject any response that doesn't start with '{' and contain '"id"'.
* fix: add 1s delay between shelly cert/key clear and upload calls
The Shelly device has a race condition where uploading data immediately
after clearing the existing cert/key returns -103 'Missing required
argument data!'. A 1-second delay fixes this.
* fix: remove clear-before-upload in shelly deploy hook
Shelly auto-removes all three TLS files (cert, key, CA bundle) when any
single one is cleared. The old sequence clear-cert → upload-cert →
clear-key → upload-key resulted in the key clear wiping the newly
uploaded cert, leaving only the key at boot time. The mbedtls
pk_check_pair then silently skipped the HTTPS listener.
Fix: just upload directly (overwrite in place). No clearing needed.
* Fix ShellCheck SC2090 and shfmt in shelly deploy hook
SC2090: false positive on export _H1 (used quoted in _post)
shfmt: no space after "<" in _json_encode redirects
* moved two lines to cover the whole if block
---------
Co-authored-by: neil <github@neilpang.com>
Co-authored-by: cysimons <cysimons@cisco.com>
The success check only grepped "message" from the response body, but
PVE/PBS auth failures return HTTP 401 with an empty body, so wrong or
unauthorized API tokens were reported as "Certificate successfully
deployed". Also _retval captured the exit code of the message pipeline
instead of _post. Check the HTTP status line from $HTTP_HEADER and
capture _post's exit code directly.
fix https://github.com/acmesh-official/acme.sh/issues/7141
dnsapi/deploy: remove POSIX character classes from sed/grep patterns
Solaris /usr/bin/sed and /usr/bin/grep parse [[:space:]] etc. as a
literal bracket set and silently mis-match. Replace with [ ]* for
JSON matching, a printf-tab bracket for user-input trimming, and
[0-9] for digits; also drop GNU-only sed -r/-E in rage4, selfhost
and selectel, and reuse _strip_blank_lines in byteplus_alb.
* Deploy certificate to FortiGate firewall using API
* Refactor FortiGate deployment functions
Prefix private functions and working variables and use a timestamped certificate name.
* Replace grep -o for POSIX compatibility
* add Baidu Cloud CDN deploy hook
Code generated by GitHub Copilot with Claude Sonnet 4.6. Tested with local environment by human.
* inline functions
Code generated by OpenAI Codex with GPT-5.5 Sol. Tested with local environment by human.
The hook is sourced by acme.sh, so the bash shebang never takes
effect: under dash, `[ x == y ]` fails with "unexpected operator",
the 403 branch never triggers and 2FA-OTP login is skipped.
Replace `==` with `=` and use the standard sh shebang.
With --signcsr the private key never exists in the cert home, so every
renewal printed ".../domain.key: No such file or directory" from the
shell redirection. Skip the key read in that case; the install_ssl call
already ran with an empty key there and cPanel keeps the installed one.
https://github.com/acmesh-official/acme.sh/issues/6228
grep -o '[0-9]*' can match the empty string; GNU grep skips empty
matches but BSD greps handle them differently, breaking the 2FA
login flow on OpenBSD. Force a non-empty match at all three sites.
from https://github.com/acmesh-official/acme.sh/pull/6725
Mirrors _clearaccountconf_mutable: clears the SAVED_ prefixed key and
the legacy unprefixed key. Replaces the local copy in synology_dsm.sh
and the direct _cleardomainconf call in multideploy.sh.
Closes#4722. Thanks to @sg1888.
ssh_deploy() ignored the result of _ssh_deploy and always returned
success, so a failed transfer to one (or all) of the servers in
DEPLOY_SSH_SERVER was silently swallowed. Track the return code across
the loop and return non-zero if any server failed, letting the caller
handle notification.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Committing after a failed import leaves a mismatched cert/key pair on
the firewall (PAN-OS does not validate the pair at commit time), which
can lock the admin out of the https management interface.
The docker deploy hook copied the key file preserving the source mode
(root:root 0600), so a non-root container service (uid >= 1000) could not
read it. Add DEPLOY_DOCKER_CONTAINER_KEY_MODE and
DEPLOY_DOCKER_CONTAINER_KEY_OWNER, applied via chmod/chown inside the
container after the key is copied and before the reload command.
Closes#5333
dash's echo has no -e flag and sends a literal "-e " prefix to the
socket, so haproxy rejects the command and the hot update always fails
on Debian/Ubuntu (/bin/sh = dash). Also accept "Transaction updated",
which haproxy replies when an uncommitted transaction already exists.
fix https://github.com/acmesh-official/acme.sh/issues/6165
- Removed scope exclusion for "standard commit".
- If 'device-and-networks' is excluded (previous behaviour), a certificate for Panorama (always outside of a template) will not be committed (imported to the config but never applied to Panorama). Therefore, panos.sh was only working for certificates used in templates and applied to devices, but not for the Panorama certificate itself.
- According to the official documentation and the XML API Browser, there is no 'policy-and-objects' that can be excluded.
- Although it is not mandatory that the user account is solely dedicated to replace certificates and to perform no other type of operations, it is recommended. If such recommendation is applied, the only changes being committed would be in relation to certificates. Therefore, it should be safe not to exclude any scopes.
- Changed the order for "force commit" from '<commit><partial><force>' (unofficial) to '<commit><force><partial>' (official). Both work, but it is recommended to use what is part of the official documentation and/or XML API Browser.
- Removed unofficial 'policy-and-objects' from commented out code (see above).
- Replaced 'exclude' with 'excluded' from commented out code, as per the official documentation. Both work, but see above.
- Replaced 'acmekeytest' with $_panos_user in the commented out code.
Official documentation: https://docs.paloaltonetworks.com/ngfw/api/pan-os-xml-api-request-types-and-actions/commit
XML API Browser: https://<PANOS HOST>/api