diff --git a/plist b/plist index a1b3bd58c0..f92792e7d6 100644 --- a/plist +++ b/plist @@ -1328,6 +1328,7 @@ /usr/local/opnsense/scripts/openssh/ssh_query.py /usr/local/opnsense/scripts/openvpn/client_connect.php /usr/local/opnsense/scripts/openvpn/client_disconnect.sh +/usr/local/opnsense/scripts/openvpn/genkey.py /usr/local/opnsense/scripts/openvpn/kill_session.py /usr/local/opnsense/scripts/openvpn/ovpn_event.py /usr/local/opnsense/scripts/openvpn/ovpn_service_control.php diff --git a/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/InstancesController.php b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/InstancesController.php index 36cad771d2..cf6ae80351 100644 --- a/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/InstancesController.php +++ b/src/opnsense/mvc/app/controllers/OPNsense/OpenVPN/Api/InstancesController.php @@ -96,16 +96,17 @@ class InstancesController extends ApiMutableModelControllerBase return $this->delBase('StaticKeys.StaticKey', $uuid); } - public function genKeyAction($type = 'secret') + public function genKeyAction(string $type = 'secret'): array { - if (in_array($type, ['secret', 'auth-token', 'tls-auth', 'tls-crypt'])) { - $key = (new Backend())->configdpRun("openvpn genkey", [$type]); - if (strpos($key, '-----BEGIN') !== false) { - return [ - 'result' => 'ok', - 'key' => trim($key) - ]; - } + // If openvpn is run in client mode, the user must supply their own tls-crypt-v2-client key. + // Generating it is pointless since the server key should remain with the server only. + // We only generate keys here that can be used verbatim in server mode. + if (!in_array($type, ['secret', 'auth-token', 'tls-auth', 'tls-crypt', 'tls-crypt-v2-server'], true)) { + return ['result' => 'failed', 'message' => gettext('unknown key type')]; + } + $key = (new Backend())->configdpRun("openvpn genkey", [$type]); + if ($key !== null) { + return ['result' => 'ok', 'key' => trim($key)]; } return ['result' => 'failed']; } diff --git a/src/opnsense/mvc/app/library/OPNsense/OpenVPN/ArchiveOpenVPN.php b/src/opnsense/mvc/app/library/OPNsense/OpenVPN/ArchiveOpenVPN.php index da5639ceba..02c57f9815 100644 --- a/src/opnsense/mvc/app/library/OPNsense/OpenVPN/ArchiveOpenVPN.php +++ b/src/opnsense/mvc/app/library/OPNsense/OpenVPN/ArchiveOpenVPN.php @@ -28,6 +28,7 @@ namespace OPNsense\OpenVPN; +use OPNsense\Base\UserException; use OPNsense\Core\AppConfig; use OPNsense\Core\Shell; @@ -99,12 +100,21 @@ class ArchiveOpenVPN extends PlainOpenVPN } } if (!empty($this->config['tls'])) { - if ($this->config['tlsmode'] === 'crypt') { - $conf[] = "tls-crypt {$base_filename}-tls.key"; + $keyfile = "{$base_filename}-tls.key"; + if ($this->config['tlsmode'] === 'crypt-v2') { + $clientKey = $this->export_crypt_v2_client_key($this->config['tls']); + if (empty($clientKey)) { + throw new UserException(gettext('Failed to generate tls-crypt-v2 client key')); + } + file_put_contents("{$content_dir}/{$keyfile}", trim(base64_decode($clientKey, true))); + $conf[] = "tls-crypt-v2 {$keyfile}"; + } elseif ($this->config['tlsmode'] === 'crypt') { + file_put_contents("{$content_dir}/{$keyfile}", trim(base64_decode($this->config['tls']))); + $conf[] = "tls-crypt {$keyfile}"; } else { - $conf[] = "tls-auth {$base_filename}-tls.key 1"; + file_put_contents("{$content_dir}/{$keyfile}", trim(base64_decode($this->config['tls']))); + $conf[] = "tls-auth {$keyfile} 1"; } - file_put_contents("{$content_dir}/{$base_filename}-tls.key", trim(base64_decode($this->config['tls']))); } file_put_contents("{$content_dir}/{$base_filename}.ovpn", implode("\n", $conf)); diff --git a/src/opnsense/mvc/app/library/OPNsense/OpenVPN/BaseExporter.php b/src/opnsense/mvc/app/library/OPNsense/OpenVPN/BaseExporter.php index 68e11386b8..c5af4f0589 100644 --- a/src/opnsense/mvc/app/library/OPNsense/OpenVPN/BaseExporter.php +++ b/src/opnsense/mvc/app/library/OPNsense/OpenVPN/BaseExporter.php @@ -1,7 +1,7 @@ configdpRun("openvpn genkey", ['tls-crypt-v2-client', $serverKey])); + } } diff --git a/src/opnsense/mvc/app/library/OPNsense/OpenVPN/PlainOpenVPN.php b/src/opnsense/mvc/app/library/OPNsense/OpenVPN/PlainOpenVPN.php index 7d5e840b42..189e1e2473 100644 --- a/src/opnsense/mvc/app/library/OPNsense/OpenVPN/PlainOpenVPN.php +++ b/src/opnsense/mvc/app/library/OPNsense/OpenVPN/PlainOpenVPN.php @@ -28,6 +28,8 @@ namespace OPNsense\OpenVPN; +use OPNsense\Base\UserException; + class PlainOpenVPN extends BaseExporter implements IExportProvider { /** @@ -196,7 +198,15 @@ class PlainOpenVPN extends BaseExporter implements IExportProvider $conf[] = ""; } if (!empty($this->config['tls'])) { - if ($this->config['tlsmode'] === 'crypt') { + if ($this->config['tlsmode'] === 'crypt-v2') { + $clientKey = $this->export_crypt_v2_client_key($this->config['tls']); + if (empty($clientKey)) { + throw new UserException(gettext('Failed to generate tls-crypt-v2 client key')); + } + $conf[] = ""; + $conf = array_merge($conf, explode("\n", trim(base64_decode($clientKey, true)))); + $conf[] = ""; + } elseif ($this->config['tlsmode'] === 'crypt') { $conf[] = ""; $conf = array_merge($conf, explode("\n", trim(base64_decode($this->config['tls'])))); $conf[] = ""; diff --git a/src/opnsense/mvc/app/library/OPNsense/OpenVPN/ViscosityVisz.php b/src/opnsense/mvc/app/library/OPNsense/OpenVPN/ViscosityVisz.php index 7af304d785..30ce96ce00 100644 --- a/src/opnsense/mvc/app/library/OPNsense/OpenVPN/ViscosityVisz.php +++ b/src/opnsense/mvc/app/library/OPNsense/OpenVPN/ViscosityVisz.php @@ -28,6 +28,7 @@ namespace OPNsense\OpenVPN; +use OPNsense\Base\UserException; use OPNsense\Core\AppConfig; use OPNsense\Core\Shell; @@ -129,12 +130,20 @@ class ViscosityVisz extends PlainOpenVPN } } if (!empty($this->config['tls'])) { - if ($this->config['tlsmode'] === 'crypt') { + if ($this->config['tlsmode'] === 'crypt-v2') { + $clientKey = $this->export_crypt_v2_client_key($this->config['tls']); + if (empty($clientKey)) { + throw new UserException(gettext('Failed to generate tls-crypt-v2 client key')); + } + file_put_contents("{$content_dir}/ta.key", trim(base64_decode($clientKey, true))); + $conf[] = "tls-crypt-v2 ta.key"; + } elseif ($this->config['tlsmode'] === 'crypt') { + file_put_contents("{$content_dir}/ta.key", trim(base64_decode($this->config['tls']))); $conf[] = "tls-crypt ta.key"; } else { + file_put_contents("{$content_dir}/ta.key", trim(base64_decode($this->config['tls']))); $conf[] = "tls-auth ta.key 1"; } - file_put_contents("{$content_dir}/ta.key", trim(base64_decode($this->config['tls']))); } file_put_contents("{$content_dir}/config.conf", implode("\n", $conf)); diff --git a/src/opnsense/mvc/app/models/OPNsense/OpenVPN/OpenVPN.xml b/src/opnsense/mvc/app/models/OPNsense/OpenVPN/OpenVPN.xml index b8fc39f03f..b3c21c804b 100644 --- a/src/opnsense/mvc/app/models/OPNsense/OpenVPN/OpenVPN.xml +++ b/src/opnsense/mvc/app/models/OPNsense/OpenVPN/OpenVPN.xml @@ -437,6 +437,7 @@ auth (Authenticate control channel packets) crypt (Encrypt and authenticate all control channel packets) + crypt-v2 (Encrypt and authenticate all control channel packets) diff --git a/src/opnsense/mvc/app/views/OPNsense/OpenVPN/instances.volt b/src/opnsense/mvc/app/views/OPNsense/OpenVPN/instances.volt index 77b568558e..b3b9b12430 100644 --- a/src/opnsense/mvc/app/views/OPNsense/OpenVPN/instances.volt +++ b/src/opnsense/mvc/app/views/OPNsense/OpenVPN/instances.volt @@ -84,8 +84,9 @@ $("#keygen").click(function() { let statickey_mode = $("#statickey\\.mode").val(); const mode_map = { - auth: "tls-auth", - crypt: "tls-crypt" + "auth": "tls-auth", + "crypt": "tls-crypt", + "crypt-v2": "tls-crypt-v2-server", }; ajaxGet("/api/openvpn/instances/gen_key/" + mode_map[statickey_mode], {}, function(data){ if (data.result === 'ok') { diff --git a/src/opnsense/scripts/openvpn/genkey.py b/src/opnsense/scripts/openvpn/genkey.py new file mode 100755 index 0000000000..3a9c0a612a --- /dev/null +++ b/src/opnsense/scripts/openvpn/genkey.py @@ -0,0 +1,53 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2026 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. +""" + +import argparse +import base64 +import subprocess +import tempfile + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('type', help='type', type=str) + parser.add_argument('--server_key', help='server key, base64 encoded', type=str) + args = parser.parse_args() + with tempfile.NamedTemporaryFile(mode='w') as fh: + cmd = [ + '/usr/local/sbin/openvpn', + '--genkey', + args.type + ] + if args.type == 'tls-crypt-v2-client': + fh.write(base64.b64decode(args.server_key).decode()) + fh.flush() + cmd.append('--tls-crypt-v2') + cmd.append(fh.name) + + sp = subprocess.run(cmd, capture_output=True, text=True) + print(sp.stdout.strip()) diff --git a/src/opnsense/service/conf/actions.d/actions_openvpn.conf b/src/opnsense/service/conf/actions.d/actions_openvpn.conf index be78b6ac66..ca415c9584 100644 --- a/src/opnsense/service/conf/actions.d/actions_openvpn.conf +++ b/src/opnsense/service/conf/actions.d/actions_openvpn.conf @@ -11,8 +11,8 @@ type:script_output message:Kill OpenVPN session %s - %s [genkey] -command:/usr/local/sbin/openvpn -parameters:--genkey %s /dev/stdout +command:/usr/local/opnsense/scripts/openvpn/genkey.py +parameters: %s --server_key %s type:script_output message: Generate new OpenVPN static %s key