From 3c7770f1af10728fbaecffce526d6ffbb3e112a9 Mon Sep 17 00:00:00 2001 From: Stephen Finucane Date: Fri, 21 Jul 2017 11:21:01 +0100 Subject: [PATCH] console: introduce framework for RFB authentication Introduce a framework for providing RFB authentication scheme implementations. This will be later used by the websocket RFB security proxy code. Change-Id: I98403ca922b83a460a4e7baa12bd5f596a79c940 Co-authored-by: Stephen Finucane Implements: bp websocket-proxy-to-host-security --- nova/conf/vnc.py | 19 +++++ nova/console/rfb/__init__.py | 0 nova/console/rfb/auth.py | 64 +++++++++++++++++ nova/console/rfb/authnone.py | 24 +++++++ nova/console/rfb/auths.py | 44 ++++++++++++ nova/exception.py | 9 +++ nova/tests/unit/console/rfb/__init__.py | 0 nova/tests/unit/console/rfb/test_auth.py | 72 +++++++++++++++++++ nova/tests/unit/console/rfb/test_authnone.py | 36 ++++++++++ ...oxy-to-host-security-c3eca0647b0cbc02.yaml | 8 +++ 10 files changed, 276 insertions(+) create mode 100644 nova/console/rfb/__init__.py create mode 100644 nova/console/rfb/auth.py create mode 100644 nova/console/rfb/authnone.py create mode 100644 nova/console/rfb/auths.py create mode 100644 nova/tests/unit/console/rfb/__init__.py create mode 100644 nova/tests/unit/console/rfb/test_auth.py create mode 100644 nova/tests/unit/console/rfb/test_authnone.py create mode 100644 releasenotes/notes/websocket-proxy-to-host-security-c3eca0647b0cbc02.yaml diff --git a/nova/conf/vnc.py b/nova/conf/vnc.py index e62afefd89..cda136b1b8 100644 --- a/nova/conf/vnc.py +++ b/nova/conf/vnc.py @@ -14,6 +14,7 @@ # under the License. from oslo_config import cfg +from oslo_config import types vnc_group = cfg.OptGroup( 'vnc', @@ -212,6 +213,24 @@ Related options: * novncproxy_host * novncproxy_base_url +"""), + cfg.ListOpt( + 'auth_schemes', + item_type=types.String( + choices=['none'] + ), + default=['none'], + help=""" +The authentication schemes to use with the compute node. + +Control what RFB authentication schemes are permitted for connections between +the proxy and the compute host. If multiple schemes are enabled, the first +matching scheme will be used, thus the strongest schemes should be listed +first. + +Possible values: + +* "none": allow connection without authentication """), ] diff --git a/nova/console/rfb/__init__.py b/nova/console/rfb/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nova/console/rfb/auth.py b/nova/console/rfb/auth.py new file mode 100644 index 0000000000..39e06b93f0 --- /dev/null +++ b/nova/console/rfb/auth.py @@ -0,0 +1,64 @@ +# Copyright (c) 2014-2017 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 abc +import enum + +import six + +VERSION_LENGTH = 12 + +AUTH_STATUS_FAIL = b"\x00" +AUTH_STATUS_PASS = b"\x01" + + +class AuthType(enum.IntEnum): + + INVALID = 0 + NONE = 1 + VNC = 2 + RA2 = 5 + RA2NE = 6 + TIGHT = 16 + ULTRA = 17 + TLS = 18 # Used by VINO + VENCRYPT = 19 # Used by VeNCrypt and QEMU + SASL = 20 # SASL type used by VINO and QEMU + ARD = 30 # Apple remote desktop (screen sharing) + MSLOGON = 0xfffffffa # Used by UltraVNC + + +@six.add_metaclass(abc.ABCMeta) +class RFBAuthScheme(object): + + @abc.abstractmethod + def security_type(self): + """Return the security type supported by this scheme + + Returns the nova.console.rfb.auth.AuthType.XX + constant representing the scheme implemented. + """ + pass + + @abc.abstractmethod + def security_handshake(self, compute_sock): + """Perform security-type-specific functionality. + + This method is expected to return the socket-like + object used to communicate with the server securely. + + Should raise exception.RFBAuthHandshakeFailed if + an error occurs + """ + pass diff --git a/nova/console/rfb/authnone.py b/nova/console/rfb/authnone.py new file mode 100644 index 0000000000..1ec359fe64 --- /dev/null +++ b/nova/console/rfb/authnone.py @@ -0,0 +1,24 @@ +# 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. + +from nova.console.rfb import auth + + +class RFBAuthSchemeNone(auth.RFBAuthScheme): + + def security_type(self): + return auth.AuthType.NONE + + def security_handshake(self, compute_sock): + return compute_sock diff --git a/nova/console/rfb/auths.py b/nova/console/rfb/auths.py new file mode 100644 index 0000000000..62f73e703c --- /dev/null +++ b/nova/console/rfb/auths.py @@ -0,0 +1,44 @@ +# Copyright (c) 2014-2017 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. + +from oslo_config import cfg + +from nova.console.rfb import authnone +from nova import exception + +CONF = cfg.CONF + + +class RFBAuthSchemeList(object): + + AUTH_SCHEME_MAP = { + "none": authnone.RFBAuthSchemeNone, + } + + def __init__(self): + self.schemes = {} + + for name in CONF.vnc.auth_schemes: + scheme = self.AUTH_SCHEME_MAP[name]() + + self.schemes[scheme.security_type()] = scheme + + def find_scheme(self, desired_types): + for security_type in desired_types: + if security_type in self.schemes: + return self.schemes[security_type] + + raise exception.RFBAuthNoAvailableScheme( + allowed_types=", ".join([str(s) for s in self.schemes.keys()]), + desired_types=", ".join([str(s) for s in desired_types])) diff --git a/nova/exception.py b/nova/exception.py index 799cd07b09..04f25e3569 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -1767,6 +1767,15 @@ class SecurityProxyNegotiationFailed(NovaException): msg_fmt = _("Failed to negotiate security type with server: %(reason)s") +class RFBAuthHandshakeFailed(NovaException): + msg_fmt = _("Failed to complete auth handshake: %(reason)s") + + +class RFBAuthNoAvailableScheme(NovaException): + msg_fmt = _("No matching auth scheme: allowed types: '%(allowed_types)s', " + "desired types: '%(desired_types)s'") + + class InvalidWatchdogAction(Invalid): msg_fmt = _("Provided watchdog action (%(action)s) is not supported.") diff --git a/nova/tests/unit/console/rfb/__init__.py b/nova/tests/unit/console/rfb/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/nova/tests/unit/console/rfb/test_auth.py b/nova/tests/unit/console/rfb/test_auth.py new file mode 100644 index 0000000000..72912c8880 --- /dev/null +++ b/nova/tests/unit/console/rfb/test_auth.py @@ -0,0 +1,72 @@ +# 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 mock + +from nova.console.rfb import auth +from nova.console.rfb import authnone +from nova.console.rfb import auths +from nova import exception +from nova import test + + +class RFBAuthSchemeListTestCase(test.NoDBTestCase): + + def setUp(self): + super(RFBAuthSchemeListTestCase, self).setUp() + + self.flags(auth_schemes=["none"], group="vnc") + + def test_load_ok(self): + schemelist = auths.RFBAuthSchemeList() + + security_types = sorted(schemelist.schemes.keys()) + self.assertEqual(security_types, [auth.AuthType.NONE]) + + def test_load_unknown(self): + """Ensure invalid auth schemes are not supported. + + We're really testing oslo_policy functionality, but this case is + esoteric enough to warrant this. + """ + self.assertRaises(ValueError, self.flags, + auth_schemes=['none', 'wibble'], group='vnc') + + def test_find_scheme_ok(self): + schemelist = auths.RFBAuthSchemeList() + + scheme = schemelist.find_scheme( + [auth.AuthType.TIGHT, + auth.AuthType.NONE]) + + self.assertIsInstance(scheme, authnone.RFBAuthSchemeNone) + + def test_find_scheme_fail(self): + schemelist = auths.RFBAuthSchemeList() + + self.assertRaises(exception.RFBAuthNoAvailableScheme, + schemelist.find_scheme, + [auth.AuthType.TIGHT]) + + def test_find_scheme_priority(self): + schemelist = auths.RFBAuthSchemeList() + + tight = mock.MagicMock(spec=auth.RFBAuthScheme) + schemelist.schemes[auth.AuthType.TIGHT] = tight + + scheme = schemelist.find_scheme( + [auth.AuthType.TIGHT, + auth.AuthType.NONE]) + + self.assertEqual(tight, scheme) diff --git a/nova/tests/unit/console/rfb/test_authnone.py b/nova/tests/unit/console/rfb/test_authnone.py new file mode 100644 index 0000000000..e628106e3b --- /dev/null +++ b/nova/tests/unit/console/rfb/test_authnone.py @@ -0,0 +1,36 @@ +# Copyright (c) 2014-2016 Red Hat, Inc +# All Rights Reserved. +# +# 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 mock + +from nova.console.rfb import auth +from nova.console.rfb import authnone +from nova import test + + +class RFBAuthSchemeNoneTestCase(test.NoDBTestCase): + + def test_handshake(self): + scheme = authnone.RFBAuthSchemeNone() + + sock = mock.MagicMock() + ret = scheme.security_handshake(sock) + + self.assertEqual(sock, ret) + + def test_types(self): + scheme = authnone.RFBAuthSchemeNone() + + self.assertEqual(auth.AuthType.NONE, scheme.security_type()) diff --git a/releasenotes/notes/websocket-proxy-to-host-security-c3eca0647b0cbc02.yaml b/releasenotes/notes/websocket-proxy-to-host-security-c3eca0647b0cbc02.yaml new file mode 100644 index 0000000000..1d79f5836c --- /dev/null +++ b/releasenotes/notes/websocket-proxy-to-host-security-c3eca0647b0cbc02.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added a number of new configuration options to the ``[vnc]`` group, which + together allow for the configuration of authentication used between the + *nova-novncproxy* server and the compute node VNC server. + + - ``auth_schemes``