console: Provide an RFB security proxy implementation

Instead of doing straight passthrough of the RFB protocol from the
tenant sock to the compute socket, insert an RFB security proxy. This
will MITM the initial RFB protocol handshake in order to negotiate an
authentication scheme with the compute node that is distinct from that
used by the tenant.

Based on earlier work by Solly Ross <sross@redhat.com>

Change-Id: I9cc9a380500715e60bd05aa5c29ee46bc6f8d6c2
Co-authored-by: Stephen Finucane <sfinucan@redhat.com>
Implements: bp websocket-proxy-to-host-security
This commit is contained in:
Daniel P. Berrange 2016-07-21 11:56:52 +01:00 committed by Stephen Finucane
parent c5a1a9e711
commit 30ceaaff5d
6 changed files with 471 additions and 2 deletions

View File

@ -25,6 +25,7 @@ from nova.cmd import baseproxy
import nova.conf import nova.conf
from nova.conf import vnc from nova.conf import vnc
from nova import config from nova import config
from nova.console.securityproxy import rfb
CONF = nova.conf.CONF CONF = nova.conf.CONF
@ -36,6 +37,13 @@ def main():
CONF.set_default('web', '/usr/share/novnc') CONF.set_default('web', '/usr/share/novnc')
config.parse_args(sys.argv) config.parse_args(sys.argv)
# TODO(stephenfin): Always enable the security proxy once we support RFB
# version 3.3, as used in XenServer.
security_proxy = None
if CONF.compute_driver != 'xenapi.XenAPIDriver':
security_proxy = rfb.RFBSecurityProxy()
baseproxy.proxy( baseproxy.proxy(
host=CONF.vnc.novncproxy_host, host=CONF.vnc.novncproxy_host,
port=CONF.vnc.novncproxy_port) port=CONF.vnc.novncproxy_port,
security_proxy=security_proxy)

View File

