Skip to content

Commit 1bb2b00

Browse files
Jan Löserstejskalleos
Jan Löser
authored andcommitted
Fixes #36834 - Add SecureBoot support for arbitrary operating systems to "Grub2 UEFI" PXE loaders
This feature consists of four patches, one each for foreman, smart-proxy, foreman-installer, and puppet-foreman_proxy. This patch adds support for individual Network Bootstrap Programs (NBP) in order to enable network based installations of SecureBoot enabled hosts for arbitrary operating systems. SecureBoot expects to follow a chain of trust from the initial boot of the host to the loading of Linux kernel modules. The very first shim that is loaded determines which distribution is allowed to be booted or kexec'ed until next reboot. Currently the "Grub2 UEFI SecureBoot" PXE loaders use NBPs provided by the vendor of the Foreman/Smart Proxy host system. All hosts receive and execute the same binary. On SecureBoot enabled hosts, this limits installations to operating systems by the vendor of the Foreman/ Smart Proxy host system. Providing shim and GRUB2 by the vendor of the operating system to be installed 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/GRUB2 binary in a host specific directory based on their MAC address. Corresponding shim and GRUB2 binaries are copied into that directory along with the generated GRUB2 configuration files. When provisioning a host, the Smart Proxy checks in a dedicated directory inside the TFTP root - the so called "bootloader universe" - if NBPs are present matching the operating system, operating system version, and architecture of the host to be installed. If this is the case, these NBPs are copied from the bootloader universe directory to the host specific directory. If not, as a fallback the default NBPs provided by the vendor of the Foreman/Smart Proxy host system are copied from the `:tftproot:/grub2` directory to the host specific directory. Up to now, shim and GRUB2 binaries have to be retrieved and set up in the bootloader universe directory manually according to the documentation. An automatic way to provide OS dependent NBPs will be added in future. In case there are no NBPs present in the bootloader universe matching the operating system, operating system version, and architecture of the host to be installed, the behaviour of the "Grub2 UEFI" PXE loaders does not change to the behavior prior to this feature. Implementation notes: --------------------- * To be future proof (e.g. to be able to provide NBPs in the bootloader universe for other PXE loaders without running into any filename conflicts) and for better structure, the PXE kind is prepended as a first directory level inside the bootloader universe. * The operating system version inside the bootloader universe consists of the major and minor version (if applicable) of the operating system separated by a dot (`.`). If no NBPs are configured for a specific operating system version the fallback directory `default` is used. * To simplify things on Foreman side in future, symlinks are used for the shim (boot-sb.efi) and GRUB2 (boot.efi) binaries. * Inside the TFTP root directory a new directory `host-config` is created for storing all the host specific directories. * Inside the TFTP root directory a new directory `bootloader-universe` is created for storing all the OS specific boot files. * For storage efficiency the shim and GRUB2 binaries from the bootloader universe or the `:tftproot:/grub2` directory are symlinked to the host specific directory. 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: AlmaLinux 8.9 [root@vm ~]# tree /var/lib/tftpboot/bootloader-universe/ /var/lib/tftpboot/bootloader-universe/ └── pxegrub2 └── almalinux ├── 8.9 │   └── x86_64 │   ├── boot.efi -> grubx64.efi │   ├── boot-sb.efi -> shimx64.efi │   ├── grubx64.efi │   └── shimx64.efi └── default └── x86_64 ├── boot.efi -> grubx64.efi ├── boot-sb.efi -> shimx64.efi ├── grubx64.efi └── shimx64.efi [root@vm ~]# hammer host update --id 241 --build true [root@vm ~]# tree /var/lib/tftpboot/host-config /var/lib/tftpboot/host-config └── 00-50-56-a3-41-a8 └── grub2 ├── boot.efi -> ../../../bootloader-universe/grubx64.efi ├── boot-sb.efi -> ../../../bootloader-universe/shimx64.efi ├── grub.cfg ├── grub.cfg-00:50:56:a3:41:a8 ├── grub.cfg-01-00-50-56-a3-41-a8 ├── grubx64.efi -> ../../../bootloader-universe/grubx64.efi ├── os_info └── shimx64.efi -> ../../../bootloader-universe/shimx64.efi [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 = "host-config/00-50-56-b4-75-5e/grub2/boot-sb.efi"; [root@vm ~]# pesign -S -i /var/lib/tftpboot/host-config/00-50-56-b4-75-5e/grub2/boot-sb.efi | grep "Microsoft Windows UEFI Driver Publisher" The signer's common name is Microsoft Windows UEFI Driver Publisher
1 parent a0e75a2 commit 1bb2b00

