From 012295ced75e96829b6495ef8d781d391a225d48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20L=C3=B6ser?= Date: Tue, 28 Feb 2023 13:16:40 +0100 Subject: [PATCH] Fixes #36833 - New PXE loader "Grub2 UEFI SecureBoot (target OS)" This feature consists of two patches, one for foreman and one for smart-proxy. This patch introduces a new loader of kind `:PXEGrub2TargetOS` which allows to provide host-specific Network Bootstrap Programs (NPB) in order to enable network based installations for SecureBoot-enabled hosts. SecureBoot expects to follow a chain of trust from the start of the host to the loading of Linux kernel modules. The very first shim that is loaded basically determines which distribution is allowed to be booted or kexec'ed until next reboot. The existing "Grub2 UEFI SecureBoot" is not sufficiant as it limits the possible installations to the vendor of the Foreman (Smart Proxy) host system. Providing shim and GRUB2 by the vendor of the to-be-installed operating system allows Foreman to install any operating system on SecureBoot-enabled hosts over network. To achieve this, the host's DHCP filename option is set to a shim path in a directory that is host-specific (contains MAC address). Corresponding shim and GRUB2 binaries are copied into that directory along with the generated GRUB2 configuration files as we know from "Grub2 UEFI". The required binaries must be provided once in the so called "bootloader universe". This directory can be configured via the settings file `/etc/foreman-proxy/settings.d/tftp.yml` and defaults to `/usr/local/share/bootloader-universe//`. These binaries can be manually retrieved from the installation media and is not part of this patchset. Full example: ------------- [root@vm ~]# hammer host info --id 241 | grep -E "(MAC address|Operating System)" MAC address: 00:50:56:b4:75:5e Operating System: Ubuntu 22.04 LTS [root@vm ~]# tree /usr/local/share/bootloader-universe/ /usr/local/share/bootloader-universe/ |-- centos | |-- grubx64.efi | `-- shimx64.efi `-- ubuntu |-- grubx64.efi `-- shimx64.efi [root@vm ~]# hammer host update --id 241 --build true [root@vm ~]# tree /var/lib/tftpboot/grub2/00-50-56-b4-75-5e/ /var/lib/tftpboot/grub2/00-50-56-b4-75-5e/ |-- grub.cfg |-- grub.cfg-00:50:56:b4:75:5e |-- grub.cfg-01-00-50-56-b4-75-5e |-- grubx64.efi |-- shimx64.efi `-- targetos [root@vm ~]# grep -B2 00-50-56-b4-75-5e /var/lib/dhcpd/dhcpd.leases hardware ethernet 00:50:56:b4:75:5e; fixed-address 192.168.145.84; supersede server.filename = "grub2/00-50-56-b4-75-5e/shimx64.efi"; [root@vm ~]# pesign -S -i /var/lib/tftpboot/grub2/00-50-56-b4-75-5e/grubx64.efi | grep Canonical The signer's common name is Canonical Ltd. Secure Boot Signing (2021 v1) --- config/settings.d/tftp.yml.example | 4 ++ modules/tftp/server.rb | 77 ++++++++++++++++++++++++++++-- modules/tftp/tftp_api.rb | 7 +-- modules/tftp/tftp_plugin.rb | 2 + test/tftp/integration_test.rb | 4 +- test/tftp/tftp_api_test.rb | 5 ++ test/tftp/tftp_server_test.rb | 16 +++++++ 7 files changed, 107 insertions(+), 8 deletions(-) diff --git a/config/settings.d/tftp.yml.example b/config/settings.d/tftp.yml.example index 27688934e..beb4fef76 100644 --- a/config/settings.d/tftp.yml.example +++ b/config/settings.d/tftp.yml.example @@ -13,3 +13,7 @@ # Defines the default certificate action for certificate checking. # When false, the argument --no-check-certificate will be used. #:verify_server_cert: true + +# Directory where to search for GRUB2 and shim binaries to provision SecureBoot enabled +# hosts with the "Grub2 UEFI SecureBoot (target OS)" PXE loader +#:bootloader_universe: diff --git a/modules/tftp/server.rb b/modules/tftp/server.rb index 98218d8bd..350cfc039 100644 --- a/modules/tftp/server.rb +++ b/modules/tftp/server.rb @@ -63,6 +63,18 @@ def delete_file(file) logger.debug "TFTP: Skipping a request to delete a file which doesn't exists" end end + + def delete_dir(dir) + if Dir.exist?(dir) + FileUtils.rm_rf dir + logger.debug "TFTP: #{dir} removed successfully" + else + logger.debug "TFTP: Skipping a request to delete a directory which doesn't exists" + end + end + + def setup_bootloader(mac, os, major, minor, arch) + end end class Syslinux < Server @@ -95,8 +107,66 @@ def pxeconfig_file(mac) end class Pxegrub2 < Server - def pxeconfig_dir - "#{path}/grub2" + def bootloader_path(os, version, arch) + unless (bootloader_universe = Proxy::TFTP::Plugin.settings.bootloader_universe) + logger.debug "TFTP: bootloader universe not configured." + return + end + + bootloader_path = "#{bootloader_universe}/pxegrub2/#{os}/#{version}/#{arch}" + + logger.debug "TFTP: Checking bootloader universe for suitable bootloader directory for" + logger.debug "TFTP: * Operating system: #{os}" + logger.debug "TFTP: * Version: #{version}" + logger.debug "TFTP: * Architecture: #{arch}" + logger.debug "TFTP: Checking bootloader universe if \"#{bootloader_path}\" exists." + unless Dir.exist?(bootloader_path) + logger.debug "TFTP: Directory \"#{bootloader_path}\" does not exist." + + bootloader_path = "#{bootloader_universe}/pxegrub2/#{os}/default/#{arch}" + logger.debug "TFTP: Checking if fallback directory at \"#{bootloader_path}\" exists." + unless Dir.exist?(bootloader_path) + logger.debug "TFTP: Directory \"#{bootloader_path}\" does not exist." + return + end + end + + bootloader_path + end + + def setup_bootloader(mac, os, major, minor, arch) + pxeconfig_dir_mac = pxeconfig_dir(mac) + FileUtils.mkdir_p(pxeconfig_dir_mac) + + version = "#{major}#{".#{minor}" unless minor.empty?}" + bootloader_path = bootloader_path(os, version, arch) + + if bootloader_path + logger.debug "TFTP: Copying bootloader files from bootloader universe:" + logger.debug "TFTP: - \"#{bootloader_path}/*\" => \"#{pxeconfig_dir_mac}/\"" + FileUtils.cp_r("#{bootloader_path}/.", "#{pxeconfig_dir_mac}/", remove_destination: true) + else + logger.debug "TFTP: Copying default bootloader files:" + logger.debug "TFTP: - \"#{pxeconfig_dir}/grubx64.efi\" => \"#{pxeconfig_dir_mac}/grubx64.efi\"" + logger.debug "TFTP: - \"#{pxeconfig_dir}/shimx64.efi\" => \"#{pxeconfig_dir_mac}/shimx64.efi\"" + logger.debug "TFTP: - \"#{pxeconfig_dir}/grubx64.efi\" => \"#{pxeconfig_dir_mac}/boot.efi\"" + logger.debug "TFTP: - \"#{pxeconfig_dir}/shimx64.efi\" => \"#{pxeconfig_dir_mac}/boot-sb.efi\"" + FileUtils.cp_r("#{pxeconfig_dir}/grubx64.efi", "#{pxeconfig_dir_mac}/grubx64.efi", remove_destination: true) + FileUtils.cp_r("#{pxeconfig_dir}/shimx64.efi", "#{pxeconfig_dir_mac}/shimx64.efi", remove_destination: true) + FileUtils.cp_r("#{pxeconfig_dir}/grubx64.efi", "#{pxeconfig_dir_mac}/boot.efi", remove_destination: true) + FileUtils.cp_r("#{pxeconfig_dir}/shimx64.efi", "#{pxeconfig_dir_mac}/boot-sb.efi", remove_destination: true) + end + + File.write(File.join(pxeconfig_dir_mac, 'os_info'), "#{os} #{version} #{arch}") + end + + def del(mac) + super mac + delete_dir "#{path}/host_config/#{mac.tr(':', '-').downcase}" + end + + def pxeconfig_dir(mac = nil) + "#{path}#{mac ? "/host_config/#{mac.tr(':', '-').downcase}" : ''}/grub2" end def pxe_default @@ -104,7 +174,8 @@ def pxe_default end def pxeconfig_file(mac) - ["#{pxeconfig_dir}/grub.cfg-01-" + mac.tr(':', '-').downcase, "#{pxeconfig_dir}/grub.cfg-#{mac.downcase}"] + pxeconfig_dir_mac = pxeconfig_dir(mac) + ["#{pxeconfig_dir_mac}/grub.cfg", "#{pxeconfig_dir_mac}/grub.cfg-01-#{mac.tr(':', '-').downcase}", "#{pxeconfig_dir_mac}/grub.cfg-#{mac.downcase}", "#{pxeconfig_dir}/grub.cfg-01-" + mac.tr(':', '-').downcase, "#{pxeconfig_dir}/grub.cfg-#{mac.downcase}"] end end diff --git a/modules/tftp/tftp_api.rb b/modules/tftp/tftp_api.rb index 1bc6276c7..33f288a14 100644 --- a/modules/tftp/tftp_api.rb +++ b/modules/tftp/tftp_api.rb @@ -18,8 +18,9 @@ def instantiate(variant, mac = nil) Object.const_get("Proxy").const_get('TFTP').const_get(variant.capitalize).new end - def create(variant, mac) + def create(variant, mac, os, major, minor, arch) tftp = instantiate variant, mac + log_halt(400, "TFTP: Failed to setup target OS bootloader directory: ") { tftp.setup_bootloader(mac, os, major, minor, arch) } log_halt(400, "TFTP: Failed to create pxe config file: ") { tftp.set(mac, (params[:pxeconfig] || params[:syslinux_config])) } end @@ -48,7 +49,7 @@ def create_default(variant) end post "/:variant/:mac" do |variant, mac| - create variant, mac + create variant, mac, params[:targetos], params[:major], params[:minor], params[:arch] end delete "/:variant/:mac" do |variant, mac| @@ -60,7 +61,7 @@ def create_default(variant) end post "/:mac" do |mac| - create "syslinux", mac + create "syslinux", mac, nil, nil, nil, nil end delete("/:mac") do |mac| diff --git a/modules/tftp/tftp_plugin.rb b/modules/tftp/tftp_plugin.rb index d4dc06450..9d8f8d4f5 100644 --- a/modules/tftp/tftp_plugin.rb +++ b/modules/tftp/tftp_plugin.rb @@ -2,6 +2,8 @@ module Proxy::TFTP class Plugin < ::Proxy::Plugin plugin :tftp, ::Proxy::VERSION + capability -> { settings[:bootloader_universe] ? :target_os_bootloader_support : nil } + rackup_path File.expand_path("http_config.ru", __dir__) default_settings :tftproot => '/var/lib/tftpboot', diff --git a/test/tftp/integration_test.rb b/test/tftp/integration_test.rb index a9b88a8f8..cc94b552b 100644 --- a/test/tftp/integration_test.rb +++ b/test/tftp/integration_test.rb @@ -5,7 +5,7 @@ class TftpApiFeaturesTest < SmartProxyRootApiTestCase def test_features - Proxy::DefaultModuleLoader.any_instance.expects(:load_configuration_file).with('tftp.yml').returns(enabled: true, tftproot: '/var/lib/tftpboot', tftp_servername: 'tftp.example.com') + Proxy::DefaultModuleLoader.any_instance.expects(:load_configuration_file).with('tftp.yml').returns(enabled: true, tftproot: '/var/lib/tftpboot', tftp_servername: 'tftp.example.com', bootloader_universe: '/usr/local/share/bootloader-universe') get '/features' @@ -14,7 +14,7 @@ def test_features mod = response['tftp'] refute_nil(mod) assert_equal('running', mod['state'], Proxy::LogBuffer::Buffer.instance.info[:failed_modules][:tftp]) - assert_equal([], mod['capabilities']) + assert_equal(["secure_boot_target_os_bootloader"], mod['capabilities']) expected_settings = { 'tftp_servername' => 'tftp.example.com' } assert_equal(expected_settings, mod['settings']) diff --git a/test/tftp/tftp_api_test.rb b/test/tftp/tftp_api_test.rb index 3ffcfb0cb..f29f73de2 100644 --- a/test/tftp/tftp_api_test.rb +++ b/test/tftp/tftp_api_test.rb @@ -40,6 +40,11 @@ def test_instantiate_pxegrub2 assert_equal "Proxy::TFTP::Pxegrub2", obj.class.name end + def test_instantiate_pxegrub2targetos + obj = app.helpers.instantiate "pxegrub2targetos", "AA:BB:CC:DD:EE:FF" + assert_equal "Proxy::TFTP::Pxegrub2targetos", obj.class.name + end + def test_instantiate_ztp obj = app.helpers.instantiate "ztp", "AA:BB:CC:DD:EE:FF" assert_equal "Proxy::TFTP::Ztp", obj.class.name diff --git a/test/tftp/tftp_server_test.rb b/test/tftp/tftp_server_test.rb index 42d6f1071..fb588a3f2 100644 --- a/test/tftp/tftp_server_test.rb +++ b/test/tftp/tftp_server_test.rb @@ -118,6 +118,22 @@ def setup_paths end end +class TftpPxegrub2targetosServerTest < Test::Unit::TestCase + include TftpGenericServerSuite + + def setup_paths + @subject = Proxy::TFTP::Pxegrub2targetos.new + @pxe_config_files = [ + "grub2/aa-bb-cc-dd-ee-ff/grub.cfg", + "grub2/aa-bb-cc-dd-ee-ff/grub.cfg-01-aa-bb-cc-dd-ee-ff", + "grub2/aa-bb-cc-dd-ee-ff/grub.cfg-aa:bb:cc:dd:ee:ff", + "grub2/grub.cfg-01-aa-bb-cc-dd-ee-ff", + "grub2/grub.cfg-aa:bb:cc:dd:ee:ff", + ] + @pxe_default_files = [] + end +end + class TftpPoapServerTest < Test::Unit::TestCase include TftpGenericServerSuite