From 44e66ce88f2e2a0eb583913fb13487388335ef7c Mon Sep 17 00:00:00 2001 From: Frank Wall Date: Mon, 14 Jan 2019 00:18:03 +0100 Subject: [PATCH 1/3] security/acme-client: support uploading certs to Highwinds CDN --- .../AcmeClient/forms/dialogAction.xml | 17 ++ .../models/OPNsense/AcmeClient/AcmeClient.xml | 11 + .../OPNsense/AcmeClient/certhelper.php | 13 +- .../OPNsense/AcmeClient/upload_highwinds.php | 247 ++++++++++++++++++ .../conf/actions.d/actions_acmeclient.conf | 6 + 5 files changed, 292 insertions(+), 2 deletions(-) create mode 100755 security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_highwinds.php 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 271bf6c24..41cc22877 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 @@ -23,6 +23,23 @@ dropdown Pre-defined commands for this restart action. + + + header + + + + action.highwinds_account_hash + + text + Account hash for Highwinds API. + + + action.highwinds_access_token + + text + Access token for Highwinds API. + header 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 aa73224eb..d3b7c2303 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 @@ -702,9 +702,20 @@ Restart OPNsense Web UI Restart HAProxy (OPNsense plugin) Restart Nginx (OPNsense plugin) + Upload certificate to Highwinds CDN System or Plugin Command (select below) + + 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. + /^(?!.*(Let\'s\ Encrypt|acme|[fF]irmware))([\S\s]{1,255})/ 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 77f0a39e7..937b04b90 100755 --- a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/certhelper.php @@ -1138,7 +1138,10 @@ function run_restart_actions($certlist, $modelObj) continue; } // Store by UUID, automatically eliminates duplicates. - $restart_actions[$_action] = $action; + $_data = array(); + $_data['obj'] = $action; + $_data['cert_id'] = $certObj->id; + $restart_actions[$_action] = $_data; } } } @@ -1147,7 +1150,10 @@ function run_restart_actions($certlist, $modelObj) // Run the collected restart actions. if (!empty($restart_actions) and is_array($restart_actions)) { // Extract cert object - foreach ($restart_actions as $action) { + foreach ($restart_actions as $_action) { + $action = $_action['obj']; + $cert_id = $_action['cert_id']; + $action_id = $action->id; // Run pre-defined or custom command? log_error("AcmeClient: running restart action: " . $action->name); switch ((string)$action->type) { @@ -1160,6 +1166,9 @@ function run_restart_actions($certlist, $modelObj) case 'restart_nginx': $response = $backend->configdRun("nginx restart"); break; + case 'upload_highwinds': + $response = $backend->configdRun("acmeclient upload_highwinds ${cert_id} ${action_id}"); + break; case 'configd': // Make sure a configd command was specified. if (empty((string)$action->configd)) { diff --git a/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_highwinds.php b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_highwinds.php new file mode 100755 index 000000000..03d6e6236 --- /dev/null +++ b/security/acme-client/src/opnsense/scripts/OPNsense/AcmeClient/upload_highwinds.php @@ -0,0 +1,247 @@ +#!/usr/local/bin/php +object(); + if (isset($configObj->OPNsense->AcmeClient->certificates) && $configObj->OPNsense->AcmeClient->certificates->count() > 0) { + foreach ($configObj->OPNsense->AcmeClient->certificates->children() as $certObj) { + $cert_id = (string)$certObj->id; + $cert_name = (string)$certObj->name; + if ($cert_id == $acme_cert_id) { + if ($certObj->enabled == 0) { + log_error("AcmeClient: certificate ${cert_name} is disabled, ignoring upload request"); + return None; + } + if (isset($certObj->certRefId)) { + $data = array(); + $data['name'] = $cert_name; + $data['refid'] = (string)$certObj->certRefId; + return $data; + } else { + log_error("AcmeClient: certificate ${cert_name} could not be found in trust storage, ignoring upload request"); + break; + } + } + } + return None; + } +} + +function export_certificate($cert_refid) { + $configObj = Config::getInstance()->object(); + foreach ($configObj->cert as $cert) { + if ($cert_refid == (string)$cert->refid) { + $cert_content = str_replace("\n\n", "\n", str_replace("\r", "", base64_decode((string)$cert->crt))); + $key_content = str_replace("\n\n", "\n", str_replace("\r", "", base64_decode((string)$cert->prv))); + // check if a CA is linked + if (!empty((string)$cert->caref)) { + $cert = (array)$cert; + $ca = ca_chain($cert); + $ca_content = $ca; + } + $result = array(); + $result['cert'] = $cert_content; + $result['key'] = $key_content; + $result['ca'] = $ca_content; + return $result; + } + } + log_error("AcmeClient: cert with refid ${cert_refid} not found in trust storage"); + return None; +} + +function upload_certificate($cert_name,$cert_refid,$acme_cert_id,$acme_automation_id) { + $modelObj = new OPNsense\AcmeClient\AcmeClient; + $configObj = Config::getInstance()->object(); + if (isset($configObj->OPNsense->AcmeClient->actions) && $configObj->OPNsense->AcmeClient->actions->count() > 0) { + foreach ($configObj->OPNsense->AcmeClient->actions->children() as $automObj) { + $autom_id = (string)$automObj->id; + if ($autom_id == $acme_automation_id) { + if ($automObj->enabled == 0) { + log_error("AcmeClient: ignoring disabled upload job for cert ${cert_name}"); + return None; + } + if (isset($automObj->highwinds_account_hash) && isset($automObj->highwinds_access_token)) { + $hw_account_hash = (string)$automObj->highwinds_account_hash; + $hw_access_token = (string)$automObj->highwinds_access_token; + $cert_data = export_certificate($cert_refid); + if ($cert_data !== None) { + $hw_result = hw_upload_certificate($hw_account_hash,$hw_access_token,$cert_name,$cert_data); + if ($hw_result !== None) { + return true; + } + } + } else { + log_error("AcmeClient: upload job for cert ${cert_name} is incomplete, missing Highwinds configuration"); + return None; + } + } + } + return None; + } +} + +function hw_list_certificates($account_hash,$access_token) +{ + global $HIGHWINDS_API_URL; + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_URL => "${HIGHWINDS_API_URL}/${account_hash}/certificates", + CURLOPT_CUSTOMREQUEST => 'GET', + CURLOPT_RETURNTRANSFER => true, + CURLOPT_MAXREDIRS => 1, + CURLOPT_TIMEOUT => 10, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_HTTPHEADER => array( + "Authorization: Bearer ${access_token}", + "Content-Type: application/json", + "User-Agent: OPNsense Firewall", + "X-Application-Id: OPNsense Firewall" + ) + )); + $response = curl_exec($curl); + $err = curl_error($curl); + $info = curl_getinfo($curl); + curl_close($curl); + $http_code = $info['http_code']; + if ($http_code != 200 || $err) { + log_error("AcmeClient: failed to access Highwinds API, HTTP Code: ${http_code}, error ${err}"); + return None; + } + return json_decode($response); +} + +function hw_get_certificate($account_hash,$access_token,$cert_name) +{ + $certificates = hw_list_certificates($account_hash,$access_token); + if ($certificates !== None) { + foreach ($certificates->list as $cert) { + if ($cert->commonName == $cert_name) { + return $cert; + } + } + } + return None; +} + +function hw_upload_certificate($account_hash,$access_token,$cert_name,$cert_data) +{ + global $HIGHWINDS_API_URL; + // Check current status of certificate at Highwinds + $hw_cert = hw_get_certificate($account_hash,$access_token,$cert_name); + $hw_url = 'certificates'; + if ($hw_cert == None) { + log_error("AcmeClient: cert for ${cert_name} not found in Highwinds API, starting upload..."); + } else { + log_error("AcmeClient: cert for ${cert_name} found in Highwinds API"); + + // Extract certificate details + $cert = openssl_x509_parse($cert_data['cert']); + $cert_sn = (int)$cert['serialNumber']; + $hw_cert_sn = (int)$hw_cert->certificateInformation->serialNumber; + $hw_cert_id = $hw_cert->id; + + // Compare local and remote certificates + if ($cert_sn == $hw_cert_sn) { + log_error("AcmeClient: cert ${cert_name} has same serial in Highwinds API, not updating (${cert_sn})"); + return None; + } + $hw_url = "${hw_url}/${hw_cert_id}"; + } + + // adjust data format for Highwinds API + $cert_post = json_encode(array('certificate' => $cert_data['cert'], 'key' => $cert_data['key'], 'caBundle' => $cert_data['ca'])); + + $curl = curl_init(); + curl_setopt_array($curl, array( + CURLOPT_URL => "${HIGHWINDS_API_URL}/${account_hash}/${hw_url}", + CURLOPT_CUSTOMREQUEST => 'POST', + CURLOPT_POSTFIELDS => (string)$cert_post, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_MAXREDIRS => 1, + CURLOPT_TIMEOUT => 10, + CURLOPT_SAFE_UPLOAD => true, + CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1, + CURLOPT_HTTPHEADER => array( + "Authorization: Bearer ${access_token}", + "Content-Type: application/json", + "User-Agent: OPNsense Firewall", + "X-Application-Id: OPNsense Firewall", + "Expect:" + ) + )); + $response = curl_exec($curl); + $err = curl_error($curl); + $info = curl_getinfo($curl); + curl_close($curl); + $http_code = $info['http_code']; + if ($http_code != 200 || $err) { + log_error("AcmeClient: Failed to upload cert ${cert_name} to Highwinds API, HTTP Code: ${http_code}, error ${err}"); + return None; + } + return json_decode($response); +} + +// Evaluate CLI arguments +$options = getopt("a:c:"); +if (!isset($options["a"]) or !isset($options["c"])) { + print "ERROR: not enough arguments\n"; + exit(1); +} +$acme_cert_id = $options["c"]; +$acme_automation_id = $options["a"]; + +// Search certificate in configuration +$cert_data = find_certificate($acme_cert_id); +if ($cert_data == None) { + log_error("AcmeClient: ignoring cert ID ${acme_cert_id}"); + exit(1); +} else { + // Upload certificate (if required) + $upload_result = upload_certificate($cert_data['name'], $cert_data['refid'], $acme_cert_id, $acme_automation_id); + if ($upload_result === None) { + log_error("AcmeClient: cert ID ${acme_cert_id} was neither uploaded nor updated"); + } else { + log_error("AcmeClient: cert ID ${acme_cert_id} was uploaded or updated"); + } +} +exit(0); diff --git a/security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf b/security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf index 3bf17b4b0..b44a19be7 100644 --- a/security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf +++ b/security/acme-client/src/opnsense/service/conf/actions.d/actions_acmeclient.conf @@ -65,3 +65,9 @@ parameters: type:script message:cronjob running to sign or renew certificates description:Renew Let's Encrypt certificates + +[upload_highwinds] +command:/usr/local/opnsense/scripts/OPNsense/AcmeClient/upload_highwinds.php +parameters:-c %s -a %s +type:script +message:uploading a certificate to highwinds From 31a8f778b08c92e92fba3e90da3905eed93b5b88 Mon Sep 17 00:00:00 2001 From: Frank Wall Date: Tue, 15 Jan 2019 00:15:31 +0100 Subject: [PATCH 2/3] security/acme-client: rename "Restart Actions" to "Automation" --- .../OPNsense/AcmeClient/forms/dialogAction.xml | 12 ++++++------ .../OPNsense/AcmeClient/forms/dialogCertificate.xml | 4 ++-- .../mvc/app/models/OPNsense/AcmeClient/Menu/Menu.xml | 4 ++-- .../mvc/app/views/OPNsense/AcmeClient/actions.volt | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) 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 41cc22877..54669fa74 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 @@ -3,25 +3,25 @@ action.enabled checkbox - Enable this restart action. + Enable this automation. action.name text - Name to identify this restart action. + Name to identify this automation. action.description text - Description for this restart action. + Description for this automation. action.type - + dropdown - Pre-defined commands for this restart action. + Pre-defined commands for this automation. @@ -49,7 +49,7 @@ action.configd dropdown - Select a pre-defined system command which should be run for this action. + Select a pre-defined system command which should be run. diff --git a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml index f7ab3d93a..4e6292142 100644 --- a/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml +++ b/security/acme-client/src/opnsense/mvc/app/controllers/OPNsense/AcmeClient/forms/dialogCertificate.xml @@ -52,11 +52,11 @@ certificate.restartActions - + select_multiple true - Choose the actions that should be run after certificate renewal. Basically every application requires a quick restart to reload the updated certificate. If you don't configure a restart action, the in-memory certificate may expire and cause security warnings and other issues. + Choose the automations that should be run after certificate creation and renewal. Basically every application requires a quick restart to reload the updated certificate. If you don't configure an automation, the in-memory certificate may expire and cause security warnings and other issues. certificate.autoRenewal diff --git a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Menu/Menu.xml b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Menu/Menu.xml index 6ed9f6393..472b15b89 100644 --- a/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Menu/Menu.xml +++ b/security/acme-client/src/opnsense/mvc/app/models/OPNsense/AcmeClient/Menu/Menu.xml @@ -7,8 +7,8 @@ - - + + 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 d46c22306..54fd0c35b 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 @@ -1,6 +1,6 @@ {# -Copyright (C) 2017 Frank Wall +Copyright (C) 2017-2019 Frank Wall OPNsense® is Copyright © 2014-2015 by Deciso B.V. All rights reserved. @@ -66,7 +66,7 @@ POSSIBILITY OF SUCH DAMAGE.
@@ -97,4 +97,4 @@ POSSIBILITY OF SUCH DAMAGE.
{# include dialogs #} -{{ partial("layout_partials/base_dialog",['fields':formDialogAction,'id':'DialogAction','label':lang._('Edit Restart Action')])}} +{{ partial("layout_partials/base_dialog",['fields':formDialogAction,'id':'DialogAction','label':lang._('Edit Automation')])}} From 1e797492ff08ea6d54b5e5f8df9b31763928027c Mon Sep 17 00:00:00 2001 From: Frank Wall Date: Tue, 15 Jan 2019 00:24:02 +0100 Subject: [PATCH 3/3] security/acme-client: bump version --- security/acme-client/Makefile | 2 +- .../opnsense/mvc/app/models/OPNsense/AcmeClient/AcmeClient.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/security/acme-client/Makefile b/security/acme-client/Makefile index bf0523622..da392e57d 100644 --- a/security/acme-client/Makefile +++ b/security/acme-client/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= acme-client -PLUGIN_VERSION= 1.18 +PLUGIN_VERSION= 1.19 PLUGIN_COMMENT= Let's Encrypt client PLUGIN_MAINTAINER= opnsense@moov.de PLUGIN_DEPENDS= acme.sh bind912 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 d3b7c2303..3914c1e71 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 @@ -1,6 +1,6 @@ //OPNsense/AcmeClient - 1.4.0 + 1.5.0 A secure Let's Encrypt plugin