Merge pull request #1134 from fraenki/acme_automation

security/acme-client: release 1.19
This commit is contained in:
Frank Wall
2019-01-15 10:47:38 +01:00
committed by GitHub
9 changed files with 307 additions and 17 deletions
+1 -1
View File
@@ -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
@@ -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>
@@ -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>
@@ -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)) {
@@ -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