@ -0,0 +1,190 @@
# 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 struct
from oslo_config import cfg
from oslo_log import log as logging
import six
from nova.console.rfb import auth
from nova.console.rfb import auths
from nova.console.securityproxy import base
from nova import exception
from nova.i18n import _, _LI
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
class RFBSecurityProxy(base.SecurityProxy):
"""RFB Security Proxy Negotiation Helper.
This class proxies the initial setup of the RFB connection between the
client and the server. Then, when the RFB security negotiation step
arrives, it intercepts the communication, posing as a server with the
"None" authentication type to the client, and acting as a client (via
the methods below) to the server. After security negotiation, normal
proxying can be used.
Note: this code mandates RFB version 3.8, since this is supported by any
client and server impl written in the past 10+ years.
See the general RFB specification at:
https://tools.ietf.org/html/rfc6143
"""
def __init__(self):
self.auth_schemes = auths.RFBAuthSchemeList()
def _make_var_str(self, message):
message_str = six.text_type(message)
message_bytes = message_str.encode('utf-8')
message_len = struct.pack("!I", len(message_bytes))
return message_len + message_bytes
def _fail(self, tenant_sock, compute_sock, message):
# Tell the client there's been a problem
result_code = struct.pack("!I", 1)
tenant_sock.sendall(result_code + self._make_var_str(message))
if compute_sock is not None:
# Tell the server that there's been a problem
# by sending the "Invalid" security type
compute_sock.sendall(auth.AUTH_STATUS_FAIL)
def _parse_version(self, version_str):
maj_str = version_str[4:7]
min_str = version_str[8:11]
return float("%d.%d" % (int(maj_str), int(min_str)))
def connect(self, tenant_sock, compute_sock):
"""Initiate the RFB connection process.
This method performs the initial ProtocolVersion
and Security messaging, and returns the socket-like
object to use to communicate with the server securely.
If an error occurs SecurityProxyNegotiationFailed
will be raised.
"""
def recv(sock, num):
b = sock.recv(num)
if len(b) != num:
reason = _("Incorrect read from socket, wanted %(wanted)d "
"bytes but got %(got)d. Socket returned "
"%(result)r") % {'wanted': num, 'got': len(b),
'result': b}
raise exception.RFBAuthHandshakeFailed(reason=reason)
return b
# Negotiate version with compute server
compute_version = recv(compute_sock, auth.VERSION_LENGTH)
LOG.debug("Got version string '%s' from compute node",
compute_version[:-1])
if self._parse_version(compute_version) != 3.8:
reason = _("Security proxying requires RFB protocol "
"version 3.8, but server sent %s"), compute_version[:-1]
raise exception.SecurityProxyNegotiationFailed(reason=reason)
compute_sock.sendall(compute_version)
# Negotiate version with tenant
tenant_sock.sendall(compute_version)
tenant_version = recv(tenant_sock, auth.VERSION_LENGTH)
LOG.debug("Got version string '%s' from tenant",
tenant_version[:-1])
if self._parse_version(tenant_version) != 3.8:
reason = _("Security proxying requires RFB protocol version "
"3.8, but tenant asked for %s"), tenant_version[:-1]
raise exception.SecurityProxyNegotiationFailed(reason=reason)
# Negotiate security with server
permitted_auth_types_cnt = six.byte2int(recv(compute_sock, 1))
if permitted_auth_types_cnt == 0:
reason_len_raw = recv(compute_sock, 4)
reason_len = struct.unpack('!I', reason_len_raw)[0]
reason = recv(compute_sock, reason_len)
tenant_sock.sendall(auth.AUTH_STATUS_FAIL +
reason_len_raw + reason)
raise exception.SecurityProxyNegotiationFailed(reason=reason)
f = recv(compute_sock, permitted_auth_types_cnt)
permitted_auth_types = []
for auth_type in f:
if isinstance(auth_type, six.string_types):
auth_type = ord(auth_type)
permitted_auth_types.append(auth_type)
LOG.debug("The server sent security types %s", permitted_auth_types)
# Negotiate security with client before we say "ok" to the server
# send 1:[None]
tenant_sock.sendall(auth.AUTH_STATUS_PASS +
six.int2byte(auth.AuthType.NONE))
client_auth = six.byte2int(recv(tenant_sock, 1))
if client_auth != auth.AuthType.NONE:
self._fail(tenant_sock, compute_sock,
_("Only the security type None (%d) is supported") %
auth.AuthType.NONE)
reason = _("Client requested a security type other than "
" None (%(none_code)d): "
"%(auth_type)s") % {
'auth_type': client_auth,
'none_code': auth.AuthType.NONE}
raise exception.SecurityProxyNegotiationFailed(reason=reason)
try:
scheme = self.auth_schemes.find_scheme(permitted_auth_types)
except exception.RFBAuthNoAvailableScheme as e:
# Intentionally don't tell client what really failed
# as that's information leakage
self._fail(tenant_sock, compute_sock,
_("Unable to negotiate security with server"))
raise exception.SecurityProxyNegotiationFailed(
reason=_("No compute auth available: %s") % six.text_type(e))
compute_sock.sendall(six.int2byte(scheme.security_type()))
LOG.debug("Using security type %d with server, None with client",
scheme.security_type())
try:
compute_sock = scheme.security_handshake(compute_sock)
except exception.RFBAuthHandshakeFailed as e:
# Intentionally don't tell client what really failed
# as that's information leakage
self._fail(tenant_sock, None,
_("Unable to negotiate security with server"))
LOG.debug("Auth failed %s", six.text_type(e))
raise exception.SecurityProxyNegotiationFailed(
reason="Auth handshake failed")
LOG.info(_LI("Finished security handshake, resuming normal proxy "
"mode using secured socket"))
# we can just proxy the security result -- if the server security
# negotiation fails, we want the client to think it has failed
return compute_sock

View File

@ -80,7 +80,7 @@ class TenantSock(object):
self.reqhandler.send_frames([encodeutils.safe_encode(data)]) self.reqhandler.send_frames([encodeutils.safe_encode(data)])
def finish_up(self): def finish_up(self):
self.reqhandler.send_frames([b''.join([self.queue])]) self.reqhandler.send_frames([b''.join(self.queue)])
def close(self): def close(self):
self.finish_up() self.finish_up()

View File

