From e06890d10105a198404348d6fe9899a549387b7d Mon Sep 17 00:00:00 2001 From: Michael Still Date: Tue, 30 Jul 2024 19:40:30 +1000 Subject: [PATCH] libvirt: Add config option to require secure SPICE. This patch adds the following SPICE-related configuration option to the 'spice' configuration group: - require_secure When set to true, libvirt will be provided with domain XML which configures SPICE VDI consoles to require secure connections (that is, connections protected by TLS). Attempts to connect without TLS will receive an error indicating they should retry the connection on the TLS port. Change-Id: Ica7083b0836f8d66cad8a4b4097613103fc91560 --- doc/source/admin/remote-console-access.rst | 5 +- nova/conf/spice.py | 17 ++++ nova/tests/unit/virt/libvirt/test_config.py | 77 ++++++++++++++++++- nova/tests/unit/virt/libvirt/test_driver.py | 41 ++++++++++ nova/virt/libvirt/config.py | 6 ++ nova/virt/libvirt/driver.py | 1 + ...pice-direct-consoles-4bee40633633c971.yaml | 8 ++ 7 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/spice-direct-consoles-4bee40633633c971.yaml diff --git a/doc/source/admin/remote-console-access.rst b/doc/source/admin/remote-console-access.rst index 85ba556cfd47..31f098fea2c0 100644 --- a/doc/source/admin/remote-console-access.rst +++ b/doc/source/admin/remote-console-access.rst @@ -318,7 +318,6 @@ be told where to find them. This requires editing :file:`nova.conf` to set. vencrypt_client_cert=/etc/pki/nova-novncproxy/client-cert.pem vencrypt_ca_certs=/etc/pki/nova-novncproxy/ca-cert.pem - .. _spice-console: SPICE console @@ -403,6 +402,10 @@ for SPICE consoles. - :oslo.config:option:`spice.playback_compression` - :oslo.config:option:`spice.streaming_mode` +As well as the following option to require that connections be protected by TLS: + +- :oslo.config:option:`spice.require_secure` + .. _serial-console: Serial console diff --git a/nova/conf/spice.py b/nova/conf/spice.py index e5854946f15d..d01a83c3b937 100644 --- a/nova/conf/spice.py +++ b/nova/conf/spice.py @@ -194,6 +194,23 @@ Related options: * This option depends on the ``server_listen`` option. The proxy client must be able to access the address specified in ``server_listen`` using the value of this option. +"""), + cfg.BoolOpt('require_secure', + default=False, + help=""" +Whether to require secure TLS connections to SPICE consoles. + +If you're providing direct access to SPICE consoles instead of using the HTML5 +proxy, you may wish those connections to be encrypted. If so, set this value to +True. + +Note that use of secure consoles requires that you setup TLS certificates on +each hypervisor. + +Possible values: + +* False: console traffic is not encrypted. +* True: console traffic is required to be protected by TLS. """), ] diff --git a/nova/tests/unit/virt/libvirt/test_config.py b/nova/tests/unit/virt/libvirt/test_config.py index 01c5e434854a..013307f0cdf1 100644 --- a/nova/tests/unit/virt/libvirt/test_config.py +++ b/nova/tests/unit/virt/libvirt/test_config.py @@ -1715,9 +1715,9 @@ class LibvirtConfigGuestGraphicsTest(LibvirtConfigBaseTest): obj.listen = "127.0.0.1" xml = obj.to_xml() - self.assertXmlEqual(xml, """ + self.assertXmlEqual(""" - """) + """, xml) def test_config_graphics_spice(self): obj = config.LibvirtConfigGuestGraphics() @@ -1726,6 +1726,18 @@ class LibvirtConfigGuestGraphicsTest(LibvirtConfigBaseTest): obj.keymap = "en_US" obj.listen = "127.0.0.1" + xml = obj.to_xml() + self.assertXmlEqual(""" + + """, xml) + + def test_config_graphics_spice_compression(self): + obj = config.LibvirtConfigGuestGraphics() + obj.type = "spice" + obj.autoport = False + obj.keymap = "en_US" + obj.listen = "127.0.0.1" + obj.image_compression = "auto_glz" obj.jpeg_compression = "auto" obj.zlib_compression = "always" @@ -1733,7 +1745,7 @@ class LibvirtConfigGuestGraphicsTest(LibvirtConfigBaseTest): obj.streaming_mode = "filter" xml = obj.to_xml() - self.assertXmlEqual(xml, """ + self.assertXmlEqual(""" @@ -1741,7 +1753,64 @@ class LibvirtConfigGuestGraphicsTest(LibvirtConfigBaseTest): - """) + """, xml) + + def test_config_graphics_spice_secure(self): + obj = config.LibvirtConfigGuestGraphics() + obj.type = "spice" + obj.autoport = False + obj.keymap = "en_US" + obj.listen = "127.0.0.1" + + obj.secure = True + + xml = obj.to_xml() + self.assertXmlEqual(""" + + + + + + + + + + + """, xml) + + def test_config_graphics_spice_secure_compression(self): + obj = config.LibvirtConfigGuestGraphics() + obj.type = "spice" + obj.autoport = False + obj.keymap = "en_US" + obj.listen = "127.0.0.1" + + obj.secure = True + + obj.image_compression = "auto_glz" + obj.jpeg_compression = "auto" + obj.zlib_compression = "always" + obj.playback_compression = True + obj.streaming_mode = "filter" + + xml = obj.to_xml() + self.assertXmlEqual(""" + + + + + + + + + + + + + + + + """, xml) class LibvirtConfigGuestHostdev(LibvirtConfigBaseTest): diff --git a/nova/tests/unit/virt/libvirt/test_driver.py b/nova/tests/unit/virt/libvirt/test_driver.py index 8046a4f7efda..ae37a57b9221 100644 --- a/nova/tests/unit/virt/libvirt/test_driver.py +++ b/nova/tests/unit/virt/libvirt/test_driver.py @@ -5897,6 +5897,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertIsNone(cfg.devices[3].zlib_compression) self.assertIsNone(cfg.devices[3].playback_compression) self.assertIsNone(cfg.devices[3].streaming_mode) + self.assertFalse(cfg.devices[3].secure) def test_get_guest_config_with_vnc_and_tablet(self): self.flags(enabled=True, group='vnc') @@ -5932,6 +5933,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertIsNone(cfg.devices[3].zlib_compression) self.assertIsNone(cfg.devices[3].playback_compression) self.assertIsNone(cfg.devices[3].streaming_mode) + self.assertFalse(cfg.devices[3].secure) self.assertEqual(cfg.devices[5].type, 'tablet') def test_get_guest_config_with_spice_and_tablet(self): @@ -5973,6 +5975,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertIsNone(cfg.devices[3].zlib_compression) self.assertIsNone(cfg.devices[3].playback_compression) self.assertIsNone(cfg.devices[3].streaming_mode) + self.assertFalse(cfg.devices[3].secure) self.assertEqual(cfg.devices[5].type, 'tablet') @mock.patch.object(host.Host, "_check_machine_type", new=mock.Mock()) @@ -6037,6 +6040,7 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertIsNone(cfg.devices[4].zlib_compression) self.assertIsNone(cfg.devices[4].playback_compression) self.assertIsNone(cfg.devices[4].streaming_mode) + self.assertFalse(cfg.devices[4].secure) self.assertEqual(cfg.devices[5].type, video_type) def test_get_guest_config_with_spice_compression(self): @@ -6082,6 +6086,43 @@ class LibvirtConnTestCase(test.NoDBTestCase, self.assertEqual(cfg.devices[3].zlib_compression, 'always') self.assertFalse(cfg.devices[3].playback_compression) self.assertEqual(cfg.devices[3].streaming_mode, 'all') + self.assertFalse(cfg.devices[3].secure) + + def test_get_guest_config_with_spice_secure(self): + self.flags(enabled=False, group='vnc') + self.flags(virt_type='kvm', group='libvirt') + self.flags(enabled=True, + agent_enabled=False, + require_secure=True, + server_listen='10.0.0.1', + group='spice') + self.flags(pointer_model='usbtablet') + + cfg = self._get_guest_config_with_graphics() + + self.assertEqual(len(cfg.devices), 9) + self.assertIsInstance(cfg.devices[0], + vconfig.LibvirtConfigGuestDisk) + self.assertIsInstance(cfg.devices[1], + vconfig.LibvirtConfigGuestDisk) + self.assertIsInstance(cfg.devices[2], + vconfig.LibvirtConfigGuestSerial) + self.assertIsInstance(cfg.devices[3], + vconfig.LibvirtConfigGuestGraphics) + self.assertIsInstance(cfg.devices[4], + vconfig.LibvirtConfigGuestVideo) + self.assertIsInstance(cfg.devices[5], + vconfig.LibvirtConfigGuestInput) + self.assertIsInstance(cfg.devices[6], + vconfig.LibvirtConfigGuestRng) + self.assertIsInstance(cfg.devices[7], + vconfig.LibvirtConfigGuestUSBHostController) + self.assertIsInstance(cfg.devices[8], + vconfig.LibvirtConfigMemoryBalloon) + + self.assertEqual(cfg.devices[3].type, 'spice') + self.assertEqual(cfg.devices[3].listen, '10.0.0.1') + self.assertTrue(cfg.devices[3].secure) @mock.patch.object(host.Host, 'get_guest') @mock.patch.object(libvirt_driver.LibvirtDriver, diff --git a/nova/virt/libvirt/config.py b/nova/virt/libvirt/config.py index a4395b4d28ee..059b3c686ddc 100644 --- a/nova/virt/libvirt/config.py +++ b/nova/virt/libvirt/config.py @@ -2188,6 +2188,7 @@ class LibvirtConfigGuestGraphics(LibvirtConfigGuestDevice): self.autoport = True self.keymap = None self.listen = None + self.secure = None self.image_compression = None self.jpeg_compression = None @@ -2222,6 +2223,11 @@ class LibvirtConfigGuestGraphics(LibvirtConfigGuestDevice): if self.streaming_mode is not None: dev.append(etree.Element( 'streaming', mode=self.streaming_mode)) + if self.secure: + for channel in ['main', 'display', 'inputs', 'cursor', + 'playback', 'record', 'smartcard', 'usbredir']: + dev.append(etree.Element('channel', name=channel, + mode='secure')) return dev diff --git a/nova/virt/libvirt/driver.py b/nova/virt/libvirt/driver.py index e90c6284544e..7e701c740d44 100644 --- a/nova/virt/libvirt/driver.py +++ b/nova/virt/libvirt/driver.py @@ -7692,6 +7692,7 @@ class LibvirtDriver(driver.ComputeDriver): graphics.zlib_compression = CONF.spice.zlib_compression graphics.playback_compression = CONF.spice.playback_compression graphics.streaming_mode = CONF.spice.streaming_mode + graphics.secure = CONF.spice.require_secure guest.add_device(graphics) add_video_driver = True diff --git a/releasenotes/notes/spice-direct-consoles-4bee40633633c971.yaml b/releasenotes/notes/spice-direct-consoles-4bee40633633c971.yaml new file mode 100644 index 000000000000..0149fa051d21 --- /dev/null +++ b/releasenotes/notes/spice-direct-consoles-4bee40633633c971.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + This release adds a new config option require_secure to the spice + configuration group. Defaulting to false to match the previous + behavior, if set to true the SPICE consoles will require TLS + protected connections. Unencrypted connections will be gracefully + redirected to the TLS port via the SPICE protocol.