diff --git a/src/etc/ssl/ext_sources/README b/src/etc/ssl/ext_sources/README
new file mode 100644
index 000000000..a2c9c87fb
--- /dev/null
+++ b/src/etc/ssl/ext_sources/README
@@ -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.
diff --git a/src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CertificatesField.php b/src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CertificatesField.php
index 39fbf8253..703c48140 100644
--- a/src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CertificatesField.php
+++ b/src/opnsense/mvc/app/models/OPNsense/Trust/FieldTypes/CertificatesField.php
@@ -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 = [];
diff --git a/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt b/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt
index f3038b5d9..33a84bf5c 100644
--- a/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt
+++ b/src/opnsense/mvc/app/views/OPNsense/Trust/cert.volt
@@ -48,7 +48,9 @@
},
formatters: {
in_use: function (column, row) {
- if (row.in_use === '1') {
+ if (!row.uuid.includes('-')) {
+ return "";
+ } else if (row.in_use === '1') {
return "";
} else if (row.is_user === '1') {
return "";
@@ -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) {
diff --git a/src/opnsense/scripts/system/cert_fetch_local.py b/src/opnsense/scripts/system/cert_fetch_local.py
new file mode 100755
index 000000000..cadbba714
--- /dev/null
+++ b/src/opnsense/scripts/system/cert_fetch_local.py
@@ -0,0 +1,59 @@
+#!/usr/local/bin/python3
+"""
+ Copyright (c) 2025 Ad Schellevis
+ 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))
\ No newline at end of file
diff --git a/src/opnsense/service/conf/actions.d/actions_system.conf b/src/opnsense/service/conf/actions.d/actions_system.conf
index 61638f7a7..bf062736d 100644
--- a/src/opnsense/service/conf/actions.d/actions_system.conf
+++ b/src/opnsense/service/conf/actions.d/actions_system.conf
@@ -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