VPN: OpenVPN: add tls-crypt-v2 support (#10069)

* VPN: OpenVPN: Add tls-crypt-v2 support, initial implementation

* Unify key generation into a single bash script that handles stdout parsing and always emits base64, consume that in the key generator

* plist fix

* Add comment that explains stuff a bit better

* VPN: OpenVPN: add tls-crypt-v2 support - refactor https://github.com/opnsense/core/pull/10069

---------

Co-authored-by: Ad Schellevis <ad@opnsense.org>
This commit is contained in:
Monviech 2026-04-02 18:10:12 +02:00 committed by GitHub
parent 45b3d35761
commit 84ec45409d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 118 additions and 21 deletions

1
plist
View file

@ -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

View file

@ -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'];
}

View file

@ -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));

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright (C) 2018 Deciso B.V.
* Copyright (C) 2018-2026 Deciso B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -28,6 +28,8 @@
namespace OPNsense\OpenVPN;
use OPNsense\Core\Backend;
/**
* Export stub file, contains shared logic for all types
* @package OPNsense\Backup
@ -79,4 +81,13 @@ abstract class BaseExporter
openssl_pkcs12_export($crt, $p12, $prv, $pass, $args);
return $p12;
}
/**
* @param string $serverKey single line base64 encoded key
* @return string crypt-v2 client key
*/
protected function export_crypt_v2_client_key(string $serverKey)
{
return trim((new Backend())->configdpRun("openvpn genkey", ['tls-crypt-v2-client', $serverKey]));
}
}

View file

@ -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[] = "</key>";
}
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[] = "<tls-crypt-v2>";
$conf = array_merge($conf, explode("\n", trim(base64_decode($clientKey, true))));
$conf[] = "</tls-crypt-v2>";
} elseif ($this->config['tlsmode'] === 'crypt') {
$conf[] = "<tls-crypt>";
$conf = array_merge($conf, explode("\n", trim(base64_decode($this->config['tls']))));
$conf[] = "</tls-crypt>";

View file

@ -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));

View file

@ -437,6 +437,7 @@
<OptionValues>
<auth>auth (Authenticate control channel packets)</auth>
<crypt>crypt (Encrypt and authenticate all control channel packets)</crypt>
<crypt-v2>crypt-v2 (Encrypt and authenticate all control channel packets)</crypt-v2>
</OptionValues>
</mode>
<key type="TextField">

View file

@ -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') {

View file

@ -0,0 +1,53 @@
#!/usr/local/bin/python3
"""
Copyright (c) 2026 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.
"""
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())

View file

@ -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