System: Trust: Certificates - offer config directory (/usr/local/etc/ssl/ext_sources/) to store locations for certificates not managed by us, but practical to know about their existence. closes https://github.com/opnsense/core/issues/8279

This is useful for services like OPNWAF and Caddy. This commit only adds the facility and changes the admin page, the widget is left unaltered.
This commit is contained in:
Ad Schellevis 2025-04-03 15:08:44 +02:00
parent 63b9f2e1aa
commit f90e5445db
5 changed files with 114 additions and 2 deletions

View file

@ -0,0 +1,14 @@
OPNsense certificates used by external applications and registered in the gui for viewing purposes only.
In this directory you can create files with a .conf extension (e.g. myapp.conf) with the following (sample) config:
[location]
base=/usr/local/md/domains
pattern=pubcert.pem
description=OPNWAF
"base" is the directory to recursively iterate, "pattern" is a regex matcher and each item returned will have a description
attached to it for the frontend.
Files found can be identified by their id, which is an md5 hash of the contents.

View file

@ -28,6 +28,7 @@
namespace OPNsense\Trust\FieldTypes;
use OPNsense\Core\Backend;
use OPNsense\Core\Config;
use OPNsense\Base\FieldTypes\ArrayField;
use OPNsense\Base\FieldTypes\ContainerField;
@ -105,6 +106,26 @@ class CertificatesField extends ArrayField
return $container_node;
}
/**
* @return array of externally managed certificates
*/
protected static function getStaticChildren()
{
$result = [];
$ext_data = json_decode((new Backend())->configdRun('system trust ext_sources') ?? '', true);
if (is_array($ext_data)) {
foreach ($ext_data as $data) {
$payload = \OPNsense\Trust\Store::parseX509($data['cert'] ?? '');
if ($payload !== false && !empty($data['id'])) {
$payload['crt_payload'] = $data['cert'];
$payload['descr'] = $data['descr'] ?? '';
$result[$data['id']] = $payload;
}
}
}
return $result;
}
protected function actionPostLoadingEvent()
{
$usernames = [];

View file

@ -48,7 +48,9 @@
},
formatters: {
in_use: function (column, row) {
if (row.in_use === '1') {
if (!row.uuid.includes('-')) {
return "<span class=\"fa fa-fw fa-sign-out\" data-value=\"1\" data-row-id=\"" + row.uuid + "\"></span>";
} else if (row.in_use === '1') {
return "<span class=\"fa fa-fw fa-check\" data-value=\"1\" data-row-id=\"" + row.uuid + "\"></span>";
} else if (row.is_user === '1') {
return "<span class=\"fa fa-fw fa-user-o\" data-value=\"1\" data-row-id=\"" + row.uuid + "\"></span>";
@ -148,7 +150,17 @@
}
});
grid_cert.on("loaded.rs.jquery.bootgrid", function (e){
// reload categories before grid load
/* should probably live in the "commands" section to optionally render items */
grid_cert.find('tr').each(function(){
let tr = $(this);
if (tr.data('row-id') !== undefined && !tr.data('row-id').includes('-')) {
tr.find('button.command-edit').hide();
tr.find('button.command-copy').hide();
tr.find('button.command-delete').hide();
}
});
// reload categories after grid load
if ($("#ca_filter > option").length == 0) {
ajaxGet('/api/trust/cert/ca_list', {}, function(data, status){
if (data.rows !== undefined) {

View file

@ -0,0 +1,59 @@
#!/usr/local/bin/python3
"""
Copyright (c) 2025 Ad Schellevis <ad@opnsense.org>
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.
---------------------------------------------------------------------------------------------------
fetch a pluggable set of locally managed certificates which are not available in the configuration
"""
import glob
import hashlib
import os
import re
import ujson
from configparser import ConfigParser
if __name__ == '__main__':
result = []
for conffile in glob.glob("/usr/local/etc/ssl/ext_sources/*.conf"):
cnf = ConfigParser()
cnf.read(conffile)
if cnf.has_section('location') and cnf.has_option('location', 'base') and cnf.has_option('location', 'pattern'):
if cnf.has_option('location', 'description'):
loc_descr = cnf.get('location', 'description')
else:
loc_descr = os.path.basename(conffile)[0:-5]
match_pattern = re.compile(cnf.get('location', 'pattern'))
for root, dirs, files in os.walk(cnf.get('location', 'base')):
for filename in files:
full_path = "%s/%s" % (root, filename)
if match_pattern.match(filename) and os.path.getsize(full_path) < 1024*1024:
payload = open(full_path).read()
result.append({
'cert': payload,
'descr': loc_descr,
'id': hashlib.md5(payload.encode()).hexdigest()
})
print(ujson.dumps(result))

View file

@ -196,6 +196,12 @@ command:/usr/local/sbin/pluginctl -c crl
type:script
message: trigger CRL changed event
[trust.ext_sources]
command:/usr/local/opnsense/scripts/system/cert_fetch_local.py
type:script_output
message: fetch list of externally managed certificates
cache_ttl: 60
[cpu.stream]
command:/usr/local/opnsense/scripts/system/cpu.py
parameters:--interval %s