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
This commit is contained in:
Michael Still 2024-07-30 19:40:30 +10:00
parent a2cc66a377
commit e06890d101
7 changed files with 150 additions and 5 deletions

View File

@ -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

View File

@ -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.
"""),
]

View File

@ -1715,9 +1715,9 @@ class LibvirtConfigGuestGraphicsTest(LibvirtConfigBaseTest):
obj.listen = "127.0.0.1"
xml = obj.to_xml()
self.assertXmlEqual(xml, """
self.assertXmlEqual("""
<graphics type="vnc" autoport="yes" keymap="en_US" listen="127.0.0.1"/>
""")
""", 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("""
<graphics type="spice" autoport="no" keymap="en_US" listen="127.0.0.1"/>
""", 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("""
<graphics type="spice" autoport="no" keymap="en_US" listen="127.0.0.1">
<image compression="auto_glz"/>
<jpeg compression="auto"/>
@ -1741,7 +1753,64 @@ class LibvirtConfigGuestGraphicsTest(LibvirtConfigBaseTest):
<playback compression="on"/>
<streaming mode="filter"/>
</graphics>
""")
""", 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("""
<graphics type="spice" autoport="no" keymap="en_US" listen="127.0.0.1">
<channel name="main" mode="secure"/>
<channel name="display" mode="secure"/>
<channel name="inputs" mode="secure"/>
<channel name="cursor" mode="secure"/>
<channel name="playback" mode="secure"/>
<channel name="record" mode="secure"/>
<channel name="smartcard" mode="secure"/>
<channel name="usbredir" mode="secure"/>
</graphics>
""", 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("""
<graphics type="spice" autoport="no" keymap="en_US" listen="127.0.0.1">
<image compression="auto_glz"/>
<jpeg compression="auto"/>
<zlib compression="always"/>
<playback compression="on"/>
<streaming mode="filter"/>
<channel name="main" mode="secure"/>
<channel name="display" mode="secure"/>
<channel name="inputs" mode="secure"/>
<channel name="cursor" mode="secure"/>
<channel name="playback" mode="secure"/>
<channel name="record" mode="secure"/>
<channel name="smartcard" mode="secure"/>
<channel name="usbredir" mode="secure"/>
</graphics>
""", xml)
class LibvirtConfigGuestHostdev(LibvirtConfigBaseTest):

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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.