console: introduce the VeNCrypt RFB authentication scheme
Provide an implementation for the VeNCrypt RFB authentication scheme, which uses TLS and x509 certificates to provide both encryption and mutual authentication / authorization. Based on earlier work by Solly Ross <sross@redhat.com> Change-Id: I6a63d2535e86faf369ed1c0eeba6cb5a52252b80 Co-authored-by: Stephen Finucane <sfinucan@redhat.com> Implements: bp websocket-proxy-to-host-security
This commit is contained in:
@@ -217,7 +217,7 @@ Related options:
|
|||||||
cfg.ListOpt(
|
cfg.ListOpt(
|
||||||
'auth_schemes',
|
'auth_schemes',
|
||||||
item_type=types.String(
|
item_type=types.String(
|
||||||
choices=['none']
|
choices=['none', 'vencrypt']
|
||||||
),
|
),
|
||||||
default=['none'],
|
default=['none'],
|
||||||
help="""
|
help="""
|
||||||
@@ -231,6 +231,41 @@ first.
|
|||||||
Possible values:
|
Possible values:
|
||||||
|
|
||||||
* "none": allow connection without authentication
|
* "none": allow connection without authentication
|
||||||
|
"""),
|
||||||
|
cfg.StrOpt(
|
||||||
|
'vencrypt_client_key',
|
||||||
|
help="""The path to the client certificate PEM file (for x509)
|
||||||
|
|
||||||
|
The fully qualified path to a PEM file containing the private key which the VNC
|
||||||
|
proxy server presents to the compute node during VNC authentication.
|
||||||
|
|
||||||
|
Related options:
|
||||||
|
|
||||||
|
* ``vnc.auth_schemes``: must include "vencrypt"
|
||||||
|
* ``vnc.vencrypt_client_cert```: must also be set
|
||||||
|
"""),
|
||||||
|
cfg.StrOpt(
|
||||||
|
'vencrypt_client_cert',
|
||||||
|
help="""The path to the client key file (for x509)
|
||||||
|
|
||||||
|
The fully qualified path to a PEM file containing the x509 certificate which
|
||||||
|
the VNC proxy server presents to the compute node during VNC authentication.
|
||||||
|
|
||||||
|
Realted options:
|
||||||
|
|
||||||
|
* ``vnc.auth_schemes``: must include "vencrypt"
|
||||||
|
* ``vnc.vencrypt_client_key```: must also be set
|
||||||
|
"""),
|
||||||
|
cfg.StrOpt(
|
||||||
|
'vencrypt_ca_certs',
|
||||||
|
help="""The path to the CA certificate PEM file
|
||||||
|
|
||||||
|
The fully qualified path to a PEM file containing one or more x509 certificates
|
||||||
|
for the certificate authorities used by the compute node VNC server.
|
||||||
|
|
||||||
|
Related options:
|
||||||
|
|
||||||
|
* ``vnc.auth_schemes``: must include "vencrypt"
|
||||||
"""),
|
"""),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import enum
|
|||||||
import six
|
import six
|
||||||
|
|
||||||
VERSION_LENGTH = 12
|
VERSION_LENGTH = 12
|
||||||
|
SUBTYPE_LENGTH = 4
|
||||||
|
|
||||||
AUTH_STATUS_FAIL = b"\x00"
|
AUTH_STATUS_FAIL = b"\x00"
|
||||||
AUTH_STATUS_PASS = b"\x01"
|
AUTH_STATUS_PASS = b"\x01"
|
||||||
@@ -39,6 +40,19 @@ class AuthType(enum.IntEnum):
|
|||||||
MSLOGON = 0xfffffffa # Used by UltraVNC
|
MSLOGON = 0xfffffffa # Used by UltraVNC
|
||||||
|
|
||||||
|
|
||||||
|
class AuthVeNCryptSubtype(enum.IntEnum):
|
||||||
|
|
||||||
|
PLAIN = 256
|
||||||
|
TLSNONE = 257
|
||||||
|
TLSVNC = 258
|
||||||
|
TLSPLAIN = 259
|
||||||
|
X509NONE = 260
|
||||||
|
X509VNC = 261
|
||||||
|
X509PLAIN = 262
|
||||||
|
X509SASL = 263
|
||||||
|
TLSSASL = 264
|
||||||
|
|
||||||
|
|
||||||
@six.add_metaclass(abc.ABCMeta)
|
@six.add_metaclass(abc.ABCMeta)
|
||||||
class RFBAuthScheme(object):
|
class RFBAuthScheme(object):
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
|
||||||
from nova.console.rfb import authnone
|
from nova.console.rfb import authnone
|
||||||
|
from nova.console.rfb import authvencrypt
|
||||||
from nova import exception
|
from nova import exception
|
||||||
|
|
||||||
CONF = cfg.CONF
|
CONF = cfg.CONF
|
||||||
@@ -24,6 +25,7 @@ class RFBAuthSchemeList(object):
|
|||||||
|
|
||||||
AUTH_SCHEME_MAP = {
|
AUTH_SCHEME_MAP = {
|
||||||
"none": authnone.RFBAuthSchemeNone,
|
"none": authnone.RFBAuthSchemeNone,
|
||||||
|
"vencrypt": authvencrypt.RFBAuthSchemeVeNCrypt,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|||||||
128
nova/console/rfb/authvencrypt.py
Normal file
128
nova/console/rfb/authvencrypt.py
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Copyright (c) 2014-2016 Red Hat, Inc
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import ssl
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from oslo_config import cfg
|
||||||
|
from oslo_log import log as logging
|
||||||
|
|
||||||
|
from nova.console.rfb import auth
|
||||||
|
from nova import exception
|
||||||
|
from nova.i18n import _, _LI
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
CONF = cfg.CONF
|
||||||
|
|
||||||
|
|
||||||
|
class RFBAuthSchemeVeNCrypt(auth.RFBAuthScheme):
|
||||||
|
"""A security proxy helper which uses VeNCrypt.
|
||||||
|
|
||||||
|
This security proxy helper uses the VeNCrypt security
|
||||||
|
type to achieve SSL/TLS-secured VNC. It supports both
|
||||||
|
standard SSL/TLS encryption and SSL/TLS encryption with
|
||||||
|
x509 authentication.
|
||||||
|
|
||||||
|
Refer to https://www.berrange.com/~dan/vencrypt.txt for
|
||||||
|
a brief overview of the protocol.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def security_type(self):
|
||||||
|
return auth.AuthType.VENCRYPT
|
||||||
|
|
||||||
|
def security_handshake(self, compute_sock):
|
||||||
|
def recv(num):
|
||||||
|
b = compute_sock.recv(num)
|
||||||
|
if len(b) != num:
|
||||||
|
reason = _("Short read from compute socket, wanted "
|
||||||
|
"%(wanted)d bytes but got %(got)d") % {
|
||||||
|
'wanted': num, 'got': len(b)}
|
||||||
|
raise exception.RFBAuthHandshakeFailed(reason=reason)
|
||||||
|
return b
|
||||||
|
|
||||||
|
# get the VeNCrypt version from the server
|
||||||
|
maj_ver = ord(recv(1))
|
||||||
|
min_ver = ord(recv(1))
|
||||||
|
|
||||||
|
LOG.debug("Server sent VeNCrypt version "
|
||||||
|
"%(maj)s.%(min)s", {'maj': maj_ver, 'min': min_ver})
|
||||||
|
|
||||||
|
if maj_ver != 0 or min_ver != 2:
|
||||||
|
reason = _("Only VeNCrypt version 0.2 is supported by this "
|
||||||
|
"proxy, but the server wanted to use version "
|
||||||
|
"%(maj)s.%(min)s") % {'maj': maj_ver, 'min': min_ver}
|
||||||
|
raise exception.RFBAuthHandshakeFailed(reason=reason)
|
||||||
|
|
||||||
|
# use version 0.2
|
||||||
|
compute_sock.sendall(b"\x00\x02")
|
||||||
|
|
||||||
|
can_use_version = ord(recv(1))
|
||||||
|
|
||||||
|
if can_use_version > 0:
|
||||||
|
reason = _("Server could not use VeNCrypt version 0.2")
|
||||||
|
raise exception.RFBAuthHandshakeFailed(reason=reason)
|
||||||
|
|
||||||
|
# get the supported sub-auth types
|
||||||
|
sub_types_cnt = ord(recv(1))
|
||||||
|
sub_types_raw = recv(sub_types_cnt * auth.SUBTYPE_LENGTH)
|
||||||
|
sub_types = struct.unpack('!' + str(sub_types_cnt) + 'I',
|
||||||
|
sub_types_raw)
|
||||||
|
|
||||||
|
LOG.debug("Server supports VeNCrypt sub-types %s", sub_types)
|
||||||
|
|
||||||
|
if auth.AuthVeNCryptSubtype.X509NONE not in sub_types:
|
||||||
|
reason = _("Server does not support the x509None (%s) VeNCrypt"
|
||||||
|
" sub-auth type") % \
|
||||||
|
auth.AuthVeNCryptSubtype.X509NONE
|
||||||
|
raise exception.RFBAuthHandshakeFailed(reason=reason)
|
||||||
|
|
||||||
|
LOG.debug("Attempting to use the x509None (%s) auth sub-type",
|
||||||
|
auth.AuthVeNCryptSubtype.X509NONE)
|
||||||
|
|
||||||
|
compute_sock.sendall(struct.pack(
|
||||||
|
'!I', auth.AuthVeNCryptSubtype.X509NONE))
|
||||||
|
|
||||||
|
# NB(sross): the spec is missing a U8 here that's used in
|
||||||
|
# multiple implementations (e.g. QEMU, GTK-VNC). 1 means
|
||||||
|
# acceptance, 0 means failure (unlike the rest of RFB)
|
||||||
|
auth_accepted = ord(recv(1))
|
||||||
|
if auth_accepted == 0:
|
||||||
|
reason = _("Server didn't accept the requested auth sub-type")
|
||||||
|
raise exception.RFBAuthHandshakeFailed(reason=reason)
|
||||||
|
|
||||||
|
LOG.debug("Server accepted the requested sub-auth type")
|
||||||
|
|
||||||
|
if (CONF.vnc.vencrypt_client_key and
|
||||||
|
CONF.vnc.vencrypt_client_cert):
|
||||||
|
client_key = CONF.vnc.vencrypt_client_key
|
||||||
|
client_cert = CONF.vnc.vencrypt_client_cert
|
||||||
|
else:
|
||||||
|
client_key = None
|
||||||
|
client_cert = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
wrapped_sock = ssl.wrap_socket(
|
||||||
|
compute_sock,
|
||||||
|
keyfile=client_key,
|
||||||
|
certfile=client_cert,
|
||||||
|
server_side=False,
|
||||||
|
cert_reqs=ssl.CERT_REQUIRED,
|
||||||
|
ca_certs=CONF.vnc.vencrypt_ca_certs)
|
||||||
|
|
||||||
|
LOG.info(_LI("VeNCrypt security handshake accepted"))
|
||||||
|
return wrapped_sock
|
||||||
|
|
||||||
|
except ssl.SSLError as e:
|
||||||
|
reason = _("Error establishing TLS connection to server: %s") % e
|
||||||
|
raise exception.RFBAuthHandshakeFailed(reason=reason)
|
||||||
@@ -26,13 +26,14 @@ class RFBAuthSchemeListTestCase(test.NoDBTestCase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(RFBAuthSchemeListTestCase, self).setUp()
|
super(RFBAuthSchemeListTestCase, self).setUp()
|
||||||
|
|
||||||
self.flags(auth_schemes=["none"], group="vnc")
|
self.flags(auth_schemes=["none", "vencrypt"], group="vnc")
|
||||||
|
|
||||||
def test_load_ok(self):
|
def test_load_ok(self):
|
||||||
schemelist = auths.RFBAuthSchemeList()
|
schemelist = auths.RFBAuthSchemeList()
|
||||||
|
|
||||||
security_types = sorted(schemelist.schemes.keys())
|
security_types = sorted(schemelist.schemes.keys())
|
||||||
self.assertEqual(security_types, [auth.AuthType.NONE])
|
self.assertEqual(security_types, [auth.AuthType.NONE,
|
||||||
|
auth.AuthType.VENCRYPT])
|
||||||
|
|
||||||
def test_load_unknown(self):
|
def test_load_unknown(self):
|
||||||
"""Ensure invalid auth schemes are not supported.
|
"""Ensure invalid auth schemes are not supported.
|
||||||
|
|||||||
203
nova/tests/unit/console/rfb/test_authvencrypt.py
Normal file
203
nova/tests/unit/console/rfb/test_authvencrypt.py
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
# Copyright (c) 2016 Red Hat, Inc
|
||||||
|
#
|
||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import ssl
|
||||||
|
import struct
|
||||||
|
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from nova.console.rfb import auth
|
||||||
|
from nova.console.rfb import authvencrypt
|
||||||
|
from nova import exception
|
||||||
|
from nova import test
|
||||||
|
|
||||||
|
|
||||||
|
class RFBAuthSchemeVeNCryptTestCase(test.NoDBTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(RFBAuthSchemeVeNCryptTestCase, self).setUp()
|
||||||
|
|
||||||
|
self.scheme = authvencrypt.RFBAuthSchemeVeNCrypt()
|
||||||
|
self.compute_sock = mock.MagicMock()
|
||||||
|
|
||||||
|
self.compute_sock.recv.side_effect = []
|
||||||
|
|
||||||
|
self.expected_calls = []
|
||||||
|
|
||||||
|
self.flags(vencrypt_ca_certs="/certs/ca.pem", group="vnc")
|
||||||
|
|
||||||
|
def _expect_send(self, val):
|
||||||
|
self.expected_calls.append(mock.call.sendall(val))
|
||||||
|
|
||||||
|
def _expect_recv(self, amt, ret_val):
|
||||||
|
self.expected_calls.append(mock.call.recv(amt))
|
||||||
|
self.compute_sock.recv.side_effect = (
|
||||||
|
list(self.compute_sock.recv.side_effect) + [ret_val])
|
||||||
|
|
||||||
|
@mock.patch.object(ssl, "wrap_socket", return_value="wrapped")
|
||||||
|
def test_security_handshake_with_x509(self, mock_socket):
|
||||||
|
self.flags(vencrypt_client_key='/certs/keyfile',
|
||||||
|
vencrypt_client_cert='/certs/cert.pem',
|
||||||
|
group="vnc")
|
||||||
|
|
||||||
|
self._expect_recv(1, "\x00")
|
||||||
|
self._expect_recv(1, "\x02")
|
||||||
|
|
||||||
|
self._expect_send(b"\x00\x02")
|
||||||
|
self._expect_recv(1, "\x00")
|
||||||
|
|
||||||
|
self._expect_recv(1, "\x02")
|
||||||
|
subtypes_raw = [auth.AuthVeNCryptSubtype.X509NONE,
|
||||||
|
auth.AuthVeNCryptSubtype.X509VNC]
|
||||||
|
subtypes = struct.pack('!2I', *subtypes_raw)
|
||||||
|
self._expect_recv(8, subtypes)
|
||||||
|
|
||||||
|
self._expect_send(struct.pack('!I', subtypes_raw[0]))
|
||||||
|
|
||||||
|
self._expect_recv(1, "\x01")
|
||||||
|
|
||||||
|
self.assertEqual("wrapped", self.scheme.security_handshake(
|
||||||
|
self.compute_sock))
|
||||||
|
|
||||||
|
mock_socket.assert_called_once_with(
|
||||||
|
self.compute_sock,
|
||||||
|
keyfile='/certs/keyfile',
|
||||||
|
certfile='/certs/cert.pem',
|
||||||
|
server_side=False,
|
||||||
|
cert_reqs=ssl.CERT_REQUIRED,
|
||||||
|
ca_certs='/certs/ca.pem')
|
||||||
|
|
||||||
|
self.assertEqual(self.expected_calls, self.compute_sock.mock_calls)
|
||||||
|
|
||||||
|
@mock.patch.object(ssl, "wrap_socket", return_value="wrapped")
|
||||||
|
def test_security_handshake_without_x509(self, mock_socket):
|
||||||
|
self._expect_recv(1, "\x00")
|
||||||
|
self._expect_recv(1, "\x02")
|
||||||
|
|
||||||
|
self._expect_send(b"\x00\x02")
|
||||||
|
self._expect_recv(1, "\x00")
|
||||||
|
|
||||||
|
self._expect_recv(1, "\x02")
|
||||||
|
subtypes_raw = [auth.AuthVeNCryptSubtype.X509NONE,
|
||||||
|
auth.AuthVeNCryptSubtype.X509VNC]
|
||||||
|
subtypes = struct.pack('!2I', *subtypes_raw)
|
||||||
|
self._expect_recv(8, subtypes)
|
||||||
|
|
||||||
|
self._expect_send(struct.pack('!I', subtypes_raw[0]))
|
||||||
|
|
||||||
|
self._expect_recv(1, "\x01")
|
||||||
|
|
||||||
|
self.assertEqual("wrapped", self.scheme.security_handshake(
|
||||||
|
self.compute_sock))
|
||||||
|
mock_socket.assert_called_once_with(
|
||||||
|
self.compute_sock,
|
||||||
|
keyfile=None,
|
||||||
|
certfile=None,
|
||||||
|
server_side=False,
|
||||||
|
cert_reqs=ssl.CERT_REQUIRED,
|
||||||
|
ca_certs='/certs/ca.pem'
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(self.expected_calls, self.compute_sock.mock_calls)
|
||||||
|
|
||||||
|
def _test_security_handshake_fails(self):
|
||||||
|
self.assertRaises(exception.RFBAuthHandshakeFailed,
|
||||||
|
self.scheme.security_handshake,
|
||||||
|
self.compute_sock)
|
||||||
|
self.assertEqual(self.expected_calls, self.compute_sock.mock_calls)
|
||||||
|
|
||||||
|
def test_security_handshake_fails_on_low_version(self):
|
||||||
|
self._expect_recv(1, "\x00")
|
||||||
|
self._expect_recv(1, "\x01")
|
||||||
|
|
||||||
|
self._test_security_handshake_fails()
|
||||||
|
|
||||||
|
def test_security_handshake_fails_on_cant_use_version(self):
|
||||||
|
self._expect_recv(1, "\x00")
|
||||||
|
self._expect_recv(1, "\x02")
|
||||||
|
|
||||||
|
self._expect_send(b"\x00\x02")
|
||||||
|
self._expect_recv(1, "\x01")
|
||||||
|
|
||||||
|
self._test_security_handshake_fails()
|
||||||
|
|
||||||
|
def test_security_handshake_fails_on_missing_subauth(self):
|
||||||
|
self._expect_recv(1, "\x00")
|
||||||
|
self._expect_recv(1, "\x02")
|
||||||
|
|
||||||
|
self._expect_send(b"\x00\x02")
|
||||||
|
self._expect_recv(1, "\x00")
|
||||||
|
|
||||||
|
self._expect_recv(1, "\x01")
|
||||||
|
subtypes_raw = [auth.AuthVeNCryptSubtype.X509VNC]
|
||||||
|
subtypes = struct.pack('!I', *subtypes_raw)
|
||||||
|
self._expect_recv(4, subtypes)
|
||||||
|
|
||||||
|
self._test_security_handshake_fails()
|
||||||
|
|
||||||
|
def test_security_handshake_fails_on_auth_not_accepted(self):
|
||||||
|
self._expect_recv(1, "\x00")
|
||||||
|
self._expect_recv(1, "\x02")
|
||||||
|
|
||||||
|
self._expect_send(b"\x00\x02")
|
||||||
|
self._expect_recv(1, "\x00")
|
||||||
|
|
||||||
|
self._expect_recv(1, "\x02")
|
||||||
|
subtypes_raw = [auth.AuthVeNCryptSubtype.X509NONE,
|
||||||
|
auth.AuthVeNCryptSubtype.X509VNC]
|
||||||
|
subtypes = struct.pack('!2I', *subtypes_raw)
|
||||||
|
self._expect_recv(8, subtypes)
|
||||||
|
|
||||||
|
self._expect_send(struct.pack('!I', subtypes_raw[0]))
|
||||||
|
|
||||||
|
self._expect_recv(1, "\x00")
|
||||||
|
|
||||||
|
self._test_security_handshake_fails()
|
||||||
|
|
||||||
|
@mock.patch.object(ssl, "wrap_socket")
|
||||||
|
def test_security_handshake_fails_on_ssl_failure(self, mock_socket):
|
||||||
|
self._expect_recv(1, "\x00")
|
||||||
|
self._expect_recv(1, "\x02")
|
||||||
|
|
||||||
|
self._expect_send(b"\x00\x02")
|
||||||
|
self._expect_recv(1, "\x00")
|
||||||
|
|
||||||
|
self._expect_recv(1, "\x02")
|
||||||
|
subtypes_raw = [auth.AuthVeNCryptSubtype.X509NONE,
|
||||||
|
auth.AuthVeNCryptSubtype.X509VNC]
|
||||||
|
subtypes = struct.pack('!2I', *subtypes_raw)
|
||||||
|
self._expect_recv(8, subtypes)
|
||||||
|
|
||||||
|
self._expect_send(struct.pack('!I', subtypes_raw[0]))
|
||||||
|
|
||||||
|
self._expect_recv(1, "\x01")
|
||||||
|
|
||||||
|
mock_socket.side_effect = ssl.SSLError("cheese")
|
||||||
|
|
||||||
|
self._test_security_handshake_fails()
|
||||||
|
|
||||||
|
mock_socket.assert_called_once_with(
|
||||||
|
self.compute_sock,
|
||||||
|
keyfile=None,
|
||||||
|
certfile=None,
|
||||||
|
server_side=False,
|
||||||
|
cert_reqs=ssl.CERT_REQUIRED,
|
||||||
|
ca_certs='/certs/ca.pem'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_types(self):
|
||||||
|
scheme = authvencrypt.RFBAuthSchemeVeNCrypt()
|
||||||
|
|
||||||
|
self.assertEqual(auth.AuthType.VENCRYPT,
|
||||||
|
scheme.security_type())
|
||||||
@@ -6,3 +6,6 @@ features:
|
|||||||
*nova-novncproxy* server and the compute node VNC server.
|
*nova-novncproxy* server and the compute node VNC server.
|
||||||
|
|
||||||
- ``auth_schemes``
|
- ``auth_schemes``
|
||||||
|
- ``vencrypt_client_key``
|
||||||
|
- ``vencrypt_client_cert``
|
||||||
|
- ``vencrypt_ca_certs``
|
||||||
|
|||||||
Reference in New Issue
Block a user