diff --git a/ironic_python_agent/agent.py b/ironic_python_agent/agent.py index 4d8406c01..66bc29c58 100644 --- a/ironic_python_agent/agent.py +++ b/ironic_python_agent/agent.py @@ -513,6 +513,9 @@ def process_lookup_data(self, content): # Update config with values from Ironic config = content.get('config', {}) + if config.get('enable_bios_bootloader_install'): + cfg.CONF.set_override('enable_bios_bootloader_install', + config['enable_bios_bootloader_install']) if config.get('agent_containers'): for opt, val in config['agent_containers'].items(): cfg.CONF.set_override(opt, val, group='container') diff --git a/ironic_python_agent/config.py b/ironic_python_agent/config.py index e89b80af6..32b1ba950 100644 --- a/ironic_python_agent/config.py +++ b/ironic_python_agent/config.py @@ -404,6 +404,13 @@ help='This disables bootc deployment methods in the ramdisk ' 'because the bootc command inside of the ramdisk ' 'comes from the supplied image to be deployed.'), + cfg.BoolOpt('enable_bios_bootloader_install', + default=False, + help='Enables support for partition images which require a ' + 'legacy bootloader -- and a call to ``grub-install``. ' + 'Generally, this should remain disabled for maximum ' + 'security, however, this option allows it to be ' + 're-enabled for compatibility.'), ] disk_utils_opts = [ diff --git a/ironic_python_agent/extensions/image.py b/ironic_python_agent/extensions/image.py index 5ca3c6465..7c0f86741 100644 --- a/ironic_python_agent/extensions/image.py +++ b/ironic_python_agent/extensions/image.py @@ -723,15 +723,24 @@ def install_bootloader(self, root_uuid, efi_system_part_uuid=None, ' Assuming a whole disk image') return - # In case we can't use efibootmgr for uefi we will continue using grub2 - LOG.debug('Using grub2-install to set up boot files') - try: - _install_grub2(device, - root_uuid=root_uuid, - efi_system_part_uuid=efi_system_part_uuid, - prep_boot_part_uuid=prep_boot_part_uuid, - target_boot_mode=target_boot_mode) - except Exception as e: - LOG.error('Error setting up bootloader. Error %s', e) + if CONF.enable_bios_bootloader_install: + # In case we can't use efibootmgr for uefi we will continue + # using grub2 + LOG.debug('Using grub2-install to set up boot files') + try: + _install_grub2(device, + root_uuid=root_uuid, + efi_system_part_uuid=efi_system_part_uuid, + prep_boot_part_uuid=prep_boot_part_uuid, + target_boot_mode=target_boot_mode) + except Exception as e: + LOG.error('Error setting up bootloader. Error %s', e) + if not ignore_failure: + raise + else: + msg = ("Install of legacy BIOS bootloaders disabled by " + "CONF.enable_bios_bootloader_install as part of " + "CVE-2026-43003 mitigation.") + LOG.error(msg) if not ignore_failure: - raise + raise errors.InvalidImage(details=msg) diff --git a/ironic_python_agent/tests/unit/extensions/test_image.py b/ironic_python_agent/tests/unit/extensions/test_image.py index 93f7717a8..47275d26a 100644 --- a/ironic_python_agent/tests/unit/extensions/test_image.py +++ b/ironic_python_agent/tests/unit/extensions/test_image.py @@ -52,6 +52,7 @@ def setUp(self): self.fake_efi_system_part_uuid = '45AB-2312' self.fake_prep_boot_part_uuid = '76937797-3253-8843-999999999999' self.fake_dir = '/tmp/fake-dir' + self.config(enable_bios_bootloader_install=True) @mock.patch.object(image, '_install_grub2', autospec=True) def test__install_bootloader_bios(self, mock_grub2, @@ -68,8 +69,39 @@ def test__install_bootloader_bios(self, mock_grub2, self.fake_dev, root_uuid=self.fake_root_uuid, efi_system_part_uuid=None, prep_boot_part_uuid=None, target_boot_mode='bios' + ) + @mock.patch.object(image, '_install_grub2', autospec=True) + def test__install_bootloader_bios_disabled(self, mock_grub2, + mock_execute, mock_dispatch): + self.config(enable_bios_bootloader_install=False) + mock_dispatch.side_effect = [ + self.fake_dev, hardware.BootInfo(current_boot_mode='bios') + ] + self.agent_extension.install_bootloader( + root_uuid=self.fake_root_uuid).join() + mock_dispatch.assert_any_call('get_os_install_device') + mock_dispatch.assert_any_call('get_boot_info') + self.assertEqual(2, mock_dispatch.call_count) + mock_grub2.assert_not_called() + + @mock.patch.object(image, '_install_grub2', autospec=True) + def test__install_bootloader_bios_disabled_dont_ignore_failures( + self, mock_grub2, mock_execute, mock_dispatch): + self.config(enable_bios_bootloader_install=False) + self.config(ignore_bootloader_failure=False) + mock_dispatch.side_effect = [ + self.fake_dev, hardware.BootInfo(current_boot_mode='bios') + ] + result = self.agent_extension.install_bootloader( + root_uuid=self.fake_root_uuid).join() + mock_dispatch.assert_any_call('get_os_install_device') + mock_dispatch.assert_any_call('get_boot_info') + self.assertEqual(2, mock_dispatch.call_count) + self.assertIsNotNone(result.command_error) + mock_grub2.assert_not_called() + @mock.patch.object(efi_utils, 'manage_uefi', autospec=True) @mock.patch.object(image, '_install_grub2', autospec=True) def test__install_bootloader_uefi(self, mock_grub2, mock_uefi, diff --git a/releasenotes/notes/disable-installing-bootloaders-by-default-1029ed4faaf049aa.yaml b/releasenotes/notes/disable-installing-bootloaders-by-default-1029ed4faaf049aa.yaml new file mode 100644 index 000000000..28a771345 --- /dev/null +++ b/releasenotes/notes/disable-installing-bootloaders-by-default-1029ed4faaf049aa.yaml @@ -0,0 +1,9 @@ +--- +security: + - | + Disable installation of bootloaders (via grub-install) by default in order + to improve security posture by adding a new configuration option + `enable_bios_bootloader_install` which defaults to `False`. Operators + who still need this functionality can re-enable installation of + bootloaders by setting `enable_bios_bootloader_install` to `True`. + Addresses CVE-2026-43003.