mirror of
https://github.com/netbirdio/plugins.git
synced 2026-05-22 18:44:07 -07:00
Merge pull request #1134 from fraenki/acme_automation
security/acme-client: release 1.19
This commit is contained in:
@@ -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
|
||||
|
||||
+23
-6
@@ -3,25 +3,42 @@
|
||||
<id>action.enabled</id>
|
||||
<label>Enabled</label>
|
||||
<type>checkbox</type>
|
||||
<help>Enable this restart action.</help>
|
||||
<help>Enable this automation.</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.name</id>
|
||||
<label>Name</label>
|
||||
<type>text</type>
|
||||
<help>Name to identify this restart action.</help>
|
||||
<help>Name to identify this automation.</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.description</id>
|
||||
<label>Description</label>
|
||||
<type>text</type>
|
||||
<help>Description for this restart action.</help>
|
||||
<help>Description for this automation.</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.type</id>
|
||||
<label>Restart Command</label>
|
||||
<label>Run Command</label>
|
||||
<type>dropdown</type>
|
||||
<help>Pre-defined commands for this restart action.</help>
|
||||
<help>Pre-defined commands for this automation.</help>
|
||||
</field>
|
||||
<field>
|
||||
<label>Required Parameters</label>
|
||||
<type>header</type>
|
||||
<style>method_table method_table_upload_highwinds</style>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.highwinds_account_hash</id>
|
||||
<label>Account Hash</label>
|
||||
<type>text</type>
|
||||
<help>Account hash for Highwinds API.</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>action.highwinds_access_token</id>
|
||||
<label>Access Token</label>
|
||||
<type>text</type>
|
||||
<help>Access token for Highwinds API.</help>
|
||||
</field>
|
||||
<field>
|
||||
<label>Required Parameters</label>
|
||||
@@ -32,7 +49,7 @@
|
||||
<id>action.configd</id>
|
||||
<label>System Command</label>
|
||||
<type>dropdown</type>
|
||||
<help>Select a pre-defined system command which should be run for this action.</help>
|
||||
<help>Select a pre-defined system command which should be run.</help>
|
||||
<style>table_optional table_optional_configd</style>
|
||||
</field>
|
||||
</form>
|
||||
|
||||
+2
-2
@@ -52,11 +52,11 @@
|
||||
</field>
|
||||
<field>
|
||||
<id>certificate.restartActions</id>
|
||||
<label>Restart Actions</label>
|
||||
<label>Automations</label>
|
||||
<type>select_multiple</type>
|
||||
<style>tokenize</style>
|
||||
<allownew>true</allownew>
|
||||
<help>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.</help>
|
||||
<help>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.</help>
|
||||
</field>
|
||||
<field>
|
||||
<id>certificate.autoRenewal</id>
|
||||
|
||||
+12
-1
@@ -1,6 +1,6 @@
|
||||
<model>
|
||||
<mount>//OPNsense/AcmeClient</mount>
|
||||
<version>1.4.0</version>
|
||||
<version>1.5.0</version>
|
||||
<description>A secure Let's Encrypt plugin</description>
|
||||
<items>
|
||||
<settings>
|
||||
@@ -702,9 +702,20 @@
|
||||
<restart_gui>Restart OPNsense Web UI</restart_gui>
|
||||
<restart_haproxy>Restart HAProxy (OPNsense plugin)</restart_haproxy>
|
||||
<restart_nginx>Restart Nginx (OPNsense plugin)</restart_nginx>
|
||||
<upload_highwinds>Upload certificate to Highwinds CDN</upload_highwinds>
|
||||
<configd>System or Plugin Command (select below)</configd>
|
||||
</OptionValues>
|
||||
</type>
|
||||
<highwinds_account_hash type="TextField">
|
||||
<Required>N</Required>
|
||||
<mask>/^.{1,1024}$/u</mask>
|
||||
<ValidationMessage>Should be a string between 1 and 1024 characters.</ValidationMessage>
|
||||
</highwinds_account_hash>
|
||||
<highwinds_access_token type="TextField">
|
||||
<Required>N</Required>
|
||||
<mask>/^.{1,1024}$/u</mask>
|
||||
<ValidationMessage>Should be a string between 1 and 1024 characters.</ValidationMessage>
|
||||
</highwinds_access_token>
|
||||
<configd type="ConfigdActionsField">
|
||||
<filters>
|
||||
<description>/^(?!.*(Let\'s\ Encrypt|acme|[fF]irmware))([\S\s]{1,255})/</description>
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
</Settings>
|
||||
<Accounts VisibleName="Accounts" order="20" url="/ui/acmeclient/accounts"/>
|
||||
<Validations VisibleName="Validation Methods" order="30" url="/ui/acmeclient/validations"/>
|
||||
<Actions VisibleName="Restart Actions" order="40" url="/ui/acmeclient/actions"/>
|
||||
<Certificates order="50" url="/ui/acmeclient/certificates"/>
|
||||
<Certificates order="40" url="/ui/acmeclient/certificates"/>
|
||||
<Automation VisibleName="Automation" order="50" url="/ui/acmeclient/actions"/>
|
||||
<LogFile VisibleName="Log File" order="60" url="/diag_logs_acmeclient.php"/>
|
||||
</LEAcmeClient>
|
||||
</Services>
|
||||
|
||||
@@ -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.
|
||||
</script>
|
||||
|
||||
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
|
||||
<li class="active"><a data-toggle="tab" href="#actions">{{ lang._('Restart Actions') }}</a></li>
|
||||
<li class="active"><a data-toggle="tab" href="#actions">{{ lang._('Automation') }}</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content content-box tab-content">
|
||||
@@ -97,4 +97,4 @@ POSSIBILITY OF SUCH DAMAGE.
|
||||
</div>
|
||||
|
||||
{# 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')])}}
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
#!/usr/local/bin/php
|
||||
<?php
|
||||
/*
|
||||
* Copyright (C) 2019 Frank Wall
|
||||
* Copyright (C) 2015 Deciso B.V.
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
|
||||
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
* AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
* AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
|
||||
* OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
||||
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
||||
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
* POSSIBILITY OF SUCH DAMAGE.
|
||||
*/
|
||||
|
||||
require_once("config.inc");
|
||||
require_once("certs.inc");
|
||||
require_once("legacy_bindings.inc");
|
||||
require_once("util.inc");
|
||||
|
||||
use OPNsense\Core\Backend;
|
||||
use OPNsense\Core\Config;
|
||||
use OPNsense\Base;
|
||||
use OPNsense\AcmeClient\AcmeClient;
|
||||
|
||||
$HIGHWINDS_API_URL = 'https://striketracker.highwinds.com/api/v1/accounts';
|
||||
|
||||
function find_certificate($acme_cert_id) {
|
||||
$modelObj = new OPNsense\AcmeClient\AcmeClient;
|
||||
$configObj = Config::getInstance()->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);
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user