File tree

10 files changed

+89
-27
lines changed

10 files changed

+89
-27
lines changed

app/models/concerns/orchestration/dhcp.rb

+15-1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,20 @@ def build_dhcp_record(record_mac)
108108
end
109109
end
110110

111+
def dhcp_filename(record_mac)
112+
filename = operatingsystem.boot_filename(host)
113+
if filename.include? "@@subdir@@"
114+
if host.subnet&.tftp&.has_capability?(:TFTP, :bootloader_universe)
115+
filename = filename.gsub("@@subdir@@", "host-config/#{record_mac.tr(':', '-').downcase}")
116+
filename = filename.gsub(/\/grub\w*\.efi$/, "/boot.efi")
117+
return filename.gsub(/\/shim\w*\.efi$/, "/boot-sb.efi")
118+
else
119+
return filename.gsub("@@subdir@@/", "")
120+
end
121+
end
122+
filename
123+
end
124+
111125
# returns a hash of dhcp record settings
112126
def dhcp_attrs(record_mac)
113127
raise ::Foreman::Exception.new(N_("DHCP not supported for this NIC")) unless dhcp?
@@ -124,7 +138,7 @@ def dhcp_attrs(record_mac)
124138

125139
if provision?
126140
dhcp_attr[:nextServer] = boot_server unless host.pxe_loader == 'None'
127-
filename = operatingsystem.boot_filename(host)
141+
filename = dhcp_filename(record_mac)
128142
dhcp_attr[:filename] = filename if filename.present?
129143
if jumpstart?
130144
jumpstart_arguments = os.jumpstart_params host, model.vendor_class

app/models/concerns/orchestration/tftp.rb

+7-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,13 @@ def setTFTP(kind)
8787
logger.info "Deploying TFTP #{kind} configuration for #{host.name}"
8888
each_unique_feasible_tftp_proxy do |proxy|
8989
mac_addresses_for_provisioning.each do |mac_addr|
90-
proxy.set(kind, mac_addr, :pxeconfig => content)
90+
proxy.set(kind, mac_addr, {
91+
:pxeconfig => content,
92+
:targetos => host.operatingsystem.name.downcase,
93+
:release => host.operatingsystem.release,
94+
:arch => host.arch.name,
95+
:bootfile_suffix => host.arch.bootfilename_efi,
96+
})
9197
end
9298
end
9399
else

app/models/concerns/pxe_loader_support.rb

+6-6
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ module PxeLoaderSupport
55
PXE_KINDS = {
66
:PXELinux => /^(pxelinux.*|PXELinux (BIOS|UEFI))$/,
77
:PXEGrub => /^(grub\/|Grub UEFI).*/,
8-
:PXEGrub2 => /^(grub2\/|Grub2 (BIOS|UEFI|ELF)|http.*grub2\/).*/,
8+
:PXEGrub2 => /(^Grub2 (BIOS|UEFI|ELF).*|\/?grub2\/)/,
99
:iPXE => /^((iPXE|http.*\/ipxe-).*|ipxe\.efi|undionly\.kpxe)$/,
1010
}.with_indifferent_access.freeze
1111

