Skip to content

Commit

Permalink
feat: implement secure boot from disk
Browse files Browse the repository at this point in the history
This includes sd-boot handling, EFI variables, etc.

There are some TODOs which need to be addressed to make things smooth.

Install to disk, upgrades work.

Signed-off-by: Andrey Smirnov <andrey.smirnov@talos-systems.com>
  • Loading branch information
smira authored and frezbo committed Jun 16, 2023
1 parent 19bc223 commit 5e633c0
Show file tree
Hide file tree
Showing 19 changed files with 441 additions and 35 deletions.
10 changes: 10 additions & 0 deletions .drone.jsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,15 @@ local default_pipeline_steps = [

local integration_qemu = Step('e2e-qemu', privileged=true, depends_on=[load_artifacts], environment={ IMAGE_REGISTRY: local_registry });

local integration_qemu_trusted_boot = Step('e2e-qemu-trusted-boot', target='e2e-qemu', privileged=true, depends_on=[load_artifacts], environment={
// TODO: frezbo: do we need the full suite here?
SHORT_INTEGRATION_TEST: 'yes',
IMAGE_REGISTRY: local_registry,
VIA_MAINTENANCE_MODE: "true",
WITH_TRUSTED_BOOT: "true",
WITH_TEST: "validate_booted_secureboot"
});

local build_race = Step('build-race', target='initramfs installer', depends_on=[load_artifacts], environment={ IMAGE_REGISTRY: local_registry, PUSH: true, TAG_SUFFIX: '-race', WITH_RACE: '1', PLATFORM: 'linux/amd64' });
local integration_qemu_race = Step('e2e-qemu-race', target='e2e-qemu', privileged=true, depends_on=[build_race], environment={ IMAGE_REGISTRY: local_registry, TAG_SUFFIX: '-race' });

Expand Down Expand Up @@ -576,6 +585,7 @@ local integration_trigger(names) = {
local integration_pipelines = [
// regular pipelines, triggered on promote events
Pipeline('integration-qemu', default_pipeline_steps + [integration_qemu, push_edge]) + integration_trigger(['integration-qemu']),
Pipeline('integration-trusted-boot', default_pipeline_steps + [integration_qemu_trusted_boot]) + integration_trigger(['integration-trusted-boot']),
Pipeline('integration-provision-0', default_pipeline_steps + [integration_provision_tests_prepare, integration_provision_tests_track_0]) + integration_trigger(['integration-provision', 'integration-provision-0']),
Pipeline('integration-provision-1', default_pipeline_steps + [integration_provision_tests_prepare, integration_provision_tests_track_1]) + integration_trigger(['integration-provision', 'integration-provision-1']),
Pipeline('integration-provision-2', default_pipeline_steps + [integration_provision_tests_prepare, integration_provision_tests_track_2]) + integration_trigger(['integration-provision', 'integration-provision-2']),
Expand Down
12 changes: 10 additions & 2 deletions cmd/installer/pkg/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ func NewInstaller(cmdline *procfs.Cmdline, seq runtime.Sequence, opts *Options)
}
}

i.manifest, err = NewManifest(seq, bootLoaderPresent, i.options)
i.manifest, err = NewManifest(seq, i.bootloader.UEFIBoot(), bootLoaderPresent, i.options)
if err != nil {
return nil, fmt.Errorf("failed to create installation manifest: %w", err)
}
Expand Down Expand Up @@ -158,7 +158,15 @@ func (i *Installer) Install(seq runtime.Sequence) (err error) {
// Mount the partitions.
mountpoints := mount.NewMountPoints()

for _, label := range []string{constants.BootPartitionLabel, constants.EFIPartitionLabel} {
var bootLabels []string

if i.bootloader.UEFIBoot() {
bootLabels = []string{constants.EFIPartitionLabel}
} else {
bootLabels = []string{constants.BootPartitionLabel, constants.EFIPartitionLabel}
}

for _, label := range bootLabels {
err = func() error {
var device string
// searching targets for the device to be used
Expand Down
54 changes: 34 additions & 20 deletions cmd/installer/pkg/install/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,18 @@ type Device struct {
// NewManifest initializes and returns a Manifest.
//
//nolint:gocyclo
func NewManifest(sequence runtime.Sequence, bootLoaderPresent bool, opts *Options) (manifest *Manifest, err error) {
func NewManifest(sequence runtime.Sequence, uefiOnlyBoot bool, bootLoaderPresent bool, opts *Options) (manifest *Manifest, err error) {
manifest = &Manifest{
Devices: map[string]Device{},
Targets: map[string][]*Target{},
LegacyBIOSSupport: opts.LegacyBIOSSupport,
}

if opts.Board != constants.BoardNone {
if uefiOnlyBoot {
return nil, fmt.Errorf("board option can't be used with uefi-only-boot")
}

var b runtime.Board

b, err = board.NewBoard(opts.Board)
Expand Down Expand Up @@ -107,27 +111,37 @@ func NewManifest(sequence runtime.Sequence, bootLoaderPresent bool, opts *Option
manifest.Targets[opts.Disk] = []*Target{}
}

efiTarget := EFITarget(opts.Disk, nil)
biosTarget := BIOSTarget(opts.Disk, nil)

bootTarget := BootTarget(opts.Disk, &Target{
PreserveContents: bootLoaderPresent,
})

metaTarget := MetaTarget(opts.Disk, &Target{
PreserveContents: bootLoaderPresent,
})

stateTarget := StateTarget(opts.Disk, &Target{
PreserveContents: bootLoaderPresent,
FormatOptions: &partition.FormatOptions{
FileSystemType: partition.FilesystemTypeNone,
},
})
targets := []*Target{}

ephemeralTarget := EphemeralTarget(opts.Disk, NoFilesystem)
// create GRUB BIOS+UEFI partitions, or only one big EFI partition if not using GRUB
if !uefiOnlyBoot {
targets = append(targets,
EFITarget(opts.Disk, nil),
BIOSTarget(opts.Disk, nil),
BootTarget(opts.Disk, &Target{
PreserveContents: bootLoaderPresent,
}),
)
} else {
targets = append(targets,
EFITargetUKI(opts.Disk, &Target{
PreserveContents: bootLoaderPresent,
}),
)
}

targets := []*Target{efiTarget, biosTarget, bootTarget, metaTarget, stateTarget, ephemeralTarget}
targets = append(targets,
MetaTarget(opts.Disk, &Target{
PreserveContents: bootLoaderPresent,
}),
StateTarget(opts.Disk, &Target{
PreserveContents: bootLoaderPresent,
FormatOptions: &partition.FormatOptions{
FileSystemType: partition.FilesystemTypeNone,
},
}),
EphemeralTarget(opts.Disk, NoFilesystem),
)

if !opts.Force {
for _, target := range targets {
Expand Down
10 changes: 5 additions & 5 deletions cmd/installer/pkg/install/manifest_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ const extendedStateSize = 300 * 1024 * 1024
func (suite *manifestSuite) TestExecuteManifestClean() {
suite.skipUnderBuildkit()

manifest, err := install.NewManifest(runtime.SequenceInstall, false, &install.Options{
manifest, err := install.NewManifest(runtime.SequenceInstall, false, false, &install.Options{
Disk: suite.loopbackDevice.Name(),
Force: true,
Board: constants.BoardNone,
Expand All @@ -247,7 +247,7 @@ func (suite *manifestSuite) TestExecuteManifestClean() {
func (suite *manifestSuite) TestExecuteManifestForce() {
suite.skipUnderBuildkit()

manifest, err := install.NewManifest(runtime.SequenceInstall, false, &install.Options{
manifest, err := install.NewManifest(runtime.SequenceInstall, false, false, &install.Options{
Disk: suite.loopbackDevice.Name(),
Force: true,
Board: constants.BoardNone,
Expand All @@ -272,7 +272,7 @@ func (suite *manifestSuite) TestExecuteManifestForce() {

// reinstall

manifest, err = install.NewManifest(runtime.SequenceUpgrade, true, &install.Options{
manifest, err = install.NewManifest(runtime.SequenceUpgrade, false, true, &install.Options{
Disk: suite.loopbackDevice.Name(),
Force: true,
Zero: true,
Expand Down Expand Up @@ -300,7 +300,7 @@ func (suite *manifestSuite) TestExecuteManifestForce() {
func (suite *manifestSuite) TestExecuteManifestPreserve() {
suite.skipUnderBuildkit()

manifest, err := install.NewManifest(runtime.SequenceInstall, false, &install.Options{
manifest, err := install.NewManifest(runtime.SequenceInstall, false, false, &install.Options{
Disk: suite.loopbackDevice.Name(),
Force: true,
Board: constants.BoardNone,
Expand All @@ -325,7 +325,7 @@ func (suite *manifestSuite) TestExecuteManifestPreserve() {

// reinstall

manifest, err = install.NewManifest(runtime.SequenceUpgrade, true, &install.Options{
manifest, err = install.NewManifest(runtime.SequenceUpgrade, false, true, &install.Options{
Disk: suite.loopbackDevice.Name(),
Force: false,
Board: constants.BoardNone,
Expand Down
4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ require (
github.com/docker/docker v24.0.2+incompatible
github.com/docker/go-connections v0.4.0
github.com/dustin/go-humanize v1.0.1
github.com/ecks/uefi v0.0.0-20221116212947-caef65d070eb
github.com/emicklei/dot v1.4.2
github.com/fatih/color v1.15.0
github.com/freddierice/go-losetup/v2 v2.0.1
Expand Down Expand Up @@ -128,6 +129,7 @@ require (
golang.org/x/sync v0.2.0
golang.org/x/sys v0.8.0
golang.org/x/term v0.8.0
golang.org/x/text v0.9.0
golang.org/x/time v0.3.0
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
google.golang.org/grpc v1.55.0
Expand All @@ -143,6 +145,7 @@ require (
cloud.google.com/go/compute v1.19.0 // indirect
cloud.google.com/go/iam v0.13.0 // indirect
cloud.google.com/go/storage v1.28.1 // indirect
github.com/0x5a17ed/itkit v0.6.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/MakeNowJust/heredoc v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.0 // indirect
Expand Down Expand Up @@ -280,7 +283,6 @@ require (
golang.org/x/crypto v0.8.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/oauth2 v0.6.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/tools v0.7.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
Expand Down
6 changes: 5 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuW
cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0=
cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/0x5a17ed/itkit v0.6.0 h1:g1SnJQM61e0nAEk0Qu7cGGiL4zOHrk7ta55KoKwRcCs=
github.com/0x5a17ed/itkit v0.6.0/go.mod h1:v22t2Uc3bKewFBwLkY2U1KM7Us8iiEWw3qGqJFU76rI=
github.com/Azure/azure-sdk-for-go v16.2.1+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
Expand Down Expand Up @@ -509,6 +511,8 @@ github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:Htrtb
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/ecks/uefi v0.0.0-20221116212947-caef65d070eb h1:LZBZtPpqHDydudNAs2sHmo4Zp9bxEyxHdGCk3Fr6tv8=
github.com/ecks/uefi v0.0.0-20221116212947-caef65d070eb/go.mod h1:jP/WitZVr91050NiqxEEp0ynBFbP2eUQC0CnxWPlQTA=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/emicklei/dot v1.4.2 h1:UbK6gX4yvrpHKlxuUQicwoAis4zl8Dzwit9SnbBAXWw=
github.com/emicklei/dot v1.4.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
Expand Down Expand Up @@ -2041,8 +2045,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk=
gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 h1:Wobr37noukisGxpKo5jAsLREcpj61RxrWYzD8uwveOY=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
Expand Down
8 changes: 8 additions & 0 deletions hack/test/e2e-qemu.sh
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,14 @@ case "${WITH_SKIP_BOOT_PHASE_FINISHED_CHECK:-no}" in
;;
esac

case "${WITH_TRUSTED_BOOT:-false}" in
false)
;;
*)
QEMU_FLAGS="${QEMU_FLAGS} --iso-path=_out/talos-uki-amd64.iso --with-secureboot=true --with-tpm2=true"
;;
esac

function create_cluster {
build_registry_mirrors

Expand Down
4 changes: 4 additions & 0 deletions hack/test/e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,10 @@ function validate_rlimit_nofile {
${KUBECTL} run --rm --restart=Never -it rlimit-test --image=alpine -- /bin/sh -c "ulimit -n" | grep 1048576
}

function validate_booted_secureboot {
${TALOSCTL} dmesg | grep -q "Secure boot enabled"
}

function install_and_run_cilium_cni_tests {
get_kubeconfig

Expand Down
2 changes: 1 addition & 1 deletion hack/ukify/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ func run() error {
}{
Name: version.Name,
ID: strings.ToLower(version.Name),
Version: version.Tag,
Version: version.Tag, // TODO: this is abbrev tag, not the full tag, it should be fixed by building from sources + generate data
}); err != nil {
return err
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"os"

"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub"
"github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot"
)

// Bootloader describes a bootloader.
Expand All @@ -19,6 +20,8 @@ type Bootloader interface {
Revert() error
// PreviousLabel returns the previous bootloader label.
PreviousLabel() string
// UEFIBoot returns true if the bootloader is UEFI-only.
UEFIBoot() bool
}

// Probe checks if any supported bootloaders are installed.
Expand All @@ -31,14 +34,29 @@ func Probe(disk string) (Bootloader, error) {
return nil, err
}

if grubBootloader == nil {
return nil, os.ErrNotExist
if grubBootloader != nil {
return grubBootloader, nil
}

return grubBootloader, nil
sdbootBootloader, err := sdboot.Probe(disk)
if err != nil {
return nil, err
}

if sdbootBootloader != nil {
return sdbootBootloader, nil
}

return nil, os.ErrNotExist
}

// New returns a new bootloader.
func New() (Bootloader, error) {
// TODO: there should be a way to force sd-boot/GRUB based on installer args,
// to build a disk image with specified bootloader.
if sdboot.IsBootedUsingSDBoot() {
return sdboot.New(), nil
}

return grub.NewConfig(), nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ func NewConfig() *Config {
}
}

// UEFIBoot returns true if bootloader is UEFI-only.
func (c *Config) UEFIBoot() bool {
// grub supports BIOS boot, so false here.
return false
}

// Put puts a new menu entry to the grub config (nothing is written to disk).
func (c *Config) Put(entry bootloader.BootLabel, cmdline string) error {
c.Entries[entry] = buildMenuEntry(entry, cmdline)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

package sdboot

import (
"errors"

"github.com/ecks/uefi/efi/efiguid"
"github.com/ecks/uefi/efi/efivario"
"golang.org/x/text/encoding/unicode"
)

// SystemdBootGUIDString is the GUID of the SystemdBoot EFI variables.
const SystemdBootGUIDString = "4a67b082-0a4c-41cf-b6c7-440b29bb8c4f"

// SystemdBootGUID is the GUID of the SystemdBoot EFI variables.
var SystemdBootGUID = efiguid.MustFromString(SystemdBootGUIDString)

// Variable names.
const (
LoaderEntryDefaultName = "LoaderEntryDefault"
LoaderEntrySelectedName = "LoaderEntrySelected"
LoaderConfigTimeoutName = "LoaderConfigTimeout"
)

// ReadVariable reads a SystemdBoot EFI variable.
func ReadVariable(c efivario.Context, name string) (string, error) {
_, data, err := efivario.ReadAll(c, name, SystemdBootGUID)
if err != nil {
if errors.Is(err, efivario.ErrNotFound) {
return "", nil
}

return "", err
}

out := make([]byte, len(data))

decoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewDecoder()

n, _, err := decoder.Transform(out, data, true)
if err != nil {
return "", err
}

if n > 0 && out[n-1] == 0 {
n--
}

return string(out[:n]), nil
}

// WriteVariable reads a SystemdBoot EFI variable.
func WriteVariable(c efivario.Context, name, value string) error {
out := make([]byte, (len(value)+1)*2)

encoder := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM).NewEncoder()

n, _, err := encoder.Transform(out, []byte(value), true)
if err != nil {
return err
}

out = append(out[:n], 0, 0)

return c.Set(name, SystemdBootGUID, efivario.BootServiceAccess|efivario.RuntimeAccess|efivario.NonVolatile, out)
}
Loading

0 comments on commit 5e633c0

Please sign in to comment.