Skip to content

Commit

Permalink
Fixes #36833 - New PXE loader "Grub2 UEFI SecureBoot (target OS)"
Browse files Browse the repository at this point in the history
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/<os>/`. 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)
  • Loading branch information
Jan Löser authored and goarsna committed Feb 28, 2024
1 parent bce882c commit 012295c
Show file tree
Hide file tree
Showing 7 changed files with 107 additions and 8 deletions.
4 changes: 4 additions & 0 deletions config/settings.d/tftp.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -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:
77 changes: 74 additions & 3 deletions modules/tftp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -95,16 +107,75 @@ 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
["#{pxeconfig_dir}/grub.cfg"]
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

Expand Down
7 changes: 4 additions & 3 deletions modules/tftp/tftp_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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|
Expand All @@ -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|
Expand Down
2 changes: 2 additions & 0 deletions modules/tftp/tftp_plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
4 changes: 2 additions & 2 deletions test/tftp/integration_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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'])
Expand Down
5 changes: 5 additions & 0 deletions test/tftp/tftp_api_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 16 additions & 0 deletions test/tftp/tftp_server_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit 012295c

Please sign in to comment.