@@ -26,11 +26,11 @@ def all_loaders_map(precision = 'x64', httpboot_host = "httpboot_host")
2626
"Grub UEFI" => "grub/grub#{precision}.efi",
2727
"Grub2 BIOS" => "grub2/grub#{precision}.0",
2828
"Grub2 ELF" => "grub2/grub#{precision}.elf",
29-
"Grub2 UEFI" => "grub2/grub#{precision}.efi",
30-
"Grub2 UEFI SecureBoot" => "grub2/shim#{precision}.efi",
31-
"Grub2 UEFI HTTP" => "http://#{httpboot_host}/httpboot/grub2/grub#{precision}.efi",
32-
"Grub2 UEFI HTTPS" => "https://#{httpboot_host}/httpboot/grub2/grub#{precision}.efi",
33-
"Grub2 UEFI HTTPS SecureBoot" => "https://#{httpboot_host}/httpboot/grub2/shim#{precision}.efi",
29+
"Grub2 UEFI" => "@@subdir@@/grub2/grub#{precision}.efi",
30+
"Grub2 UEFI SecureBoot" => "@@subdir@@/grub2/shim#{precision}.efi",
31+
"Grub2 UEFI HTTP" => "http://#{httpboot_host}/httpboot/@@subdir@@/grub2/grub#{precision}.efi",
32+
"Grub2 UEFI HTTPS" => "https://#{httpboot_host}/httpboot/@@subdir@@/grub2/grub#{precision}.efi",
33+
"Grub2 UEFI HTTPS SecureBoot" => "https://#{httpboot_host}/httpboot/@@subdir@@/grub2/shim#{precision}.efi",
3434
"iPXE Embedded" => nil, # renders directly as foreman_url('iPXE')
3535
"iPXE UEFI HTTP" => "http://#{httpboot_host}/httpboot/ipxe-#{precision}.efi",
3636
"iPXE Chain BIOS" => "undionly-ipxe.0",

app/services/proxy_api/tftp.rb

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ def initialize(args)
1010
# [+mac+] : MAC address
1111
# [+args+] : Hash containing
1212
# :pxeconfig => String containing the configuration
13+
# :targetos => String containing the lowercase operating system name
14+
# :release => String containing the operating system major and minor version
15+
# :arch => String containing the operating system architecture
16+
# :bootfile_suffix => String containing the architecture specific boot filename suffix
1317
# Returns : Boolean status
1418
def set(kind, mac, args)
1519
parse(post(args, "#{kind}/#{mac}"))

test/factories/architecture.rb

+4
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,9 @@
55
trait :for_snapshots_x86_64 do
66
name { 'x86_64' }
77
end
8+
9+
trait :x64 do
10+
name { 'x64' }
11+
end
812
end
913
end

test/factories/operatingsystem.rb

+11
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,17 @@
160160
title { 'Red Hat Enterprise Linux 7.5' }
161161
end
162162

163+
factory :rhel9, class: Redhat do
164+
name { 'RHEL' }
165+
major { '9' }
166+
minor { '0' }
167+
type { 'Redhat' }
168+
title { 'Red Hat Enterprise Linux 9.0' }
169+
architectures { [FactoryBot.build(:architecture, :x64)] }
170+
media { [FactoryBot.build(:rhel_for_snapshots)] }
171+
ptables { [FactoryBot.build(:ptable, name: 'ptable')] }
172+
end
173+
163174
factory :for_snapshots_centos_7_0, class: Redhat do
164175
name { 'CentOS' }
165176
major { '7' }

test/models/concerns/pxe_loader_support_test.rb

+17-12
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,28 @@ def setup
3232
assert_equal :PXEGrub, @subject.pxe_loader_kind(@host)
3333
end
3434

