diff --git a/security/acme-client/pkg-descr b/security/acme-client/pkg-descr index c0fbc4497..0777c8d51 100644 --- a/security/acme-client/pkg-descr +++ b/security/acme-client/pkg-descr @@ -8,6 +8,14 @@ WWW: https://github.com/acmesh-official/acme.sh Plugin Changelog ================ +3.5 + +Added: +* add support for Synology DSM deploy hook (#2236) + +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..2c875b786 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,58 @@ 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. 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..c514f31c4 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,13 @@ 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 Synology DSM + System or Plugin Command @@ -1136,6 +1137,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 +1145,47 @@ 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. + 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; + } + } +}