@ -0,0 +1,248 @@
# 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.
"""Tests the Console Security Proxy Framework."""
import six
import mock
from nova.console.rfb import auth
from nova.console.rfb import authnone
from nova.console.securityproxy import rfb
from nova import exception
from nova import test
class RFBSecurityProxyTestCase(test.NoDBTestCase):
"""Test case for the base RFBSecurityProxy."""
def setUp(self):
super(RFBSecurityProxyTestCase, self).setUp()
self.manager = mock.Mock()
self.tenant_sock = mock.Mock()
self.compute_sock = mock.Mock()
self.tenant_sock.recv.side_effect = []
self.compute_sock.recv.side_effect = []
self.expected_manager_calls = []
self.expected_tenant_calls = []
self.expected_compute_calls = []
self.proxy = rfb.RFBSecurityProxy()
def _assert_expected_calls(self):
self.assertEqual(self.expected_manager_calls,
self.manager.mock_calls)
self.assertEqual(self.expected_tenant_calls,
self.tenant_sock.mock_calls)
self.assertEqual(self.expected_compute_calls,
self.compute_sock.mock_calls)
def _version_handshake(self):
full_version_str = "RFB 003.008\n"
self._expect_compute_recv(auth.VERSION_LENGTH, full_version_str)
self._expect_compute_send(full_version_str)
self._expect_tenant_send(full_version_str)
self._expect_tenant_recv(auth.VERSION_LENGTH, full_version_str)
def _to_binary(self, val):
if type(val) != six.binary_type:
val = six.binary_type(val, 'utf-8')
return val
def _expect_tenant_send(self, val):
val = self._to_binary(val)
self.expected_tenant_calls.append(mock.call.sendall(val))
def _expect_compute_send(self, val):
val = self._to_binary(val)
self.expected_compute_calls.append(mock.call.sendall(val))
def _expect_tenant_recv(self, amt, ret_val):
ret_val = self._to_binary(ret_val)
self.expected_tenant_calls.append(mock.call.recv(amt))
self.tenant_sock.recv.side_effect = (
list(self.tenant_sock.recv.side_effect) + [ret_val])
def _expect_compute_recv(self, amt, ret_val):
ret_val = self._to_binary(ret_val)
self.expected_compute_calls.append(mock.call.recv(amt))
self.compute_sock.recv.side_effect = (
list(self.compute_sock.recv.side_effect) + [ret_val])
def test_fail(self):
self._expect_tenant_send("\x00\x00\x00\x01\x00\x00\x00\x04blah")
self.proxy._fail(self.tenant_sock, None, 'blah')
self._assert_expected_calls()
def test_fail_server_message(self):
self._expect_tenant_send("\x00\x00\x00\x01\x00\x00\x00\x04blah")
self._expect_compute_send("\x00")
self.proxy._fail(self.tenant_sock, self.compute_sock, 'blah')
self._assert_expected_calls()
def test_parse_version(self):
res = self.proxy._parse_version("RFB 012.034\n")
self.assertEqual(12.34, res)
def test_fails_on_compute_version(self):
for full_version_str in ["RFB 003.007\n", "RFB 003.009\n"]:
self._expect_compute_recv(auth.VERSION_LENGTH, full_version_str)
self.assertRaises(exception.SecurityProxyNegotiationFailed,
self.proxy.connect,
self.tenant_sock,
self.compute_sock)
self._assert_expected_calls()
def test_fails_on_tenant_version(self):
full_version_str = "RFB 003.008\n"
for full_version_str_invalid in ["RFB 003.007\n", "RFB 003.009\n"]:
self._expect_compute_recv(auth.VERSION_LENGTH, full_version_str)
self._expect_compute_send(full_version_str)
self._expect_tenant_send(full_version_str)
self._expect_tenant_recv(auth.VERSION_LENGTH,
full_version_str_invalid)
self.assertRaises(exception.SecurityProxyNegotiationFailed,
self.proxy.connect,
self.tenant_sock,
self.compute_sock)
self._assert_expected_calls()
def test_fails_on_sec_type_cnt_zero(self):
self.proxy._fail = mock.Mock()
self._version_handshake()
self._expect_compute_recv(1, "\x00")
self._expect_compute_recv(4, "\x00\x00\x00\x06")
self._expect_compute_recv(6, "cheese")
self._expect_tenant_send("\x00\x00\x00\x00\x06cheese")
self.assertRaises(exception.SecurityProxyNegotiationFailed,
self.proxy.connect,
self.tenant_sock,
self.compute_sock)
self._assert_expected_calls()
@mock.patch.object(authnone.RFBAuthSchemeNone, "security_handshake")
def test_full_run(self, mock_handshake):
new_sock = mock.MagicMock()
mock_handshake.return_value = new_sock
self._version_handshake()
self._expect_compute_recv(1, "\x02")
self._expect_compute_recv(2, "\x01\x02")
self._expect_tenant_send("\x01\x01")
self._expect_tenant_recv(1, "\x01")
self._expect_compute_send("\x01")
self.assertEqual(new_sock, self.proxy.connect(
self.tenant_sock, self.compute_sock))
mock_handshake.assert_called_once_with(self.compute_sock)
self._assert_expected_calls()
def test_client_auth_invalid_fails(self):
self.proxy._fail = self.manager.proxy._fail
self.proxy.security_handshake = self.manager.proxy.security_handshake
self._version_handshake()
self._expect_compute_recv(1, "\x02")
self._expect_compute_recv(2, "\x01\x02")
self._expect_tenant_send("\x01\x01")
self._expect_tenant_recv(1, "\x02")
self.expected_manager_calls.append(
mock.call.proxy._fail(self.tenant_sock,
self.compute_sock,
"Only the security type "
"None (1) is supported"))
self.assertRaises(exception.SecurityProxyNegotiationFailed,
self.proxy.connect,
self.tenant_sock,
self.compute_sock)
self._assert_expected_calls()
def test_exception_in_choose_security_type_fails(self):
self.proxy._fail = self.manager.proxy._fail
self.proxy.security_handshake = self.manager.proxy.security_handshake
self._version_handshake()
self._expect_compute_recv(1, "\x02")
self._expect_compute_recv(2, "\x02\x05")
self._expect_tenant_send("\x01\x01")
self._expect_tenant_recv(1, "\x01")
self.expected_manager_calls.extend([
mock.call.proxy._fail(
self.tenant_sock, self.compute_sock,
'Unable to negotiate security with server')])
self.assertRaises(exception.SecurityProxyNegotiationFailed,
self.proxy.connect,
self.tenant_sock,
self.compute_sock)
self._assert_expected_calls()
@mock.patch.object(authnone.RFBAuthSchemeNone, "security_handshake")
def test_exception_security_handshake_fails(self, mock_auth):
self.proxy._fail = self.manager.proxy._fail
self._version_handshake()
self._expect_compute_recv(1, "\x02")
self._expect_compute_recv(2, "\x01\x02")
self._expect_tenant_send("\x01\x01")
self._expect_tenant_recv(1, "\x01")
self._expect_compute_send("\x01")
ex = exception.RFBAuthHandshakeFailed(reason="crackers")
mock_auth.side_effect = ex
self.expected_manager_calls.extend([
mock.call.proxy._fail(self.tenant_sock, None,
'Unable to negotiate security with server')])
self.assertRaises(exception.SecurityProxyNegotiationFailed,
self.proxy.connect,
self.tenant_sock,
self.compute_sock)
mock_auth.assert_called_once_with(self.compute_sock)
self._assert_expected_calls()