35-
test "PXEGrub2 is found for given filename" do
36-
@host.pxe_loader = "grub2/grubx64.efi"
35+
test "PXEGrub2 is found for grubx64.elf filename" do
36+
@host.pxe_loader = "grub2/grubx64.elf"
37+
assert_equal :PXEGrub2, @subject.pxe_loader_kind(@host)
38+
end
39+
40+
test "PXEGrub2 is found for grubx64.0 filename" do
41+
@host.pxe_loader = "grub2/grubx64.0"
42+
assert_equal :PXEGrub2, @subject.pxe_loader_kind(@host)
43+
end
44+
45+
test "PXEGrub2 is found for grubx64.efi filename" do
46+
@host.pxe_loader = "host-config/#{@host.mac.tr(':', '-')}/grub2/grubx64.efi"
3747
assert_equal :PXEGrub2, @subject.pxe_loader_kind(@host)
3848
end
3949

4050
test "PXEGrub2 is found for shimx64.efi filename" do
41-
@host.pxe_loader = "grub2/shimx64.efi"
51+
@host.pxe_loader = "host-config/#{@host.mac.tr(':', '-')}/grub2/shimx64.efi"
4252
assert_equal :PXEGrub2, @subject.pxe_loader_kind(@host)
4353
end
4454

4555
test "PXEGrub2 is found for shimia32.efi filename" do
46-
@host.pxe_loader = "grub2/shimia32.efi"
56+
@host.pxe_loader = "host-config/#{@host.mac.tr(':', '-')}/grub2/shimia32.efi"
4757
assert_equal :PXEGrub2, @subject.pxe_loader_kind(@host)
4858
end
4959

@@ -88,22 +98,17 @@ def setup
8898
end
8999

90100
test "PXEGrub2 is found for http://smart_proxy/tftp/grub2/grubx64.efi filename" do
91-
@host.pxe_loader = "http://smart_proxy/tftp/grub2/grubx64.efi"
101+
@host.pxe_loader = "http://smart_proxy/tftp/host-config/#{@host.mac.tr(':', '-')}/grub2/grubx64.efi"
92102
assert_equal :PXEGrub2, @subject.pxe_loader_kind(@host)
93103
end
94104

95105
test "PXEGrub2 is found for https://smart_proxy/tftp/grub2/grubx64.efi filename" do
96-
@host.pxe_loader = "https://smart_proxy/tftp/grub2/grubx64.efi"
97-
assert_equal :PXEGrub2, @subject.pxe_loader_kind(@host)
98-
end
99-
100-
test "PXEGrub2 is found for https://smart_proxy/tftp/grub2/shimx64.efi filename" do
101-
@host.pxe_loader = "https://smart_proxy/tftp/grub2/shimx64.efi"
106+
@host.pxe_loader = "https://smart_proxy/tftp/host-config/#{@host.mac.tr(':', '-')}/grub2/grubx64.efi"
102107
assert_equal :PXEGrub2, @subject.pxe_loader_kind(@host)
103108
end
104109

105110
test "PXEGrub2 is found for https://smart_proxy/tftp/grub2/shimx64.efi filename" do
106-
@host.pxe_loader = "https://smart_proxy/tftp/grub2/shimx64.efi"
111+
@host.pxe_loader = "https://smart_proxy/tftp/host-config/#{@host.mac.tr(':', '-')}/grub2/shimx64.efi"
107112
assert_equal :PXEGrub2, @subject.pxe_loader_kind(@host)
108113
end
109114

