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