View File

@ -9,3 +9,26 @@ features:
- ``vencrypt_client_key`` - ``vencrypt_client_key``
- ``vencrypt_client_cert`` - ``vencrypt_client_cert``
- ``vencrypt_ca_certs`` - ``vencrypt_ca_certs``
- |
The *nova-novncproxy* server can now be configured to do a security
negotiation with the compute node VNC server. If the VeNCrypt auth scheme
is enabled, this establishes a TLS session to provide encryption of all
data. The proxy will validate the x509 certs issued by the remote server to
ensure it is connecting to a valid compute node. The proxy can also send
its own x509 cert to allow the compute node to validate that the connection
comes from the official proxy server.
upgrade:
- |
To make use of VeNCrypt, configuration steps are required for both the
`nova-novncproxy` service and libvirt on all the compute nodes. The
``/etc/libvirt/qemu.conf`` file should be modified to set the ``vnc_tls``
option to ``1``, and optionally the ``vnc_tls_x509_verify`` option to
``1``. Certificates must also be deployed on the compute node.
The ``nova.conf`` file should have the ``auth_schemes`` parameter in the
``vnc`` group set. If there are a mix of compute nodes, some with VeNCrypt
enabled and others with it disabled, then the ``auth_schemes``
configuration option should be set to ``['vencrypt', 'none']``.
Once all compute nodes have VeNCrypt enabled, the ``auth_schemes``
parameter can be set to just ``['vencrypt']``.