test/models/operatingsystem_test.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -379,13 +379,13 @@ class OperatingsystemTest < ActiveSupport::TestCase
379379
test 'should be the smart proxy and httpboot port for UEFI HTTP' do
380380
SmartProxy.any_instance.expects(:setting).with(:HTTPBoot, 'http_port').returns(1234)
381381
host = FactoryBot.build(:host, :managed, :with_tftp_and_httpboot_subnet, pxe_loader: 'Grub2 UEFI HTTP')
382-
assert_match(%r{http://somewhere.*net:1234/httpboot/grub2/grubx64.efi}, host.operatingsystem.boot_filename(host))
382+
assert_match(%r{http://somewhere.*net:1234/httpboot/@@subdir@@/grub2/grubx64.efi}, host.operatingsystem.boot_filename(host))
383383
end
384384

385385
test 'should be the smart proxy and httpboot port for UEFI HTTPS' do
386386
SmartProxy.any_instance.expects(:setting).with(:HTTPBoot, 'https_port').returns(1235)
387387
host = FactoryBot.build(:host, :managed, :with_tftp_and_httpboot_subnet, pxe_loader: 'Grub2 UEFI HTTPS')
388-
assert_match(%r{https://somewhere.*net:1235/httpboot/grub2/grubx64.efi}, host.operatingsystem.boot_filename(host))
388+
assert_match(%r{https://somewhere.*net:1235/httpboot/@@subdir@@/grub2/grubx64.efi}, host.operatingsystem.boot_filename(host))
389389
end
390390

391391
test 'should not raise an error without httpboot feature for PXE' do

test/models/orchestration/dhcp_test.rb

+4-2
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,8 @@ def host_with_loader(loader)
371371

372372
h.build = true
373373
assert h.valid?, h.errors.messages.to_s
374-
assert_equal ["dhcp_remove_aa:bb:cc:dd:ee:f1", "dhcp_create_aa:bb:cc:dd:ee:f1"], h.queue.task_ids
374+
assert_includes h.queue.task_ids, "dhcp_remove_aa:bb:cc:dd:ee:f1"
375+
assert_includes h.queue.task_ids, "dhcp_create_aa:bb:cc:dd:ee:f1"
375376
end
376377

377378
test "when an existing host trigger a 'rebuild', its dhcp records should not be updated if valid dhcp records are found" do
@@ -383,7 +384,8 @@ def host_with_loader(loader)
383384
h.build = true
384385
assert h.valid?
385386
assert_empty h.errors
386-
assert_equal ["dhcp_create_aa:bb:cc:dd:ee:f1"], h.queue.task_ids
387+
assert_includes h.queue.task_ids, "dhcp_create_aa:bb:cc:dd:ee:f1"
388+
assert_not_includes h.queue.task_ids, "dhcp_remove_aa:bb:cc:dd:ee:f1"
387389
end
388390

389391
test "when an existing host change its bmc mac address, its dhcp record should be updated" do

test/models/orchestration/tftp_test.rb

+19-3
Original file line numberDiff line numberDiff line change
@@ -145,26 +145,42 @@ class TFTPOrchestrationTest < ActiveSupport::TestCase
145145
),
146146
]
147147
end
148+
let(:os) do
149+
FactoryBot.create(:rhel9)
150+
end
148151
let(:host) do
149152
FactoryBot.create(:host,
150153
:with_tftp_orchestration,
151154
:subnet => subnet,
152155
:interfaces => interfaces,
153156
:build => true,
154157
:location => subnet.locations.first,
155-
:organization => subnet.organizations.first)
158+
:organization => subnet.organizations.first,
159+
:operatingsystem => os)
156160
end
157161

158162
test '#setTFTP should provision tftp for all bond child macs' do
159163
ProxyAPI::TFTP.any_instance.expects(:set).with(
160164
'PXEGrub2',
161165
'00:53:67:ab:dd:00',
162-
:pxeconfig => 'Template'
166+
{
167+
:pxeconfig => 'Template',
168+
:targetos => os.name.downcase.to_s,
169+
:release => host.operatingsystem.release,
170+
:arch => host.arch.name,
171+
:bootfile_suffix => host.arch.bootfilename_efi,
172+
}
163173
).once
164174
ProxyAPI::TFTP.any_instance.expects(:set).with(
165175
'PXEGrub2',
166176
'00:53:67:ab:dd:01',
167-
:pxeconfig => 'Template'
177+
{
178+
:pxeconfig => 'Template',
179+
:targetos => os.name.downcase.to_s,
180+
:release => host.operatingsystem.release,
181+
:arch => host.arch.name,
182+
:bootfile_suffix => host.arch.bootfilename_efi,
183+
}
168184
).once
169185
host.provision_interface.stubs(:generate_pxe_template).returns('Template')
170186
host.provision_interface.send(:setTFTP, 'PXEGrub2')

0 commit comments

Comments
 (0)