Skip to content

Commit 3e5d570

Browse files
daemonhornclaude
andcommitted
security/acme-client: add JetKVM automation via acme.sh deploy hook
Adds "Upload certificate to JetKVM (SSH)" (acme_jetkvm), a thin wrapper around the acme.sh "jetkvm" deploy hook (acmesh-official/acme.sh#7254), following the exact pattern already used by AcmeFritzbox/AcmePanos/ AcmeZyxelGs1900/etc.: set a few DEPLOY_JETKVM_* env vars from the automation's config and let LeAutomation\Base::runAcme() invoke `acme.sh --deploy --deploy-hook jetkvm`. This is the "much smaller follow-up PR" requested by the maintainer on opnsense#5621 in place of that PR's full custom PHP implementation (own SSH exec session, own atomic-write logic, its own API controller/configd actions) -- all of that now lives once in the acme.sh hook instead of being duplicated here. Exposes only what a user needs to fill in: host, username (default root), port (default 22), and a "Reboot After Upload" checkbox (default checked) that maps to the hook's DEPLOY_JETKVM_RESTART_CMD ("reboot" or "none") -- narrower than the hook's own free-text restart command, kept consistent with AcmeZyxelGs1900's equivalent checkbox. Everything else (remote path, filenames, permissions, the HTTPS-mode precondition check) uses the hook's own defaults. Unlike the old SFTP/remote-SSH-style automations, this one does not use the plugin's own managed SSH identity/known_hosts store (no "Show Identity" button) -- acme.sh's own deploy hooks assume SSH access is already configured on the host, same as every other Acme* automation in this plugin. The firewall's own root SSH key needs to already be trusted by the JetKVM device. Field names cross-checked between the model, dialog, and PHP class; LeAutomationFactory's type-to-classname derivation confirmed to resolve "acme_jetkvm" to AcmeJetkvm; xmllint --noout clean on both changed XML files; php -l clean on the new class. BooleanField comparison for "Reboot After Upload" uses loose == 1, matching the house idiom already used by AcmeZyxelGs1900/AcmeVault for the same pattern. acme_jetkvm_host is Required N in the model, matching every other Acme*_host/url field in this plugin (acme_fritzbox_url, acme_panos_host, acme_zyxel_gs1900_host, etc. are all Required N too) -- so this doesn't deviate from house style. But unlike those, a blank JetKVM host isn't harmless: jetkvm.sh falls back to the certificate's own domain name as the SSH target, which will almost never be the device. prepare() now follows ConfigdGeneric's own precedent (check the field, log_error, and return false to stop the automation) rather than adding a new pattern. Validated end-to-end on a real OPNsense 26.7 box against a real JetKVM device: locally-built test packages (os-acme-client + acme.sh, each adding only the JetKVM-related file(s) on top of the currently published packages -- see pkg-test/ in the delivery repo) were installed so this automation could invoke a real deploy/jetkvm.sh. Confirmed working end to end: issuance -> this automation -> the acme.sh hook -> upload -> HTTPS-mode check -> reboot -> certificate served by the device. That run predates the empty-host check added above, so the check itself has only been exercised with php -l, not against real hardware. Not yet testable against a *stock* OPNsense install, since that still requires acmesh-official/acme.sh#7254 to merge and the FreeBSD acme.sh port to pick it up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M7rTpUF3btoBXSZ95Psjh7
1 parent acb1d44 commit 3e5d570

5 files changed

Lines changed: 121 additions & 1 deletion

File tree

‎security/acme-client/Makefile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
PLUGIN_NAME= acme-client
2-
PLUGIN_VERSION= 4.17
2+
PLUGIN_VERSION= 4.18
33
PLUGIN_COMMENT= ACME Client
44
PLUGIN_MAINTAINER= opnsense@moov.de
55
PLUGIN_DEPENDS= acme.sh py${PLUGIN_PYTHON}-dns-lexicon

‎security/acme-client/pkg-descr‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ WWW: https://github.com/acmesh-official/acme.sh
88
Plugin Changelog
99
================
1010

11+
4.18
12+
13+
Added:
14+
* new automation to upload certificate to JetKVM via SSH, using the
15+
acme.sh "jetkvm" deploy hook (#XXXX)
16+
1117
4.17
1218

1319
Added:

‎security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml‎

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -515,4 +515,43 @@
515515
<type>checkbox</type>
516516
<help>If checked the switch will be rebooted once the certificate has been successfully installed.</help>
517517
</field>
518+
<field>
519+
<label>Required Parameters</label>
520+
<type>header</type>
521+
<style>method_table method_table_acme_jetkvm</style>
522+
</field>
523+
<field>
524+
<id>action.acme_jetkvm_host</id>
525+
<label>JetKVM Host</label>
526+
<type>text</type>
527+
<help>IP address or hostname of the JetKVM device. Required first: the device must already have "HTTPS
528+
Mode" set to "Custom" in its own web UI (Settings &gt; Network) -- this automation only writes the
529+
certificate/key files (and optionally reboots); it does not switch HTTPS mode for you, and the
530+
deploy will fail if it isn't already set (check the ACME Client log for the exact reason). Also
531+
requires SSH access to already work non-interactively as the configured user (JetKVM only supports
532+
key-based SSH authentication -- enable "Developer Mode" on the device and paste this firewall's SSH
533+
public key, e.g. from /root/.ssh/id_rsa.pub, into its "SSH Keys" field under Settings &gt; Advanced).</help>
534+
</field>
535+
<field>
536+
<id>action.acme_jetkvm_user</id>
537+
<label>Username</label>
538+
<type>text</type>
539+
<help>The username to login to the JetKVM device via SSH. JetKVM only supports the "root" account for SSH
540+
access. Defaults to "root".</help>
541+
</field>
542+
<field>
543+
<id>action.acme_jetkvm_port</id>
544+
<label>SSH Port</label>
545+
<type>text</type>
546+
<help>SSH server port on the JetKVM device. Defaults to "22".</help>
547+
</field>
548+
<field>
549+
<id>action.acme_jetkvm_reboot</id>
550+
<label>Reboot After Upload</label>
551+
<type>checkbox</type>
552+
<help>JetKVM does not hot-reload a new "Custom" certificate -- a full device reboot is required to apply
553+
it, which briefly drops any active KVM-over-IP session. If checked (default), the device is
554+
rebooted automatically right after the certificate is uploaded, so an unattended renewal actually
555+
takes effect. Uncheck to upload only and apply/verify manually instead.</help>
556+
</field>
518557
</form>
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
/*
4+
* Copyright (C) 2026 daemonhorn
5+
* All rights reserved.
6+
*
7+
* Redistribution and use in source and binary forms, with or without
8+
* modification, are permitted provided that the following conditions are met:
9+
*
10+
* 1. Redistributions of source code must retain the above copyright notice,
11+
* this list of conditions and the following disclaimer.
12+
*
13+
* 2. Redistributions in binary form must reproduce the above copyright
14+
* notice, this list of conditions and the following disclaimer in the
15+
* documentation and/or other materials provided with the distribution.
16+
*
17+
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
18+
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
19+
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
20+
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
21+
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22+
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23+
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24+
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25+
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26+
* POSSIBILITY OF SUCH DAMAGE.
27+
*/
28+
29+
namespace OPNsense\AcmeClient\LeAutomation;
30+
31+
use OPNsense\AcmeClient\LeAutomationInterface;
32+
use OPNsense\AcmeClient\LeUtils;
33+
34+
/**
35+
* Run acme.sh deploy hook jetkvm
36+
* @package OPNsense\AcmeClient
37+
*/
38+
class AcmeJetkvm extends Base implements LeAutomationInterface
39+
{
40+
public function prepare()
41+
{
42+
// Make sure a host was specified. Without one, the deploy hook
43+
// would silently fall back to the certificate's own domain name
44+
// as the SSH target instead of the JetKVM device.
45+
if (empty((string)$this->config->acme_jetkvm_host)) {
46+
LeUtils::log_error('no host specified for automation: ' . $this->config->name);
47+
return false;
48+
}
49+
50+
$this->acme_env['DEPLOY_JETKVM_HOST'] = (string)$this->config->acme_jetkvm_host;
51+
$this->acme_env['DEPLOY_JETKVM_USER'] = (string)$this->config->acme_jetkvm_user;
52+
$this->acme_env['DEPLOY_JETKVM_PORT'] = (string)$this->config->acme_jetkvm_port;
53+
$this->acme_env['DEPLOY_JETKVM_RESTART_CMD'] = ((string)$this->config->acme_jetkvm_reboot == 1) ? 'reboot' : 'none';
54+
$this->acme_args[] = '--deploy-hook jetkvm';
55+
return true;
56+
}
57+
}

‎security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1449,6 +1449,7 @@
14491449
<acme_truenas_ws>Deprecated - Upload certificate to TrueNAS Server (midctl/websocket)</acme_truenas_ws>
14501450
<acme_zyxel_gs1900>Upload certificate to Zyxel GS1900 series switches</acme_zyxel_gs1900>
14511451
<acme_unifi>Update local Unifi keystore</acme_unifi>
1452+
<acme_jetkvm>Upload certificate to JetKVM (SSH)</acme_jetkvm>
14521453
<configd_generic>System or Plugin Command</configd_generic>
14531454
</OptionValues>
14541455
</type>
@@ -1842,6 +1843,23 @@
18421843
<Default>0</Default>
18431844
<Required>N</Required>
18441845
</acme_zyxel_gs1900_reboot>
1846+
<acme_jetkvm_host type="HostnameField">
1847+
<Required>N</Required>
1848+
<Mask>/^.{1,1024}$/u</Mask>
1849+
<ValidationMessage>Should be a string between 1 and 1024 characters.</ValidationMessage>
1850+
</acme_jetkvm_host>
1851+
<acme_jetkvm_user type="TextField">
1852+
<Default>root</Default>
1853+
<Required>N</Required>
1854+
</acme_jetkvm_user>
1855+
<acme_jetkvm_port type="TextField">
1856+
<Default>22</Default>
1857+
<Required>N</Required>
1858+
</acme_jetkvm_port>
1859+
<acme_jetkvm_reboot type="BooleanField">
1860+
<Default>1</Default>
1861+
<Required>N</Required>
1862+
</acme_jetkvm_reboot>
18451863
</action>
18461864
</actions>
18471865
</items>

0 commit comments

Comments
 (0)