diff --git a/security/acme-client/Makefile b/security/acme-client/Makefile
index d3a8c100b..eab794244 100644
--- a/security/acme-client/Makefile
+++ b/security/acme-client/Makefile
@@ -1,5 +1,5 @@
PLUGIN_NAME= acme-client
-PLUGIN_VERSION= 3.4
+PLUGIN_VERSION= 3.5
PLUGIN_COMMENT= ACME Client
PLUGIN_MAINTAINER= opnsense@moov.de
PLUGIN_DEPENDS= acme.sh py${PLUGIN_PYTHON}-dns-lexicon
diff --git a/security/acme-client/pkg-descr b/security/acme-client/pkg-descr
index c0fbc4497..07084fd48 100644
--- a/security/acme-client/pkg-descr
+++ b/security/acme-client/pkg-descr
@@ -8,6 +8,18 @@ WWW: https://github.com/acmesh-official/acme.sh
Plugin Changelog
================
+3.5
+
+Added:
+* new automation: cert upload to Synology DSM (#2236)
+* new automation: cert upload to FRITZ!Box router
+
+Fixed:
+* fix logging when clog is disabled (#2555)
+
+Changed:
+* refactor code to support acme.sh deploy hooks
+
3.4
Changed:
diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml
index db41ea648..15dbabc9f 100644
--- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml
+++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogAction.xml
@@ -26,7 +26,7 @@
header
-
+
action.highwinds_account_hash
@@ -43,7 +43,7 @@
header
-
+
action.sftp_host
@@ -145,13 +145,79 @@
header
-
+
- action.configd
+ action.configd_generic_command
dropdown
Select a pre-defined system command which should be run.
-
+
+
+
+ header
+
+
+
+ action.acme_synology_dsm_hostname
+
+ text
+ Hostname of IP adress of the Synology DSM, i.e. synology.example.com or 192.168.0.1.
+
+
+ action.acme_synology_dsm_port
+
+ text
+ Port that will be used when connecting to Synology DSM.
+
+
+ action.acme_synology_dsm_scheme
+
+ dropdown
+ Connection scheme that will be used when uploading certificates to Synology DSM.
+
+
+ action.acme_synology_dsm_username
+
+ text
+ Username to login, must be an administrator.
+
+
+ action.acme_synology_dsm_password
+
+ password
+
+
+ action.acme_synology_dsm_deviceid
+
+ text
+ If Synology DSM has OTP enabled, then the device ID has to be provided so that no OTP is required when running the automation.
+
+
+ action.acme_synology_dsm_create
+
+ checkbox
+ This option ensures that a new certificate is created in Synology DSM if it does not exist yet. If unchecked only existing certificates will be updated.
+
+
+
+ header
+
+
+
+ action.acme_fritzbox_url
+
+ text
+ URL of the router, i.e. https://fritzbox.example.com.
+
+
+ action.acme_fritzbox_username
+
+ text
+
+
+ action.acme_fritzbox_password
+
+ password
diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeFritzbox.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeFritzbox.php
new file mode 100644
index 000000000..ed60880aa
--- /dev/null
+++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeFritzbox.php
@@ -0,0 +1,47 @@
+acme_env['DEPLOY_FRITZBOX_URL'] = (string)$this->config->acme_fritzbox_url;
+ $this->acme_env['DEPLOY_FRITZBOX_USERNAME'] = (string)$this->config->acme_fritzbox_username;
+ $this->acme_env['DEPLOY_FRITZBOX_PASSWORD'] = (string)$this->config->acme_fritzbox_password;
+ $this->acme_args[] = '--deploy-hook fritzbox';
+ return true;
+ }
+}
diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeSynologyDsm.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeSynologyDsm.php
new file mode 100644
index 000000000..c1fb58a0a
--- /dev/null
+++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/AcmeSynologyDsm.php
@@ -0,0 +1,56 @@
+acme_env['SYNO_Certificate'] = 'OPNsense ACME cert ' . $this->cert_id;
+ $this->acme_env['SYNO_Hostname'] = (string)$this->config->acme_synology_dsm_hostname;
+ $this->acme_env['SYNO_Port'] = (string)$this->config->acme_synology_dsm_port;
+ $this->acme_env['SYNO_Scheme'] = (string)$this->config->acme_synology_dsm_scheme;
+ $this->acme_env['SYNO_Username'] = (string)$this->config->acme_synology_dsm_username;
+ $this->acme_env['SYNO_Password'] = (string)$this->config->acme_synology_dsm_password;
+ if (!empty((string)$this->config->acme_synology_dsm_create)) {
+ $this->acme_env['SYNO_Create'] = (string)$this->config->acme_synology_dsm_create;
+ }
+ if (!empty((string)$this->config->acme_synology_dsm_deviceid)) {
+ $this->acme_env['SYNO_DID'] = (string)$this->config->acme_synology_dsm_deviceid;
+ }
+ $this->acme_args[] = '--deploy-hook synology_dsm';
+ return true;
+ }
+}
diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php
index 349e22bff..e15ab16f4 100644
--- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php
+++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Base.php
@@ -47,7 +47,7 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon
* Initialize LeAutomation object by adding the required configuration.
* @return boolean
*/
- public function init(string $certid, string $accountuuid)
+ public function init(string $certid, string $certname, string $accountuuid)
{
// Get config object
$this->loadConfig(self::CONFIG_PATH, $this->uuid);
@@ -60,12 +60,25 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon
$this->account_id = (string)$account->id;
$this->account_uuid = (string)$account->uuid;
+ // Teach acme.sh about DNS API hook location
+ $this->acme_env['_SCRIPT_HOME'] = self::ACME_SCRIPT_HOME;
+
// Set log level
$this->setLoglevel();
// Set ACME CA
$this->setCa($accountuuid);
+ // Store acme filenames
+ $this->acme_args[] = LeUtils::execSafe('--home %s', self::ACME_HOME_DIR);
+ $this->acme_args[] = LeUtils::execSafe('--certpath %s', sprintf(self::ACME_CERT_FILE, $this->cert_id));
+ $this->acme_args[] = LeUtils::execSafe('--keypath %s', sprintf(self::ACME_KEY_FILE, $this->cert_id));
+ $this->acme_args[] = LeUtils::execSafe('--capath %s', sprintf(self::ACME_CHAIN_FILE, $this->cert_id));
+ $this->acme_args[] = LeUtils::execSafe('--fullchainpath %s', sprintf(self::ACME_FULLCHAIN_FILE, $this->cert_id));
+
+ // Main domain for acme
+ $this->acme_args[] = LeUtils::execSafe('--domain %s', $certname);
+
return true;
}
@@ -80,7 +93,72 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon
return true; // not an error
}
- LeUtils::log('running automation: ' . $this->config->name);
+ // The prefix determines which automation flavour is being used.
+ if (preg_match('/acme.*/i', $this->getType())) {
+ $this->runAcme();
+ } elseif (preg_match('/configd_.*/i', $this->getType())) {
+ $this->runConfigd();
+ } else {
+ LeUtils::log_error('unsupported automation flavour: ' . $this->getType());
+ return false;
+ }
+ }
+
+ /**
+ * run acme.sh deploy hooks commands
+ * @return boolean
+ */
+ public function runAcme()
+ {
+ LeUtils::log('running automation (acme.sh): ' . $this->config->name);
+
+ // Preparation to run acme client
+ $proc_env = $this->acme_env; // env variables for proc_open()
+ $proc_env['PATH'] = $this::ACME_ENV_PATH;
+ $proc_desc = array( // descriptor array for proc_open()
+ 0 => array("pipe", "r"), // stdin
+ 1 => array("pipe", "w"), // stdout
+ 2 => array("pipe", "w") // stderr
+ );
+ $proc_pipes = array();
+
+ // Run acme client
+ $acmecmd = self::ACME_CMD
+ . ' '
+ . '--deploy '
+ . implode(' ', $this->acme_args);
+ LeUtils::log_debug('running acme.sh command: ' . (string)$acmecmd, $this->debug);
+ $proc = proc_open($acmecmd, $proc_desc, $proc_pipes, null, $proc_env);
+
+ // Make sure the resource could be setup properly
+ if (is_resource($proc)) {
+ // Close all pipes
+ fclose($proc_pipes[0]);
+ fclose($proc_pipes[1]);
+ fclose($proc_pipes[2]);
+ // Get exit code
+ $result = proc_close($proc);
+ } else {
+ LeUtils::log_error('unable to start acme client process');
+ return false;
+ }
+
+ // Check validation result
+ if ($result) {
+ LeUtils::log_error('running acme.sh deploy hook failed (' . $this->getMethod() . ')');
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * run configd commands
+ * @return boolean
+ */
+ public function runConfigd()
+ {
+ LeUtils::log('running automation (configd): ' . $this->config->name);
$backend = new \OPNsense\Core\Backend();
$response = $backend->configdRun((string)$this->command, $this->command_args);
return true;
diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Configd.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdGeneric.php
similarity index 87%
rename from security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Configd.php
rename to security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdGeneric.php
index 810fafab5..c56a1bb89 100644
--- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/Configd.php
+++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdGeneric.php
@@ -1,7 +1,7 @@
config->configd)) {
+ if (empty((string)$this->config->configd_generic_command)) {
LeUtils::log_error('no configd command specified for automation: ' . $this->config->name);
return false;
}
- $this->command = (string)$this->config->configd;
+ $this->command = (string)$this->config->configd_generic_command;
return true;
}
}
diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/RestartGui.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdRestartGui.php
similarity index 93%
rename from security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/RestartGui.php
rename to security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdRestartGui.php
index e63cf479e..18d35b2b5 100644
--- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/RestartGui.php
+++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeAutomation/ConfigdRestartGui.php
@@ -1,7 +1,7 @@
getAutomation($auto_uuid);
- $automation->init($this->getId(), (string)$this->config->account);
+ $automation->init($this->getId(), (string)$this->config->name, (string)$this->config->account);
// Ignore invalid automations.
if ($automation->prepare()) {
$automation->run();
diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCommon.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCommon.php
index 5ea7b5c7d..ce1ed6e71 100644
--- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCommon.php
+++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeCommon.php
@@ -41,11 +41,14 @@ abstract class LeCommon
public const ACME_BASE_ACCOUNT_DIR = '/var/etc/acme-client/accounts';
public const ACME_BASE_CERT_DIR = '/var/etc/acme-client/certs';
public const ACME_BASE_CONFIG_DIR = '/var/etc/acme-client/configs';
+ public const ACME_CMD = '/usr/local/sbin/acme.sh';
public const ACME_HOME_DIR = '/var/etc/acme-client/home';
// Defaults for acme.sh
public const ACME_ACCOUNT_KEY_LENGTH = 4096;
public const ACME_ENV_PATH = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/games:/usr/local/sbin:/usr/local/bin';
+ public const ACME_SCRIPT_HOME = '/usr/local/share/examples/acme.sh';
+ public const ACME_WEBROOT = '/var/etc/acme-client/challenges';
// Filenames for certs, configs, ...
public const ACME_CERT_DIR = '/var/etc/acme-client/certs/%s/';
diff --git a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php
index d50292c66..433572cba 100644
--- a/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php
+++ b/security/acme-client/src/opnsense/mvc/app/library/OPNsense/AcmeClient/LeValidation/Base.php
@@ -68,7 +68,7 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon
$this->account_uuid = (string)$account->getUuid();
// Teach acme.sh about DNS API hook location
- $this->acme_env['_SCRIPT_HOME'] = '/usr/local/share/examples/acme.sh';
+ $this->acme_env['_SCRIPT_HOME'] = self::ACME_SCRIPT_HOME;
// Set log level
$this->setLoglevel();
@@ -83,7 +83,7 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon
$this->acme_args[] = LeUtils::execSafe('--dnssleep %s', (string)$this->config->dns_sleep);
break;
case 'http01':
- $this->acme_args[] = '--webroot /var/etc/acme-client/challenges';
+ $this->acme_args[] = '--webroot ' . self::ACME_WEBROOT;
break;
}
@@ -159,7 +159,8 @@ abstract class Base extends \OPNsense\AcmeClient\LeCommon
// NOTE: We "export" certificates to our own directory, so we don't have to deal
// with domain names in filesystem, but instead can use the ID of our certObj, which
// will never change.
- $acmecmd = '/usr/local/sbin/acme.sh '
+ $acmecmd = self::ACME_CMD
+ . ' '
. "--${acme_action} "
. implode(' ', $this->acme_args) . ' '
. LeUtils::execSafe('--accountconf %s', $account_conf_file);
diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml
index 9b3650d8d..362bb2017 100644
--- a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml
+++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml
@@ -1042,12 +1042,14 @@
Y
- Restart OPNsense Web UI
- Restart HAProxy (OPNsense plugin)
- Restart Nginx (OPNsense plugin)
- Upload certificate to Highwinds CDN
- Upload certificate via SFTP
- System or Plugin Command
+ Restart OPNsense Web UI
+ Restart HAProxy (OPNsense plugin)
+ Restart Nginx (OPNsense plugin)
+ Upload certificate to Highwinds CDN
+ Upload certificate via SFTP
+ Upload certificate to FRITZ!Box router
+ Upload certificate to Synology DSM
+ System or Plugin Command
@@ -1136,6 +1138,7 @@
Should be a string between 1 and 255 characters.
Characters are limited to [a-z], [0-9] and [{}@./-_%] and the string must neither begin nor end with '/'.
+
/^(?!.*(Let\'s\ Encrypt|acme|[fF]irmware))([\S\s]{1,255})/
@@ -1143,6 +1146,62 @@
Select a command from the list.
N
+
+
+ /^(?!.*(Let\'s\ Encrypt|acme|[fF]irmware))([\S\s]{1,255})/
+
+ Select a command from the list.
+ N
+
+
+
+ N
+
+
+ 5000
+ N
+
+
+ http
+ N
+
+ HTTP [default]
+ HTTPS
+
+
+
+ N
+ /^.{1,1024}$/u
+ Should be a string between 1 and 1024 characters.
+
+
+ N
+ /^.{1,1024}$/u
+ Should be a string between 1 and 1024 characters.
+
+
+ 1
+
+
+ N
+ /^.{1,1024}$/u
+ Should be a string between 1 and 1024 characters.
+
+
+ N
+ /^.{1,1024}$/u
+ Should be a string between 1 and 1024 characters.
+
+
+ N
+ /^.{1,1024}$/u
+ Should be a string between 1 and 1024 characters.
+
+
+ N
+ /^.{1,1024}$/u
+ Should be a string between 1 and 1024 characters.
+
diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M3_1_0.php b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M3_1_0.php
new file mode 100644
index 000000000..f8f751cd2
--- /dev/null
+++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Migrations/M3_1_0.php
@@ -0,0 +1,72 @@
+getNodeByReference('actions.action')->iterateItems() as $action) {
+ // Field "configd" was renamed to "configd_generic_command"
+ if (!empty((string)$action->configd)) {
+ $action->configd_generic_command = (string)$action->configd;
+ $action->configd = null; // clear old value
+ }
+
+ // Get old type and map to new value
+ $old_type = (string)$action->type;
+ switch ($old_type) {
+ case 'configd':
+ $new_type = 'configd_generic';
+ break;
+ case 'restart_gui':
+ $new_type = 'configd_restart_gui';
+ break;
+ case 'restart_haproxy':
+ $new_type = 'configd_restart_haproxy';
+ break;
+ case 'restart_nginx':
+ $new_type = 'configd_restart_nginx';
+ break;
+ case 'upload_highwinds':
+ $new_type = 'configd_upload_highwinds';
+ break;
+ case 'upload_sftp':
+ $new_type = 'configd_upload_sftp';
+ break;
+ }
+ $action->type = $new_type;
+ }
+ }
+}
diff --git a/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/+TARGETS b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/+TARGETS
index d00a85339..5190d6c33 100644
--- a/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/+TARGETS
+++ b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/+TARGETS
@@ -1,2 +1,3 @@
lighttpd-acme-challenge.conf:/var/etc/lighttpd-acme-challenge.conf
rc.conf.d:/etc/rc.conf.d/acme_http_challenge
+syslog-filter.conf:/usr/local/opnsense/service/templates/OPNsense/Syslog/local/acmeclient.conf
diff --git a/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/syslog-filter.conf b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/syslog-filter.conf
new file mode 100644
index 000000000..051781f28
--- /dev/null
+++ b/security/acme-client/src/opnsense/service/templates/OPNsense/AcmeClient/syslog-filter.conf
@@ -0,0 +1,7 @@
+###################################################################
+# Local syslog-ng configuration filter definition [acmeclient].
+###################################################################
+filter f_local_acmeclient {
+ program("acme.sh");
+};
+