diff --git a/security/acme-client/Makefile b/security/acme-client/Makefile index 8d1ada4d0..972358733 100644 --- a/security/acme-client/Makefile +++ b/security/acme-client/Makefile @@ -1,6 +1,5 @@ PLUGIN_NAME= acme-client -PLUGIN_VERSION= 1.2 -PLUGIN_REVISION= 1 +PLUGIN_VERSION= 1.3 PLUGIN_COMMENT= Let's Encrypt client PLUGIN_MAINTAINER= opnsense@moov.de 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 51254439c..4dcfa42b4 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 @@ -24,19 +24,27 @@ Pre-defined commands for this restart action. - + header + action.configd dropdown Select a pre-defined system command which should be run for this action. + + + + + header + action.custom textbox Specify a custom commands which should be run for this action. + diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml index f9dd582a2..30bceb613 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogValidation.xml @@ -35,9 +35,9 @@ - + header - + validation.http_opn_autodiscovery @@ -61,9 +61,9 @@ Enter IP addresses here. Finish each with TAB. - + header - + validation.http_haproxyInject @@ -79,20 +79,6 @@ true Choose the local HAProxy frontends. They will automatically be configured to redirect acme challenges to the internal acme client. The HAProxy service will automatically be restarted if a certificate was renewed. - header @@ -111,7 +97,7 @@ - + header @@ -122,7 +108,7 @@ - + header @@ -139,7 +125,7 @@ - + header @@ -156,7 +142,7 @@ - + header @@ -173,7 +159,7 @@ - + header @@ -190,7 +176,7 @@ - + header @@ -208,7 +194,7 @@ - + header @@ -226,7 +212,7 @@ - + header @@ -243,7 +229,7 @@ - + header @@ -261,7 +247,7 @@ - + header @@ -273,7 +259,7 @@ - + header @@ -290,7 +276,7 @@ - + header @@ -319,7 +305,7 @@ - + header @@ -342,7 +328,7 @@ - + header @@ -354,7 +340,7 @@ - + header @@ -371,7 +357,7 @@ - + header @@ -388,7 +374,7 @@ - + header @@ -405,7 +391,7 @@ - + header @@ -434,7 +420,7 @@ acme.sh documentation for further information.]]> - + header diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml index 65afd3a4f..8f5debb9d 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/settings.xml @@ -30,4 +30,11 @@ true + + acmeclient.settings.restartTimeout + + text + + true + 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 5edec0877..65e463196 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 @@ -43,6 +43,12 @@ 65535 Y + + 600 + 10 + 86400 + Y + 0 N diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt index fbb8f54c5..2eef0c115 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/actions.volt @@ -48,6 +48,19 @@ POSSIBILITY OF SUCH DAMAGE. } ); + // hook into on-show event for dialog to extend layout. + $('#DialogAction').on('shown.bs.modal', function (e) { + $("#action\\.type").change(function(){ + var service_id = 'table_optional_' + $(this).val(); + $(".table_optional").hide(); + $("."+service_id).show(); + }); + $("#action\\.type").change(function(){ + $(".method_table").hide(); + $(".method_table_"+$(this).val()).show(); + }); + $("#action\\.type").change(); + }) }); diff --git a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt index b192c9477..d42a0f1ce 100644 --- a/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt +++ b/security/acme-client/src/opnsense/mvc/app/views/OPNsense/AcmeClient/validations.volt @@ -51,16 +51,25 @@ POSSIBILITY OF SUCH DAMAGE. // hook into on-show event for dialog to extend layout. $('#DialogValidation').on('shown.bs.modal', function (e) { $("#validation\\.dns_service").change(function(){ - var service_id = 'table_' + $(this).val() ; + var service_id = 'table_' + $(this).val(); $(".table_dns").hide(); if ($("#validation\\.method").val() == 'dns01') { $("."+service_id).show(); } }); + $("#validation\\.http_service").change(function(){ + var service_id = 'table_http_' + $(this).val(); + $(".table_http").hide(); + if ($("#validation\\.method").val() == 'http01') { + $("."+service_id).show(); + } else { + } + }); $("#validation\\.method").change(function(){ $(".method_table").hide(); $(".method_table_"+$(this).val()).show(); $("#validation\\.dns_service").change(); + $("#validation\\.http_service").change(); }); $("#validation\\.method").change(); }) diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php index 3acb96297..c1fbd1767 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php @@ -2,7 +2,7 @@ id; $cert_filename = "/var/etc/acme-client/certs/${cert_id}/cert.pem"; + $cert_chain_filename = "/var/etc/acme-client/certs/${cert_id}/chain.pem"; $cert_fullchain_filename = "/var/etc/acme-client/certs/${cert_id}/fullchain.pem"; $key_filename = "/var/etc/acme-client/keys/${cert_id}/private.key"; // Check if certificate files can be found clearstatcache(); // don't let the cache fool us - foreach (array($cert_filename, $key_filename, $cert_fullchain_filename) as $file) { + foreach (array($cert_filename, $key_filename, $cert_chain_filename, $cert_fullchain_filename) as $file) { if (is_file($file)) { // certificate file found } else { @@ -791,6 +792,63 @@ function import_certificate($certObj, $modelObj) } } + /* + * Step 1: import CA + */ + + // Read contents from CA file + $ca_content = @file_get_contents($cert_chain_filename); + if ($ca_content != false) { + $ca_subject = cert_get_subject($ca_content, false); + $ca_serial = cert_get_serial($ca_content, false); + $ca_cn = local_cert_get_cn($ca_content, false); + $ca_issuer = cert_get_issuer($ca_content, false); + $ca_purpose = cert_get_purpose($ca_content, false); + } else { + log_error("AcmeClient: unable to read CA certificate content from file"); + return(1); + } + + // Prepare CA for import in Cert Manager + $ca = array(); + $ca['crt'] = base64_encode($ca_content); + $ca['refid'] = uniqid(); + $ca_found = false; + + // Check if CA was previously imported + $cacnt = 0; + foreach ($config['ca'] as $cacrt) { + $cacrt_subject = cert_get_subject($cacrt['crt'], true); + $cacrt_issuer = cert_get_issuer($cacrt['crt'], true); + if (($ca_subject == $cacrt_subject) and ($ca_issuer == $cacrt_issuer)) { + // Use old refid instead of generating a new one + $ca['refid'] = (string)$cacrt['refid']; + $ca_found = true; + break; + } + $cacnt++; + } + + // Collect required CA information + $ca_cn = local_cert_get_cn($ca_content, false); + $ca['descr'] = (string)$ca_cn . ' (Let\'s Encrypt)'; + + // Prepare CA for import + local_ca_import($ca, $ca_content); + + // Update existing CA? + if ($ca_found == true) { + $config['ca'][$cacnt] = $ca; + } else { + // Create new CA item + $config['ca'][] = $ca; + log_error("AcmeClient: importing Let's Encrypt CA: ${ca_cn}"); + } + + /* + * Step 2: import certificate + */ + // Read contents from certificate file $cert_content = @file_get_contents($cert_filename); if ($cert_content != false) { @@ -809,6 +867,7 @@ function import_certificate($certObj, $modelObj) $cert = array(); $cert_refid = uniqid(); $cert['refid'] = $cert_refid; + $cert['caref'] = (string)$ca['refid']; $import_log_message = 'Imported'; $cert_found = false; @@ -842,20 +901,13 @@ function import_certificate($certObj, $modelObj) return(1); } - // Read cert fullchain - $cert_fullchain_content = @file_get_contents($cert_fullchain_filename); - if ($cert_fullchain_content == false) { - log_error("AcmeClient: unable to read full certificate chain from file: ${cert_fullchain_filename}"); - return(1); - } - // Collect required cert information $cert_cn = local_cert_get_cn($cert_content, false); $cert['descr'] = (string)$cert_cn . ' (Let\'s Encrypt)'; $cert['refid'] = $cert_refid; // Prepare certificate for import - cert_import($cert, $cert_fullchain_content, $key_content); + cert_import($cert, $cert_content, $key_content); // Update existing certificate? if ($cert_found == true) { @@ -874,6 +926,10 @@ function import_certificate($certObj, $modelObj) $config['cert'][] = $cert; } + /* + * Step 3: update configuration + */ + // Write changes to config // TODO: Legacy code, should be replaced with code from OPNsense framework write_config("${import_log_message} Let's Encrypt SSL certificate: ${cert_cn}"); @@ -901,6 +957,7 @@ function run_restart_actions($certlist, $modelObj) { global $config; $return = 0; + $configObj = Config::getInstance()->object(); // NOTE: Do NOT run any restart action twice, collect duplicates first. $restart_actions = array(); @@ -915,11 +972,11 @@ function run_restart_actions($certlist, $modelObj) continue; } // Extract restart actions - $_actions = explode(',', $certObj->restartActions); - if (empty($_actions)) { + if (empty((string)$certObj->restartActions)) { // No restart actions configured. continue; } + $_actions = explode(',', $certObj->restartActions); // Walk through all linked restart actions. foreach ($_actions as $_action) { // Extract restart action @@ -985,8 +1042,12 @@ function run_restart_actions($certlist, $modelObj) $proc_stderr = ''; $result = ''; // exit code (or '99' in case of timeout) - // TODO: Make the timeout configurable. - $timeout = '600'; + // Timeout for custom restart actions. + if (!empty((string)$configObj->OPNsense->AcmeClient->settings->restartTimeout)) { + $timeout = (string)$configObj->OPNsense->AcmeClient->settings->restartTimeout; + } else { + $timeout = '600'; + } $starttime = time(); $proc_cmd = (string)$action->custom; @@ -1101,6 +1162,48 @@ function local_cert_get_cn($crt, $decode = true) return ""; } +// taken from system_camanager.php +function local_ca_import(& $ca, $str, $key="", $serial=0) { + global $config; + + $ca['crt'] = base64_encode($str); + if (!empty($key)) { + $ca['prv'] = base64_encode($key); + } + if (!empty($serial)) { + $ca['serial'] = $serial; + } + $subject = cert_get_subject($str, false); + $issuer = cert_get_issuer($str, false); + + // Find my issuer unless self-signed + if($issuer <> $subject) { + $issuer_crt =& lookup_ca_by_subject($issuer); + if($issuer_crt) { + $ca['caref'] = $issuer_crt['refid']; + } + } + + /* Correct if child certificate was loaded first */ + if (is_array($config['ca'])) { + foreach ($config['ca'] as & $oca) { + $issuer = cert_get_issuer($oca['crt']); + if($ca['refid']<>$oca['refid'] && $issuer==$subject) { + $oca['caref'] = $ca['refid']; + } + } + } + if (is_array($config['cert'])) { + foreach ($config['cert'] as & $cert) { + $issuer = cert_get_issuer($cert['crt']); + if($issuer==$subject) { + $cert['caref'] = $ca['refid']; + } + } + } + return true; +} + function base64url_encode($str) { return rtrim(strtr(base64_encode($str), '+/', '-_'), '='); diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh index 8215a8262..583e3f9d6 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/setup.sh @@ -5,7 +5,7 @@ ACME_DIRS="/var/etc/acme-client /var/etc/acme-client/certs /var/etc/acme-client/ for directory in ${ACME_DIRS}; do mkdir -p ${directory} chown -R root:wheel ${directory} - chmod -R 755 ${directory} + chmod -R 750 ${directory} done exit 0