security/acme-client: add automation to upload certificate to JetKVM via SSH - #5621
Open
daemonhorn wants to merge 3 commits into
Open
security/acme-client: add automation to upload certificate to JetKVM via SSH#5621daemonhorn wants to merge 3 commits into
daemonhorn wants to merge 3 commits into
Conversation
…via SSH Adds a new "Run Command" automation type, "Upload certificate to JetKVM (SSH)" (configd_upload_jetkvm), that deploys a certificate's fullchain and private key to a JetKVM KVM-over-IP device over SSH, plus an optional post-upload command (e.g. to restart/reload a service). The automation reuses the plugin's existing SSH key management (OPNsense\AcmeClient\SSHKeys) and shares its identity/known_hosts store with the existing "Upload certificate via SFTP" and "Remote Command via SSH" automations, matching JetKVM's SSH access model (key-based auth only, enabled via Developer Mode in its web UI). Files are written via a plain SSH exec session (no scp/sftp-server dependency on the device side), since JetKVM's minimal userspace is not guaranteed to include either. Partially validated against a real JetKVM device over SSH (read-only reconnaissance plus writes to throwaway filenames only; the device's real certificate files were deliberately never touched or overwritten): - Corrected the deployed filenames: JetKVM's "Custom" TLS mode reads "user-defined.crt" / "user-defined.key" from /userdata/jetkvm/tls, not "fullchain.pem" / "privkey.pem" as originally guessed. The storage directory itself was confirmed correct. - The cert and key are now staged under temporary filenames in the same directory, chmod'ed, and only "mv"-ed into their final names (an atomic rename) once both are fully written, rather than truncating the live files in place via "cat >". This prevents a dropped SSH connection or a failed write from leaving the device with a truncated or mismatched cert/key pair for its own HTTPS listener. The full staged-write-then-rename sequence was validated end-to-end against the device using throwaway filenames. - Confirmed JetKVM has no hot-reload for a "Custom" certificate: its own certificate-apply script performs a full device reboot. The post-upload command field's help text now says this explicitly; the field itself is still left blank by default since a reboot briefly drops any active KVM-over-IP session. NOT confirmed by this testing, since the device's real certificate files were left untouched: that a certificate written to these paths is actually served after a reboot, and whether JetKVM's "Custom" TLS mode needs to be selected once via the web UI before it will pick up files dropped at this path (plausible given the existing user-defined.crt/.key on the test device were several months stale next to a more recently refreshed default cert pair, suggesting "Custom" mode was not the active mode there). A real end-to-end test (deploy, reboot, verify the served certificate in a browser) is still needed before merging. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017prPdJCJSQvs1DVJqaGVGe
Replaces the (#XXXX) placeholder now that GitHub assigned opnsense#5621 to this PR, per the plugin's changelog convention. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
daemonhorn
added a commit
to daemonhorn/acmeclient-jetkvm-ssh-upload
that referenced
this pull request
Aug 11, 2026
Adds MANUAL_DEPLOY.md and a deploy/ tree containing the full, final content of the 7 application files changed by this PR (2 new, 5 modified), laid out under deploy/usr/local/opnsense/... to mirror exactly where they install on a live OPNsense system (per Mk/plugins.mk's install: target: a plugin's src/opnsense/ tree maps straight to /usr/local/opnsense/). This lets the JetKVM automation be tested on real OPNsense hardware by copying these files over an existing 4.16 os-acme-client install and restarting configd/webgui, without waiting for PR opnsense/plugins#5621 to merge and ship as an actual package update. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…om mode prerequisite Two fixes driven by real-hardware production testing (the automation running unattended as part of cron-driven ACME renewal, typically overnight): - Default the post-upload command to "reboot" instead of blank. Leaving it blank by default meant a renewed certificate never actually got applied without a human manually rebooting the device afterward, defeating the point of automating it. JetKVM devices are rebooted overnight by cron-driven renewals anyway, when an active KVM-over-IP session is unlikely, so defaulting to "reboot" is the better tradeoff for this automation's actual use case. The field can still be cleared to apply/verify manually instead. - Document, in the "JetKVM Host" field's help text, that the device must already have "HTTPS Mode" set to "Custom" in its own web UI before this automation is attached. This automation only writes the cert/key files (and optionally reboots); it does not switch the device's HTTPS mode. This was previously flagged as an open question in this PR's history; real-hardware testing has now confirmed it's required. The same real-hardware test also confirms the other open question from this PR's history: after uploading to user-defined.crt/.key and rebooting, the JetKVM device does serve the new certificate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
daemonhorn
added a commit
to daemonhorn/acmeclient-jetkvm-ssh-upload
that referenced
this pull request
Aug 12, 2026
…ix PR compliance Real-world production testing (running the automation unattended, via cron-driven ACME renewal) surfaced two things: - The post-upload command needs to default to "reboot", not blank - otherwise a renewed certificate never actually gets applied without a human manually rebooting the device afterward, defeating the point of automating it. - JetKVM's "HTTPS Mode" must already be set to "Custom" in the device's own web UI before this automation's uploads take effect; this automation doesn't switch modes for you. This was previously an open question in the PR; it's now a documented, confirmed requirement (help text, deploy notes, and PR_DESCRIPTION.md). The same testing also confirmed the other previously-open question: after uploading and rebooting, the JetKVM device does serve the new certificate. Also fixed a compliance gap in the PR itself, found while checking opnsense/plugins' CONTRIBUTING.md and PR template: the PR body was missing the required AI-tools disclosure (model + extent of involvement) and the contributing-guidelines/issue-first checklist their template expects. Added that to the top of PR_DESCRIPTION.md, matching their template's format. Switched the delivery from a single squashed patch to a 3-file patch series (0001/0002/0003) plus a corresponding bundle, matching the 3 commits now actually pushed to the open PR (the original change, the changelog-number fix, and this reboot-default/documentation fix), rather than rewriting already-pushed history to keep squashing to one commit. Regenerated the bundle from a full (unfiltered) clone again and re-verified 0 missing objects. Updated deploy/ and APPLY_INSTRUCTIONS.md/MANUAL_DEPLOY.md to match throughout. This has already been pushed to the open PR: opnsense/plugins#5621 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Important notices
If AI was used, please disclose:
agentic coding sessions (Claude Code); an earlier pass also used Claude
Opus 5 as an independent advisory reviewer of the code and documentation.
description, and the commit messages were produced by Claude operating
largely autonomously under a human maintainer's direction and review
across multiple sessions. This included direct SSH access (granted by the
maintainer) to a real JetKVM device, and separately to a live OPNsense
26.7.1_1 test system, to validate assumptions that public documentation
alone couldn't settle — the exact remote storage path/filenames, the
reboot-to-apply requirement, and the "HTTPS Mode: Custom" prerequisite —
and to debug a deployment issue after manual testing surfaced it. No
GitHub credentials were available in the sandbox the code was originally
drafted in, so the branch/commits were prepared locally and later
pushed/opened as this PR, and subsequently revised, by the maintainer's
AI assistant with their authorization.
Related issue: #5622 (opened retroactively, since this PR was already
in progress before the issue-first guideline was checked against).
Summary
Adds a new automation ("Run Command" type) to the OPNsense ACME Client
plugin: "Upload certificate to JetKVM (SSH)"
(
configd_upload_jetkvm).It deploys the fullchain certificate and private key for an ACME-issued
certificate to a JetKVM KVM-over-IP device over SSH,
with an optional post-upload command (e.g. to restart/reload a service on
the device). It reuses the plugin's existing SSH key management
(
OPNsense\AcmeClient\SSHKeys) and shares its identity/known_hostsstorewith the existing "Upload certificate via SFTP" and "Remote Command
via SSH" automations — the same SSH keypair generated/managed by the
plugin can be reused across all three.
Why this approach
JetKVM only supports key-based SSH authentication (
root@<device>,password logins are disabled), enabled by turning on "Developer Mode" and
pasting a public key into its web UI (Settings > Advanced). This lines up
naturally with how the ACME Client plugin already manages SSH identities
for its SFTP/SSH automations, so this change is additive: it adds one new
automation type alongside the existing ones rather than introducing new
SSH infrastructure.
JetKVM does not currently ship an
scpbinary or SFTP server in itsminimal userspace (unconfirmed either way from public sources), so instead
of reusing the plugin's
SftpClient/SftpUploader(SFTP-based) uploadpath, this automation opens a plain SSH exec session and pipes a small
POSIX shell script to the remote
shvia stdin. That script writes thecertificate and key via
cat > file <<'MARKER'heredocs (using random,per-run markers) and applies the configured file permissions, followed by
an optional operator-supplied restart command. This only depends on a
POSIX shell and
cat/chmod/mkdirbeing present, which is a safeassumption for busybox-class embedded Linux.
Hardware validation (2026-08-11)
JetKVM's TLS certificate handling is not part of its stable/documented
API, so the original draft of this PR was based on public research only
(JetKVM's GitHub repo/discussions/issues, see below) and flagged that as
the biggest open question before merging. That has since been tested
directly against a real JetKVM device over SSH:
/userdata/jetkvm/tlsis the correct storage directory for"Custom" TLS mode — confirmed.
corrected. JetKVM's "Custom" TLS mode reads
user-defined.crt/user-defined.keyfrom that directory — notfullchain.pem/privkey.pemas first guessed. (Other filenames present in that samedirectory, e.g.
jetkvm.crt, back JetKVM's other, non-custom TLSmodes and are unrelated to "Custom" mode.) The script's defaults and
the model/dialog help text now use the confirmed names.
script (
update-user-defined.sh, shipped in that same directory) doesa full
sync && rebootafter writing the cert/key. The post-uploadcommand field's help text states this explicitly.
cat > filetarget in place to staging both files under temporary names, chmod'ing
them, and only
mv-ing them into their final names (an atomic rename)once both are fully written — so a dropped connection or a failed
write can no longer leave the device with a truncated or mismatched
cert/key pair for its own HTTPS listener. This exact write sequence
(staging, chmod, atomic rename, cleanup) was validated end-to-end
against the device using throwaway filenames.
A follow-up round of testing on production hardware (running this
automation for real, unattended, as part of cron-driven ACME renewal)
closed out both items the first round of hardware validation had left
open:
user-defined.crt/user-defined.keyand then applied via reboot is served by thedevice's HTTPS listener afterward — verified in a browser against the
device.
the device's own web UI (Settings > Network) before this automation's
uploads take effect. This automation only writes the cert/key files
and optionally reboots — it does not switch HTTPS mode for you. The
"JetKVM Host" field's help text and the "How to use once merged" steps
below now state this as a required prerequisite rather than a
should-probably-do-this-anyway suggestion.
reboot.Leaving it blank meant a certificate renewed by an unattended cron job
never actually got applied without a human manually rebooting the
device afterward — which defeats the point of automating renewal in
the first place. Since these renewals (and the reboot they trigger)
typically run overnight, when an active KVM-over-IP session is
unlikely, defaulting to
rebootis the better tradeoff for thisautomation's real use case; the field can still be cleared for anyone
who'd rather apply/verify manually.
No further changes to the remote path/filenames are expected to be
needed, though — as the help text still notes — none of this is
documented/stable JetKVM API, so it's worth a spot-check after any
JetKVM firmware upgrade.
Code review notes (2026-08-10, updated 2026-08-11)
A follow-up review against a live clone of
opnsense/pluginsmasterturned up one design point, since fixed, plus confirmation that several
other things some review candidates might flag are actually inherited,
consistent behavior from the sibling
upload_sftp.php/remote_ssh_identity_typeautomations (not new issues introduced bythis change):
buildRemoteScript()used towrite the cert and key with
cat > file <<'MARKER', which truncateseach file in place, under
set -e. If the SSH session dropped betweenthe cert write and the key write, the device would be left with a
mismatched (or truncated) cert/key pair for what is the device's own
HTTPS listener — the SFTP automation doesn't have quite the same
failure shape. Both files are now staged under temporary names,
chmod'ed, and only
mv-ed into their final names once both are fullywritten, validated against a real device (see "Hardware validation"
above).
jetkvm_identity_typeis copied verbatim fromsftp_identity_type/remote_ssh_identity_type— confirmed against upstream, not a newinconsistency.
--no-error/--automation-idnot appearing inupload_jetkvm.php'sown
COMMANDSoption lists is correct: both are handled genericallyby the shared
Utils::runCLIMain()CLI framework used by all of thesescripts, not per-command options.
commandUpload()'s "no automations attached to any matched cert ⇒exit 0" behavior is copied byte-for-byte from
upload_sftp.php; it'sa pre-existing pattern in the plugin, not something new here.
git am 0001-security-acme-client-add-automation-to-upload-certif.patchapplies cleanly against current
opnsense/pluginsmaster.AcmeClient.xml,dialogAction.xml) iswell-formed, checked with
xmllint --nooutagainst the fullpost-patch files (not just the diff).
php -lon all three new/changed PHP files (upload_jetkvm.php,ConfigdUploadJetkvm.php,ActionsController.php) re-verified withan actual
php-cli(via a disposable container, since this reviewenvironment has no
phpbinary installed) after the atomic-write anddefault-filename fixes — no syntax errors.
What's included
AcmeClient.xml): newconfigd_upload_jetkvmoption on theautomation
typefield, plus 11 newjetkvm_*fields (host, port, hostkey, user, identity type, remote path, cert/key filenames, cert/key
chmod, restart command) — mirroring the existing
sftp_*/remote_ssh_*field groups.dialogAction.xml): new form fields shown when thisautomation type is selected, with help text covering the JetKVM
Developer Mode / SSH key setup and the caveat above.
(
LeAutomation/ConfigdUploadJetkvm.php): thinprepare()that invokesthe new configd action, following the same pattern as
ConfigdUploadSftp/ConfigdRemoteSsh.scripts/OPNsense/AcmeClient/upload_jetkvm.php):new standalone CLI script implementing
upload/test-connection/show-identity, modeled onupload_sftp.phpandrun_remote_ssh.php.Includes CLI examples and can be run standalone for testing.
actions_acmeclient.conf):upload-jetkvm,test-jetkvm-connection,show-jetkvm-identity.Api/ActionsController.php):jetkvmGetIdentityAction/
jetkvmTestConnectionAction, wired to the "Show Identity" / "TestConnection" buttons in
actions.volt(same UX as the SFTP/SSHautomations).
Makefile4.16 → 4.17,pkg-descrchangelog entry(issue/PR number placeholder
#XXXXneeds to be filled in once this PRis opened).
Testing done
php -lon every new/changed PHP file, re-verified with a realphp-cliafter the latest fixes (see "Code review notes").xmllint --noout) on the full, post-patchmodel and dialog XML files.
jetkvm_*field referenced in the dialogform and in the backend script's
getOptionsById()exists in the modelwith a matching name (11/11).
typestring (configd_upload_jetkvm) throughLeAutomationFactory's class-name derivation(
str_replace(' ', '', ucwords(str_replace(['-','_'], ' ', $type))))to confirm it resolves to
ConfigdUploadJetkvm, matching the new classfile name.
quoting / injection issues, and a follow-up defensive pass adding
filename sanitization (
basename()) against path traversal in theconfigurable cert/key filename fields, plus a graceful error path
instead of an uncaught assertion when the remote path is empty.
git amverified to apply the patch cleanly against currentopnsense/pluginsmaster.confirmed the remote path, corrected the deployed filenames, confirmed
the reboot-to-apply behavior, and validated the atomic-write sequence
end-to-end using throwaway filenames (never touching the device's real
certificate files). See "Hardware validation" above for details.
testing against a running OPNsense install (this plugin depends on the
private OPNsense core framework —
OPNsense\Core\Config,OPNsense\Trust\Cert, etc. — which isn't available outside a realOPNsense system), i.e. the model/dialog/API-controller wiring and the
actual ACME-issued-certificate-to-device flow haven't been exercised
end-to-end, only the underlying SSH/remote-script mechanism against
the JetKVM side.
How to use once merged
set "HTTPS Mode" to "Custom". This automation only writes the
user-defined.crt/.keyfiles (and optionally reboots) — it does notswitch HTTPS mode for you, and uploads won't take effect until this is
set. Confirmed on production hardware (see "Hardware validation"
above).
set "Run Command" to "Upload certificate to JetKVM (SSH)".
one already configured for another SFTP/SSH automation).
and paste that public key into the SSH key field.
root), click "TestConnection" to verify SSH connectivity and host key trust.
reboot, since JetKVMrequires a full device reboot to pick up a new "Custom" certificate
and this automation is meant to run unattended. This briefly drops any
active KVM-over-IP session — clear the field if you'd rather
apply/verify manually instead.
after issuance/renewal.
Research sources
Them Without GUI
settings
certificates
web_tls.go/ DeepWiki summaries of JetKVM's TLS storage(
/userdata/jetkvm/tls,CertStore, "user-defined" custom cert mode)See the AI-disclosure notice at the top of this description, and
APPLY_INSTRUCTIONS.mdin this delivery for how the branch/commits wereprepared and pushed.