From 2a8b0a58edbd9386b8951b6201e1fa913fe88a4c Mon Sep 17 00:00:00 2001 From: Ad Schellevis Date: Mon, 18 May 2020 15:31:18 +0200 Subject: [PATCH] stunnel: initial release (#1840) * stunnel: boilerplate for https://github.com/opnsense/plugins/issues/1829 * stunnel: work in progress for https://github.com/opnsense/plugins/issues/1829 * stunnel: add service control and acl for https://github.com/opnsense/plugins/issues/1829 * stunnel: add cipher selection for https://github.com/opnsense/plugins/issues/1829 Since stunnel uses different parameter pairs for TLSv1.[1,2] and TLSv1.3, we'll try to sort them out in our config template. When no TLSv1.3 ciphers are allowed, we should limit the sslVersionMax parameter as well as it seems. * stunnel: set TLS1.2 as minimum * stunnel: disable rc conf when no services are active https://github.com/opnsense/plugins/issues/1829 * stunnel: CRL support for https://github.com/opnsense/plugins/issues/1829 * stunnel: simplify cert creation, combine cert+key in one file. for https://github.com/opnsense/plugins/issues/1829 * stunnel: syslog and log viewer for https://github.com/opnsense/plugins/issues/1829 * stunnel: add hasync anchor, for https://github.com/opnsense/plugins/issues/1829 --- security/stunnel/Makefile | 8 ++ security/stunnel/pkg-descr | 2 + .../src/etc/inc/plugins.inc.d/stunnel.inc | 107 ++++++++++++++++++ .../Stunnel/Api/ServiceController.php | 42 +++++++ .../Stunnel/Api/ServicesController.php | 80 +++++++++++++ .../OPNsense/Stunnel/ServicesController.php | 39 +++++++ .../OPNsense/Stunnel/forms/dialogService.xml | 69 +++++++++++ .../app/models/OPNsense/Stunnel/ACL/ACL.xml | 9 ++ .../app/models/OPNsense/Stunnel/Menu/Menu.xml | 8 ++ .../app/models/OPNsense/Stunnel/Stunnel.php | 35 ++++++ .../app/models/OPNsense/Stunnel/Stunnel.xml | 73 ++++++++++++ .../app/views/OPNsense/Stunnel/services.volt | 89 +++++++++++++++ .../scripts/stunnel/generate_certs.php | 82 ++++++++++++++ .../conf/actions.d/actions_stunnel.conf | 29 +++++ .../templates/OPNsense/Stunnel/+TARGETS | 2 + .../templates/OPNsense/Stunnel/rc.conf.d | 12 ++ .../templates/OPNsense/Stunnel/stunnel.conf | 44 +++++++ .../OPNsense/Syslog/local/stunnel.conf | 6 + 18 files changed, 736 insertions(+) create mode 100644 security/stunnel/Makefile create mode 100644 security/stunnel/pkg-descr create mode 100644 security/stunnel/src/etc/inc/plugins.inc.d/stunnel.inc create mode 100644 security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/Api/ServiceController.php create mode 100644 security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/Api/ServicesController.php create mode 100644 security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/ServicesController.php create mode 100644 security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/forms/dialogService.xml create mode 100644 security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/ACL/ACL.xml create mode 100644 security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Menu/Menu.xml create mode 100644 security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.php create mode 100644 security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.xml create mode 100644 security/stunnel/src/opnsense/mvc/app/views/OPNsense/Stunnel/services.volt create mode 100755 security/stunnel/src/opnsense/scripts/stunnel/generate_certs.php create mode 100644 security/stunnel/src/opnsense/service/conf/actions.d/actions_stunnel.conf create mode 100644 security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/+TARGETS create mode 100644 security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/rc.conf.d create mode 100644 security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/stunnel.conf create mode 100644 security/stunnel/src/opnsense/service/templates/OPNsense/Syslog/local/stunnel.conf diff --git a/security/stunnel/Makefile b/security/stunnel/Makefile new file mode 100644 index 000000000..d2a57c52b --- /dev/null +++ b/security/stunnel/Makefile @@ -0,0 +1,8 @@ +PLUGIN_NAME= stunnel +PLUGIN_VERSION= 0.1 +PLUGIN_COMMENT= stunnel TLS proxy +PLUGIN_MAINTAINER= ad@opnsense.org +PLUGIN_DEPENDS= stunnel +PLUGIN_DEVEL= yes + +.include "../../Mk/plugins.mk" diff --git a/security/stunnel/pkg-descr b/security/stunnel/pkg-descr new file mode 100644 index 000000000..c25f04950 --- /dev/null +++ b/security/stunnel/pkg-descr @@ -0,0 +1,2 @@ +Stunnel is a proxy designed to add TLS encryption functionality to existing clients and servers without any changes in the programs' code. +(https://www.stunnel.org/) diff --git a/security/stunnel/src/etc/inc/plugins.inc.d/stunnel.inc b/security/stunnel/src/etc/inc/plugins.inc.d/stunnel.inc new file mode 100644 index 000000000..822abd73d --- /dev/null +++ b/security/stunnel/src/etc/inc/plugins.inc.d/stunnel.inc @@ -0,0 +1,107 @@ + array('stunnel_refresh_crls') + ); +} + +function stunnel_refresh_crls() +{ + $stunnel = new OPNsense\Stunnel\Stunnel(); + $configObj = OPNsense\Core\Config::getInstance()->object(); + foreach ($stunnel->services->service->__items as $service) { + if (!empty((string)$service->enabled) && !empty((string)$service->enableCRL)) { + foreach (explode(",", (string)$service->cacert) as $cacert) { + $this_ca = null; + if (!empty($configObj->ca)) { + foreach ($configObj->ca as $ca) { + if ((string)$ca->refid == $cacert && !empty((string)$ca->prv)) { + $this_ca = $ca; + } + } + } + if ($this_ca) { + $ca_hash = null; + $ca_crt = base64_decode((string)$this_ca->crt); + $ca_key = base64_decode((string)$this_ca->prv); + $process = proc_open("openssl x509 -hash -noout", [["pipe", "r"], ["pipe", "w"]], $pipes); + if (is_resource($process)) { + fwrite($pipes[0], $ca_crt); + fclose($pipes[0]); + $ca_hash = trim(stream_get_contents($pipes[1])); + fclose($pipes[1]); + proc_close($process); + } + if ($ca_hash) { + $crlres = openssl_crl_new($ca_crt, 0, 9999); + if (!empty($configObj->crl)) { + foreach ($configObj->crl as $crl) { + if ($crl->caref == $cacert && !empty((string)$crl->cert)) { + foreach ($crl->cert as $cert) { + openssl_crl_revoke_cert( + $crlres, + base64_decode((string)$cert->crt), + (string)$cert->revoke_time, + (string)$cert->reason + ); + } + } + } + } + $crl_text = ""; + openssl_crl_export($crlres, $crl_text, $ca_key); + file_put_contents("/var/run/stunnel/certs/{$ca_hash}.r0", $crl_text); + } + } + } + } + } +} + +function stunnel_syslog() +{ + $logfacilities = array(); + $logfacilities['stunnel'] = array( + 'facility' => array('stunnel') + ); + return $logfacilities; +} + +function stunnel_xmlrpc_sync() +{ + $result = array(); + $result[] = array( + 'description' => gettext('Stunnel'), + 'section' => 'OPNsense.Stunnel', + 'id' => 'stunnel', + ); + return $result; +} diff --git a/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/Api/ServiceController.php b/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/Api/ServiceController.php new file mode 100644 index 000000000..dc1ba13c9 --- /dev/null +++ b/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/Api/ServiceController.php @@ -0,0 +1,42 @@ +getModel()->general->enabled = "0"; + foreach ($this->getModel()->services->service->__items as $service) { + if ((string)$service->enabled == "1") { + $this->getModel()->general->enabled = "1"; + break; + } + } + parent::save(); + } + + public function searchItemAction() + { + return $this->searchBase("services.service", array('enabled', 'description'), "description"); + } + + public function setItemAction($uuid) + { + return $this->setBase("service", "services.service", $uuid); + } + + public function addItemAction() + { + return $this->addBase("service", "services.service"); + } + + public function getItemAction($uuid = null) + { + return $this->getBase("service", "services.service", $uuid); + } + + public function delItemAction($uuid) + { + return $this->delBase("services.service", $uuid); + } + + public function toggleItemAction($uuid, $enabled = null) + { + return $this->toggleBase("services.service", $uuid, $enabled); + } +} diff --git a/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/ServicesController.php b/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/ServicesController.php new file mode 100644 index 000000000..dfb898991 --- /dev/null +++ b/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/ServicesController.php @@ -0,0 +1,39 @@ +view->pick('OPNsense/Stunnel/services'); + $this->view->formDialogService = $this->getForm("dialogService"); + } +} diff --git a/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/forms/dialogService.xml b/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/forms/dialogService.xml new file mode 100644 index 000000000..9d3230280 --- /dev/null +++ b/security/stunnel/src/opnsense/mvc/app/controllers/OPNsense/Stunnel/forms/dialogService.xml @@ -0,0 +1,69 @@ +
+ + service.enabled + + checkbox + Enable this rule + + + service.accept_address + + text + If possible, a loopback address is the safest choice here, you can forward traffic to it using the firewall. + + + service.accept_port + + text + The port on which connections will be accepted. + + + service.connect_address + + text + The other end of this tunnel. + + + service.connect_port + + text + The port to forward traffic to. + + + service.servercert + + dropdown + + + + service.cacert + + select_multiple + CA Manager.]]> + + + service.enableCRL + + checkbox + + + + service.ciphers + + select_multiple + + true + + + service.description + + text + +
diff --git a/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/ACL/ACL.xml b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/ACL/ACL.xml new file mode 100644 index 000000000..8193662ca --- /dev/null +++ b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/ACL/ACL.xml @@ -0,0 +1,9 @@ + + + Services: Stunnel + + ui/stunnel/* + api/stunnel/* + + + diff --git a/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Menu/Menu.xml b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Menu/Menu.xml new file mode 100644 index 000000000..6cd50fa8c --- /dev/null +++ b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Menu/Menu.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.php b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.php new file mode 100644 index 000000000..b4075e267 --- /dev/null +++ b/security/stunnel/src/opnsense/mvc/app/models/OPNsense/Stunnel/Stunnel.php @@ -0,0 +1,35 @@ + + //OPNsense/Stunnel + 1.0.0 + MFP + + OPNsense firewall filter rules + + + + + 1 + Y + + + + + + 1 + Y + + + 1 + 65535 + port needs to be an integer value between 1 and 65535 + Y + + + Y + N + 127.0.0.1 + + + Y + + + 1 + 65535 + port needs to be an integer value between 1 and 65535 + Y + + + N + Y + ca + Please select a valid certificate from the list + + + 0 + Y + + + TLS_AES_128_GCM_SHA256,TLS_AES_256_GCM_SHA384,TLS_CHACHA20_POLY1305_SHA256,ECDHE-ECDSA-AES128-GCM-SHA256,ECDHE-RSA-AES128-GCM-SHA256,ECDHE-ECDSA-AES256-GCM-SHA384,ECDHE-RSA-AES256-GCM-SHA384,ECDHE-ECDSA-CHACHA20-POLY1305,ECDHE-RSA-CHACHA20-POLY1305,DHE-RSA-AES128-GCM-SHA256,DHE-RSA-AES256-GCM-SHA384 + Y + Y + stunnel ssl ciphers + /tmp/stunnel_ciphers_list.json + 360 + Please specify valid tls ciphers. + + + Y + cert + Please select a valid certificate from the list + + + N + /^([\t\n\v\f\r 0-9a-zA-Z.\-,_\x{00A0}-\x{FFFF}]){0,255}$/u + Description should be a string between 1 and 255 characters + + + + + diff --git a/security/stunnel/src/opnsense/mvc/app/views/OPNsense/Stunnel/services.volt b/security/stunnel/src/opnsense/mvc/app/views/OPNsense/Stunnel/services.volt new file mode 100644 index 000000000..fd6ba46c4 --- /dev/null +++ b/security/stunnel/src/opnsense/mvc/app/views/OPNsense/Stunnel/services.volt @@ -0,0 +1,89 @@ +{# + # Copyright (c) 2020 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. + #} + + + + + +
+
+ + + + + + + + + + + + + + + + + + +
{{ lang._('ID') }}{{ lang._('Enabled') }}{{ lang._('Description') }}{{ lang._('Commands') }}
+ + +
+
+
+ +
+ +

+
+
+ + +{{ partial("layout_partials/base_dialog",['fields':formDialogService, 'id':'DialogService','label':lang._('Edit Service')])}} diff --git a/security/stunnel/src/opnsense/scripts/stunnel/generate_certs.php b/security/stunnel/src/opnsense/scripts/stunnel/generate_certs.php new file mode 100755 index 000000000..b84979a96 --- /dev/null +++ b/security/stunnel/src/opnsense/scripts/stunnel/generate_certs.php @@ -0,0 +1,82 @@ +#!/usr/local/bin/php +object(); +$all_certs = []; +foreach ($stunnel->services->service->__items as $service) { + if (!empty((string)$service->enabled)) { + $this_uuid = $service->getAttributes()['uuid']; + $srv_certid = (string)$service->servercert; + foreach ($configObj->cert as $cert) { + if ($srv_certid == (string)$cert->refid) { + $all_certs["{$base_path}/{$this_uuid}.crt"] = + base64_decode((string)$cert->crt) . "\n" . base64_decode((string)$cert->prv); + } + } + if (!empty((string)$service->cacert)) { + $all_certs["{$base_path}/{$this_uuid}.ca"] = ""; + foreach (explode(",", (string)$service->cacert) as $caid) { + foreach ($configObj->ca as $ca) { + if ((string)$ca->refid == $caid) { + $all_certs["{$base_path}/{$this_uuid}.ca"] .= base64_decode((string)$ca->crt)."\n"; + } + } + } + } + } +} + +if (!is_dir("/usr/local/etc/stunnel/certs")) { + mkdir("/usr/local/etc/stunnel/certs", 0700, true); + chown("/usr/local/etc/stunnel/certs", "stunnel"); + chgrp("/usr/local/etc/stunnel/certs", "stunnel"); +} + +// cleanup stunnel cert directory +foreach (glob("{$base_path}/*") as $filename) { + if (!isset($all_certs[$filename])) { + unlink($filename); + } +} + +foreach($all_certs as $filename => $content) { + file_put_contents($filename, $content); + chown($filename, "stunnel"); +} + +// trigger certificate revocation lists update +plugins_configure('crl'); diff --git a/security/stunnel/src/opnsense/service/conf/actions.d/actions_stunnel.conf b/security/stunnel/src/opnsense/service/conf/actions.d/actions_stunnel.conf new file mode 100644 index 000000000..ac583ea67 --- /dev/null +++ b/security/stunnel/src/opnsense/service/conf/actions.d/actions_stunnel.conf @@ -0,0 +1,29 @@ +[ssl.ciphers] +command:/usr/local/opnsense/scripts/system/ssl_ciphers.py --format=key_value +parameters: +type:script_output +message:List SSL ciphers + +[start] +command:/usr/local/etc/rc.d/stunnel start +parameters: +type:script +message:stunnel service start + +[stop] +command:/usr/local/etc/rc.d/stunnel stop +parameters: +type:script +message:stunnel service stop + +[restart] +command:/usr/local/etc/rc.d/stunnel restart +parameters: +type:script +message:stunnel service restart + +[status] +command:/usr/local/etc/rc.d/stunnel status; exit 0 +parameters: +type:script_output +message:stunnel status diff --git a/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/+TARGETS b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/+TARGETS new file mode 100644 index 000000000..177c36ce4 --- /dev/null +++ b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/+TARGETS @@ -0,0 +1,2 @@ +stunnel.conf:/usr/local/etc/stunnel/stunnel.conf +rc.conf.d:/etc/rc.conf.d/stunnel diff --git a/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/rc.conf.d b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/rc.conf.d new file mode 100644 index 000000000..874ae1b74 --- /dev/null +++ b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/rc.conf.d @@ -0,0 +1,12 @@ +{% if not helpers.empty('OPNsense.Stunnel.general.enabled') %} +stunnel_enable="YES" +stunnel_pidfile="/var/run/stunnel/stunnel.pid" + +mkdir -p /var/run/stunnel/certs +chown -R stunnel:stunnel /var/run/stunnel +chmod -R 700 /var/run/stunnel + +/usr/local/opnsense/scripts/stunnel/generate_certs.php > /dev/null 2>&1 +{% else %} +stunnel_enable="NO" +{% endif %} diff --git a/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/stunnel.conf b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/stunnel.conf new file mode 100644 index 000000000..4f89d541e --- /dev/null +++ b/security/stunnel/src/opnsense/service/templates/OPNsense/Stunnel/stunnel.conf @@ -0,0 +1,44 @@ +setuid = stunnel +setgid = stunnel +chroot = /var/run/stunnel +pid = /stunnel.pid +debug = info +logId = unique + + +{% if helpers.exists('OPNsense.Stunnel.services.service') %} +{% for service in helpers.toList('OPNsense.Stunnel.services.service') %} +{% if service.enabled|default('0') == '1' %} + + +; ************************************************************************** +; * {{ service.description }} +; ************************************************************************** +[{{service['@uuid']}}] +accept = {% if service.accept_address %}{{service.accept_address}}:{% endif %}{{service.accept_port}} +connect = {% if service.connect_address.find(":") > -1 %}[{{service.connect_address}}]{% else %}{{service.connect_address}}{% endif %}:{{service.connect_port}} +cert = /usr/local/etc/stunnel/certs/{{service['@uuid']}}.crt +{% if service.cacert|default('') != '' %} +CAfile = /usr/local/etc/stunnel/certs/{{service['@uuid']}}.ca +requireCert = yes +verifyChain = yes +{% if service.enableCRL|default('0') == '1' %} +CRLpath = /certs/ +{% endif %} +{% endif %} +{% set ciphers =[] %} +{% set ciphersuites =[] %} +{% for cipher in service.ciphers.split(',') %} +{% if cipher.startswith('TLS') %} +{% do ciphersuites.append(cipher) %} +{% else %} +{% do ciphers.append(cipher) %} +{% endif %} +{% endfor %} +ciphers = {{ ciphers|join(':') }} +ciphersuites = {{ ciphersuites|join(':') }} +sslVersionMin=TLSv1.2 +sslVersionMax={% if ciphersuites %}TLSv1.3{% else %}TLSv1.2{% endif %} +{% endif %} +{% endfor %} +{% endif %} diff --git a/security/stunnel/src/opnsense/service/templates/OPNsense/Syslog/local/stunnel.conf b/security/stunnel/src/opnsense/service/templates/OPNsense/Syslog/local/stunnel.conf new file mode 100644 index 000000000..96af65e4d --- /dev/null +++ b/security/stunnel/src/opnsense/service/templates/OPNsense/Syslog/local/stunnel.conf @@ -0,0 +1,6 @@ +################################################################### +# Local syslog-ng configuration filter definition [stunnel]. +################################################################### +filter f_local_stunnel { + program("stunnel"); +};