enforce remote console shutdown
- Adds a CONF option enforce_session_timeout - Adds Timer to close connection once token expire - refactor close_connection functionality - Fixes existing and adds new unit tests - Adds release note - Updates admin guide Change-Id: I5d7e8faf1d271e9dd98d24e825631246308e7141
This commit is contained in:
parent
3209f65516
commit
5ecf1d324d
@ -69,6 +69,31 @@ This particular example is illustrated below.
|
||||
:alt: noVNC process
|
||||
:width: 95%
|
||||
|
||||
Consoleauth configuration:
|
||||
--------------------------
|
||||
|
||||
The consoleauth accepts following options:
|
||||
|
||||
- :oslo.config:option:`consoleauth.token_ttl`
|
||||
- :oslo.config:option:`consoleauth.enforce_session_timeout`
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[consoleauth]
|
||||
token_ttl = 1000 # default value is 600 second
|
||||
enforce_session_timeout = True # default is False
|
||||
|
||||
|
||||
Supported consoles:
|
||||
-------------------
|
||||
|
||||
* :ref:`vnc-console`
|
||||
* :ref:`spice-console`
|
||||
* :ref:`serial-console`
|
||||
* :ref:`mks-console`
|
||||
|
||||
|
||||
.. _vnc-console:
|
||||
|
||||
noVNC-based VNC console
|
||||
-----------------------
|
||||
@ -294,6 +319,8 @@ be told where to find them. This requires editing :file:`nova.conf` to set.
|
||||
vencrypt_ca_certs=/etc/pki/nova-novncproxy/ca-cert.pem
|
||||
|
||||
|
||||
.. _spice-console:
|
||||
|
||||
SPICE console
|
||||
-------------
|
||||
|
||||
@ -376,9 +403,10 @@ for SPICE consoles.
|
||||
- :oslo.config:option:`spice.playback_compression`
|
||||
- :oslo.config:option:`spice.streaming_mode`
|
||||
|
||||
.. _serial-console:
|
||||
|
||||
Serial
|
||||
------
|
||||
Serial console
|
||||
--------------
|
||||
|
||||
Serial consoles provide an alternative to graphical consoles like VNC or SPICE.
|
||||
They work a little differently to graphical consoles so an example is
|
||||
@ -468,9 +496,10 @@ There are some things to keep in mind when configuring these options:
|
||||
:program:`nova-serialproxy` service to determine where to connect to for
|
||||
proxying the console interaction.
|
||||
|
||||
.. _mks-console:
|
||||
|
||||
MKS
|
||||
---
|
||||
MKS console
|
||||
-----------
|
||||
|
||||
MKS is the protocol used for accessing the console of a virtual machine running
|
||||
on VMware vSphere. It is very similar to VNC. Due to the architecture of the
|
||||
@ -576,6 +605,13 @@ Frequently Asked Questions
|
||||
console connections, make sure that the value of ``novncproxy_base_url`` is
|
||||
set explicitly where the ``nova-novncproxy`` service is running.
|
||||
|
||||
- **Q: How do I know which nova config file to update to set a particular config option?**
|
||||
|
||||
A: First, find out which nova-service is responsible for the change you want
|
||||
to make, using ``ps -aux | grep nova``. Once the service is found, check the
|
||||
status of the service via systemctl. In the status output, associated conf
|
||||
files with respective paths will be listed.
|
||||
|
||||
|
||||
References
|
||||
----------
|
||||
|
@ -32,7 +32,18 @@ The lifetime of a console auth token (in seconds).
|
||||
A console auth token is used in authorizing console access for a user.
|
||||
Once the auth token time to live count has elapsed, the token is
|
||||
considered expired. Expired tokens are then deleted.
|
||||
""")
|
||||
"""),
|
||||
cfg.BoolOpt(
|
||||
'enforce_session_timeout',
|
||||
default=False,
|
||||
help="""
|
||||
Enable or disable enforce session timeout for VM console.
|
||||
|
||||
This allows operators to enforce a console session timeout.
|
||||
When set to True, Nova will automatically close the console session
|
||||
at the server end once token_ttl expires, providing enhanced
|
||||
control over console session duration.
|
||||
"""),
|
||||
]
|
||||
|
||||
|
||||
|
@ -37,6 +37,9 @@ from nova import exception
|
||||
from nova.i18n import _
|
||||
from nova import objects
|
||||
|
||||
from oslo_utils import timeutils
|
||||
import threading
|
||||
|
||||
# Location of WebSockifyServer class in websockify v0.9.0
|
||||
websockifyserver = importutils.try_import('websockify.websockifyserver')
|
||||
|
||||
@ -147,6 +150,20 @@ class NovaProxyRequestHandler(websockify.ProxyRequestHandler):
|
||||
|
||||
return connect_info
|
||||
|
||||
def _close_connection(self, tsock, host, port):
|
||||
"""takes target socket and close the connection.
|
||||
"""
|
||||
try:
|
||||
tsock.shutdown(socket.SHUT_RDWR)
|
||||
except OSError:
|
||||
pass
|
||||
finally:
|
||||
if tsock.fileno() != -1:
|
||||
tsock.close()
|
||||
self.vmsg(_("%(host)s:%(port)s: "
|
||||
"Websocket client or target closed") %
|
||||
{'host': host, 'port': port})
|
||||
|
||||
def new_websocket_client(self):
|
||||
"""Called after a new WebSocket connection has been established."""
|
||||
# Reopen the eventlet hub to make sure we don't share an epoll
|
||||
@ -260,14 +277,15 @@ class NovaProxyRequestHandler(websockify.ProxyRequestHandler):
|
||||
|
||||
# Start proxying
|
||||
try:
|
||||
if CONF.consoleauth.enforce_session_timeout:
|
||||
conn_timeout = connect_info.expires - timeutils.utcnow_ts()
|
||||
LOG.info('%s seconds to terminate connection.', conn_timeout)
|
||||
threading.Timer(conn_timeout, self._close_connection,
|
||||
[tsock, host, port]).start()
|
||||
|
||||
self.do_proxy(tsock)
|
||||
except Exception:
|
||||
if tsock:
|
||||
tsock.shutdown(socket.SHUT_RDWR)
|
||||
tsock.close()
|
||||
self.vmsg(_("%(host)s:%(port)s: "
|
||||
"Websocket client or target closed") %
|
||||
{'host': host, 'port': port})
|
||||
self._close_connection(tsock, host, port)
|
||||
raise
|
||||
|
||||
def socket(self, *args, **kwargs):
|
||||
|
@ -20,6 +20,7 @@ from oslo_log import log as logging
|
||||
from oslo_utils import strutils
|
||||
from oslo_utils import timeutils
|
||||
from oslo_utils import uuidutils
|
||||
from oslo_utils import versionutils
|
||||
|
||||
from nova.db.main import api as db
|
||||
from nova import exception
|
||||
@ -38,7 +39,9 @@ class ConsoleAuthToken(base.NovaTimestampObject, base.NovaObject):
|
||||
# Version 1.1: Add clean_expired_console_auths method.
|
||||
# The clean_expired_console_auths_for_host method
|
||||
# was deprecated.
|
||||
VERSION = '1.1'
|
||||
# Version 1.2: Add expires field.
|
||||
# This is to see token expire time.
|
||||
VERSION = '1.2'
|
||||
|
||||
fields = {
|
||||
'id': fields.IntegerField(),
|
||||
@ -52,6 +55,7 @@ class ConsoleAuthToken(base.NovaTimestampObject, base.NovaObject):
|
||||
# database. A hash of the token is stored instead and is not a
|
||||
# field on the object.
|
||||
'token': fields.StringField(nullable=False),
|
||||
'expires': fields.IntegerField(nullable=False),
|
||||
}
|
||||
|
||||
@property
|
||||
@ -77,6 +81,12 @@ class ConsoleAuthToken(base.NovaTimestampObject, base.NovaObject):
|
||||
else:
|
||||
return '%s?token=%s' % (self.access_url_base, self.token)
|
||||
|
||||
def obj_make_compatible(self, primitive, target_version):
|
||||
super().obj_make_compatible(primitive, target_version)
|
||||
target_version = versionutils.convert_version_to_tuple(target_version)
|
||||
if target_version < (1, 2) and 'expires' in primitive:
|
||||
primitive.pop('expires', None)
|
||||
|
||||
@staticmethod
|
||||
def _from_db_object(context, obj, db_obj):
|
||||
# NOTE(PaulMurray): token is not stored in the database but
|
||||
|
@ -15,6 +15,7 @@
|
||||
"""Tests for nova websocketproxy."""
|
||||
|
||||
import copy
|
||||
import fixtures
|
||||
import io
|
||||
import socket
|
||||
from unittest import mock
|
||||
@ -143,6 +144,9 @@ class NovaProxyRequestHandlerTestCase(test.NoDBTestCase):
|
||||
self.wh.do_proxy = mock.MagicMock()
|
||||
self.wh.headers = mock.MagicMock()
|
||||
|
||||
self.threading_timer_mock = self.useFixture(
|
||||
fixtures.MockPatch('threading.Timer', mock.DEFAULT)).mock
|
||||
|
||||
fake_header = {
|
||||
'cookie': 'token="123-456-789"',
|
||||
'Origin': 'https://example.net:6080',
|
||||
@ -207,6 +211,7 @@ class NovaProxyRequestHandlerTestCase(test.NoDBTestCase):
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc',
|
||||
'expires': '100',
|
||||
'access_url_base': 'https://example.net:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
@ -235,11 +240,13 @@ class NovaProxyRequestHandlerTestCase(test.NoDBTestCase):
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc',
|
||||
'expires': '100',
|
||||
'access_url_base': 'https://[2001:db8::1]:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.socket.return_value = '<socket>'
|
||||
tsock = mock.MagicMock()
|
||||
self.wh.socket.return_value = tsock
|
||||
self.wh.path = "http://[2001:db8::1]/?token=123-456-789"
|
||||
self.wh.headers = self.fake_header_ipv6
|
||||
|
||||
@ -247,7 +254,7 @@ class NovaProxyRequestHandlerTestCase(test.NoDBTestCase):
|
||||
|
||||
validate.assert_called_with(mock.ANY, "123-456-789")
|
||||
self.wh.socket.assert_called_with('node1', 10000, connect=True)
|
||||
self.wh.do_proxy.assert_called_with('<socket>')
|
||||
self.wh.do_proxy.assert_called_with(tsock)
|
||||
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_new_websocket_client_token_invalid(self, validate):
|
||||
@ -273,6 +280,7 @@ class NovaProxyRequestHandlerTestCase(test.NoDBTestCase):
|
||||
'port': '10000',
|
||||
'internal_access_path': 'vmid',
|
||||
'console_type': 'novnc',
|
||||
'expires': '100',
|
||||
'access_url_base': 'https://example.net:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
@ -304,6 +312,7 @@ class NovaProxyRequestHandlerTestCase(test.NoDBTestCase):
|
||||
'port': '10000',
|
||||
'internal_access_path': 'xxx',
|
||||
'console_type': 'novnc',
|
||||
'expires': '100',
|
||||
'access_url_base': 'https://example.net:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
@ -409,7 +418,8 @@ class NovaProxyRequestHandlerTestCase(test.NoDBTestCase):
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.socket.return_value = '<socket>'
|
||||
tsock = mock.MagicMock()
|
||||
self.wh.socket.return_value = tsock
|
||||
self.wh.path = "http://127.0.0.1/"
|
||||
self.wh.headers = self.fake_header_allowed_origin
|
||||
|
||||
@ -417,7 +427,7 @@ class NovaProxyRequestHandlerTestCase(test.NoDBTestCase):
|
||||
|
||||
validate.assert_called_with(mock.ANY, "123-456-789")
|
||||
self.wh.socket.assert_called_with('node1', 10000, connect=True)
|
||||
self.wh.do_proxy.assert_called_with('<socket>')
|
||||
self.wh.do_proxy.assert_called_with(tsock)
|
||||
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandler.'
|
||||
'_check_console_port')
|
||||
@ -455,7 +465,9 @@ class NovaProxyRequestHandlerTestCase(test.NoDBTestCase):
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.socket.return_value = '<socket>'
|
||||
tsock = mock.MagicMock()
|
||||
self.wh.socket.return_value = tsock
|
||||
|
||||
self.wh.path = "http://127.0.0.1/"
|
||||
self.wh.headers = self.fake_header_no_origin
|
||||
|
||||
@ -463,7 +475,7 @@ class NovaProxyRequestHandlerTestCase(test.NoDBTestCase):
|
||||
|
||||
validate.assert_called_with(mock.ANY, "123-456-789")
|
||||
self.wh.socket.assert_called_with('node1', 10000, connect=True)
|
||||
self.wh.do_proxy.assert_called_with('<socket>')
|
||||
self.wh.do_proxy.assert_called_with(tsock)
|
||||
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandler.'
|
||||
'_check_console_port')
|
||||
@ -694,6 +706,85 @@ class NovaProxyRequestHandlerTestCase(test.NoDBTestCase):
|
||||
websocketproxy.NovaWebSocketProxy(ssl_minimum_version=minver)
|
||||
mock_select_ssl.assert_called_once_with(minver)
|
||||
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandler.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_enforce_session_timeout_timer_called(
|
||||
self, validate, check_port):
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc',
|
||||
'expires': '100',
|
||||
'access_url_base': 'https://example.net:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.socket.return_value = '<socket>'
|
||||
self.wh.path = "http://127.0.0.1/?token=123-456-789"
|
||||
self.wh.headers = self.fake_header
|
||||
|
||||
# in CONF, set to enforce session timeout
|
||||
self.flags(enforce_session_timeout=True, group='consoleauth')
|
||||
self.wh.new_websocket_client()
|
||||
self.threading_timer_mock.assert_called_once()
|
||||
|
||||
@mock.patch('nova.console.websocketproxy.NovaProxyRequestHandler.'
|
||||
'_check_console_port')
|
||||
@mock.patch('nova.objects.ConsoleAuthToken.validate')
|
||||
def test_enforce_session_timeout_timer_not_called(
|
||||
self, validate, check_port):
|
||||
params = {
|
||||
'id': 1,
|
||||
'token': '123-456-789',
|
||||
'instance_uuid': uuids.instance,
|
||||
'host': 'node1',
|
||||
'port': '10000',
|
||||
'console_type': 'novnc',
|
||||
'expires': '100',
|
||||
'access_url_base': 'https://example.net:6080'
|
||||
}
|
||||
validate.return_value = objects.ConsoleAuthToken(**params)
|
||||
|
||||
self.wh.socket.return_value = '<socket>'
|
||||
self.wh.path = "http://127.0.0.1/?token=123-456-789"
|
||||
self.wh.headers = self.fake_header
|
||||
|
||||
self.flags(enforce_session_timeout=False, group='consoleauth')
|
||||
self.wh.new_websocket_client()
|
||||
self.threading_timer_mock.assert_not_called()
|
||||
|
||||
def test__close_connection(self):
|
||||
tsock = mock.MagicMock()
|
||||
self.wh.vmsg = mock.MagicMock()
|
||||
host = 'node1'
|
||||
port = '10000'
|
||||
|
||||
self.wh._close_connection(tsock, host, port)
|
||||
tsock.shutdown.assert_called_once_with(socket.SHUT_RDWR)
|
||||
tsock.close.assert_called_once()
|
||||
self.wh.vmsg.assert_called_once_with(
|
||||
f"{host}:{port}: Websocket client or target closed")
|
||||
|
||||
def test__close_connection_raise_OSError(self):
|
||||
tsock = mock.MagicMock()
|
||||
self.wh.vmsg = mock.MagicMock()
|
||||
host = 'node1'
|
||||
port = '10000'
|
||||
|
||||
tsock.shutdown.side_effect = OSError("Error")
|
||||
|
||||
self.wh._close_connection(tsock, host, port)
|
||||
|
||||
tsock.shutdown.assert_called_once_with(socket.SHUT_RDWR)
|
||||
tsock.close.assert_called_once()
|
||||
|
||||
self.wh.vmsg.assert_called_once_with(
|
||||
f"{host}:{port}: Websocket client or target closed")
|
||||
|
||||
|
||||
class NovaWebsocketSecurityProxyTestCase(test.NoDBTestCase):
|
||||
|
||||
|
@ -53,7 +53,7 @@ class _TestConsoleAuthToken(object):
|
||||
|
||||
expected = copy.deepcopy(fakes.fake_token_dict)
|
||||
del expected['token_hash']
|
||||
del expected['expires']
|
||||
expected['expires'] = expires
|
||||
expected['token'] = fakes.fake_token
|
||||
expected['console_type'] = console_type
|
||||
|
||||
@ -85,6 +85,14 @@ class _TestConsoleAuthToken(object):
|
||||
path)
|
||||
self.assertEqual(expected_url, url)
|
||||
|
||||
# verify auth_token 'expires' backward version compatibility
|
||||
data = lambda x: x['nova_object.data']
|
||||
console_auth_obj_primitive = data(obj.obj_to_primitive())
|
||||
self.assertIn('expires', console_auth_obj_primitive)
|
||||
obj.obj_make_compatible(console_auth_obj_primitive, '1.1')
|
||||
self.assertIn('token', console_auth_obj_primitive)
|
||||
self.assertNotIn('expires', console_auth_obj_primitive)
|
||||
|
||||
def test_authorize(self):
|
||||
self._test_authorize(fakes.fake_token_dict['console_type'])
|
||||
|
||||
|
@ -1087,7 +1087,7 @@ object_data = {
|
||||
'CellMappingList': '1.1-496ef79bb2ab41041fff8bcb57996352',
|
||||
'ComputeNode': '1.19-af6bd29a6c3b225da436a0d8487096f2',
|
||||
'ComputeNodeList': '1.17-52f3b0962b1c86b98590144463ebb192',
|
||||
'ConsoleAuthToken': '1.1-8da320fb065080eb4d3c2e5c59f8bf52',
|
||||
'ConsoleAuthToken': '1.2-a4677c3576ad91eb02068a5cb9d38eaa',
|
||||
'CpuDiagnostics': '1.0-d256f2e442d1b837735fd17dfe8e3d47',
|
||||
'Destination': '1.4-3b440d29459e2c98987ad5b25ad1cb2c',
|
||||
'DeviceBus': '1.0-77509ea1ea0dd750d5864b9bd87d3f9d',
|
||||
|
@ -0,0 +1,12 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
This is a security-enhancing feature that automatically closes console sessions
|
||||
exceeding a defined timeout period. To enable this functionality, operators are
|
||||
required to set the 'enforce_session_timeout' boolean configuration option to True.
|
||||
|
||||
The enforcement is implemented via a timer mechanism, initiating when users access
|
||||
the console and concluding upon the expiration of the set console token.
|
||||
|
||||
This ensures the graceful closure of console sessions on the server side, aligning
|
||||
with security best practices.
|
Loading…
x
Reference in New Issue
Block a user