From fe0f46980f348852907218d6f49581efe4b45d49 Mon Sep 17 00:00:00 2001 From: Andrey Smirnov Date: Wed, 14 Jun 2023 22:35:33 +0400 Subject: [PATCH] feat: implement secure boot from disk 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 --- .drone.jsonnet | 10 + Dockerfile | 27 +-- cmd/installer/pkg/install/install.go | 12 +- cmd/installer/pkg/install/manifest.go | 54 +++-- cmd/installer/pkg/install/manifest_test.go | 49 ++--- go.mod | 4 +- go.sum | 6 +- hack/test/e2e-qemu.sh | 8 + hack/test/e2e.sh | 4 + .../runtime/v1alpha1/bootloader/bootloader.go | 24 ++- .../runtime/v1alpha1/bootloader/grub/grub.go | 6 + .../v1alpha1/bootloader/sdboot/efivars.go | 69 ++++++ .../v1alpha1/bootloader/sdboot/sdboot.go | 201 ++++++++++++++++++ internal/pkg/install/install.go | 7 + internal/pkg/mount/pseudo.go | 10 + pkg/machinery/constants/constants.go | 16 ++ pkg/provision/providers/qemu/arch.go | 14 +- pkg/provision/providers/qemu/launch.go | 2 + pkg/provision/providers/qemu/pflash.go | 15 +- 19 files changed, 461 insertions(+), 77 deletions(-) create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/efivars.go create mode 100644 internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/sdboot.go diff --git a/.drone.jsonnet b/.drone.jsonnet index a9aa1f4c3f..d9240e3e7e 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -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' }); @@ -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']), diff --git a/Dockerfile b/Dockerfile index 506c2158fb..1b408c405f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -143,15 +143,6 @@ COPY ./hack/structprotogen /go/src/github.com/siderolabs/structprotogen RUN --mount=type=cache,target=/.cache cd /go/src/github.com/siderolabs/structprotogen \ && go build -o structprotogen . \ && mv structprotogen /toolchain/go/bin/ -COPY ./hack/ukify /go/src/github.com/siderolabs/ukify -RUN --mount=type=cache,target=/.cache \ - --mount=type=bind,source=pkg,target=/go/src/github.com/pkg \ - cd /go/src/github.com/siderolabs/ukify \ - && CGO_ENABLED=1 go test ./... \ - && go build -o gen-uki-certs ./gen-certs \ - && CGO_ENABLED=1 go build -o ukify . \ - && mv gen-uki-certs /toolchain/go/bin/ \ - && mv ukify /toolchain/go/bin/ # The build target creates a container that will be used to build Talos source # code. @@ -706,13 +697,25 @@ COPY --from=rootfs / / LABEL org.opencontainers.image.source https://github.com/siderolabs/talos ENTRYPOINT ["/sbin/init"] -FROM --platform=${BUILDPLATFORM} tools AS gen-uki-certs +FROM --platform=${BUILDPLATFORM} tools AS ukify-tools +# base has the talos source with the non-abrev version of TAG +COPY --from=base /src/pkg /go/src/github.com/pkg +COPY ./hack/ukify /go/src/github.com/siderolabs/ukify +RUN --mount=type=cache,target=/.cache \ + cd /go/src/github.com/siderolabs/ukify \ + && CGO_ENABLED=1 go test ./... \ + && go build -o gen-uki-certs ./gen-certs \ + && CGO_ENABLED=1 go build -o ukify . \ + && mv gen-uki-certs /toolchain/go/bin/ \ + && mv ukify /toolchain/go/bin/ + +FROM --platform=${BUILDPLATFORM} ukify-tools AS gen-uki-certs RUN gen-uki-certs FROM scratch as uki-certs COPY --from=gen-uki-certs /_out / -FROM --platform=${BUILDPLATFORM} tools AS uki-build-amd64 +FROM --platform=${BUILDPLATFORM} ukify-tools AS uki-build-amd64 WORKDIR /build COPY --from=pkg-sd-stub-amd64 / _out/ COPY --from=pkg-sd-boot-amd64 / _out/ @@ -725,7 +728,7 @@ FROM scratch AS uki-amd64 COPY --from=uki-build-amd64 /build/_out/systemd-bootx64.efi.signed /systemd-boot.efi.signed COPY --from=uki-build-amd64 /build/_out/vmlinuz.efi.signed /vmlinuz.efi.signed -FROM --platform=${BUILDPLATFORM} tools AS uki-build-arm64 +FROM --platform=${BUILDPLATFORM} ukify-tools AS uki-build-arm64 WORKDIR /build COPY --from=pkg-sd-stub-arm64 / _out/ COPY --from=pkg-sd-boot-arm64 / _out/ diff --git a/cmd/installer/pkg/install/install.go b/cmd/installer/pkg/install/install.go index f839dc6321..bd8adc89e6 100644 --- a/cmd/installer/pkg/install/install.go +++ b/cmd/installer/pkg/install/install.go @@ -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) } @@ -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 diff --git a/cmd/installer/pkg/install/manifest.go b/cmd/installer/pkg/install/manifest.go index 65da10e764..fcbc256988 100644 --- a/cmd/installer/pkg/install/manifest.go +++ b/cmd/installer/pkg/install/manifest.go @@ -47,7 +47,7 @@ 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{}, @@ -55,6 +55,10 @@ func NewManifest(sequence runtime.Sequence, bootLoaderPresent bool, opts *Option } 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) @@ -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 { diff --git a/cmd/installer/pkg/install/manifest_test.go b/cmd/installer/pkg/install/manifest_test.go index 08bdbd2824..f0b00ab85d 100644 --- a/cmd/installer/pkg/install/manifest_test.go +++ b/cmd/installer/pkg/install/manifest_test.go @@ -63,6 +63,13 @@ func (suite *manifestSuite) SetupTest() { suite.Require().NoError(loopback.Loop(suite.loopbackDevice, suite.disk)) suite.Require().NoError(loopback.LoopSetReadWrite(suite.loopbackDevice)) + + // set the env vars xfsprogs expects to use Talos STATE partition which is 100Megs + // whereas xfs expects a default minimum size of 300Megs if these are not set. + // Ref: https://git.kernel.org/pub/scm/fs/xfs/xfsprogs-dev.git/tree/mkfs/xfs_mkfs.c?h=v6.3.0#n2582 + suite.T().Setenv("TEST_DIR", "true") + suite.T().Setenv("TEST_DEV", "true") + suite.T().Setenv("QA_CHECK_FS", "true") } func (suite *manifestSuite) TearDownTest() { @@ -132,7 +139,7 @@ func (suite *manifestSuite) verifyBlockdevice(manifest *install.Manifest, curren suite.Assert().Equal(constants.StatePartitionLabel, part.Name) suite.Assert().EqualValues(0, part.Attributes) - suite.Assert().EqualValues(extendedStateSize/lbaSize, part.Length()) + suite.Assert().EqualValues(partition.StateSize/lbaSize, part.Length()) part = table.Partitions().Items()[5] suite.Assert().Equal(partition.LinuxFilesystemData, strings.ToUpper(part.Type.String())) @@ -215,12 +222,10 @@ func (suite *manifestSuite) verifyBlockdevice(manifest *install.Manifest, curren suite.Assert().NoError(os.WriteFile(filepath.Join(tempDir, "var", "content"), []byte("data"), 0o600)) } -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, @@ -232,13 +237,6 @@ func (suite *manifestSuite) TestExecuteManifestClean() { dev.SkipOverlayMountsCheck = true manifest.Devices[suite.loopbackDevice.Name()] = dev - // increase the size of the state partition - for _, t := range manifest.Targets[suite.loopbackDevice.Name()] { - if t.Label == constants.StatePartitionLabel { - t.Size = extendedStateSize - } - } - suite.Assert().NoError(manifest.Execute()) suite.verifyBlockdevice(manifest, "", "A", false, false) @@ -247,7 +245,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, @@ -259,20 +257,13 @@ func (suite *manifestSuite) TestExecuteManifestForce() { dev.SkipOverlayMountsCheck = true manifest.Devices[suite.loopbackDevice.Name()] = dev - // increase the size of the state partition - for _, t := range manifest.Targets[suite.loopbackDevice.Name()] { - if t.Label == constants.StatePartitionLabel { - t.Size = extendedStateSize - } - } - suite.Assert().NoError(manifest.Execute()) suite.verifyBlockdevice(manifest, "", "A", false, false) // 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, @@ -285,13 +276,6 @@ func (suite *manifestSuite) TestExecuteManifestForce() { dev.SkipOverlayMountsCheck = true manifest.Devices[suite.loopbackDevice.Name()] = dev - // increase the size of the state partition - for _, t := range manifest.Targets[suite.loopbackDevice.Name()] { - if t.Label == constants.StatePartitionLabel { - t.Size = extendedStateSize - } - } - suite.Assert().NoError(manifest.Execute()) suite.verifyBlockdevice(manifest, "A", "B", true, false) @@ -300,20 +284,13 @@ 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, }) suite.Require().NoError(err) - // increase the size of the state partition - for _, t := range manifest.Targets[suite.loopbackDevice.Name()] { - if t.Label == constants.StatePartitionLabel { - t.Size = extendedStateSize - } - } - // in the tests overlay mounts should be ignored dev := manifest.Devices[suite.loopbackDevice.Name()] dev.SkipOverlayMountsCheck = true @@ -325,7 +302,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, diff --git a/go.mod b/go.mod index e6ff974de1..54d1d1a83c 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/go.sum b/go.sum index 7050ecf84b..dd4e443d9c 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= @@ -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= diff --git a/hack/test/e2e-qemu.sh b/hack/test/e2e-qemu.sh index 995f0d2f13..b4c86a0aed 100755 --- a/hack/test/e2e-qemu.sh +++ b/hack/test/e2e-qemu.sh @@ -145,6 +145,14 @@ case "${CUSTOM_CNI_NAME:-none}" 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 diff --git a/hack/test/e2e.sh b/hack/test/e2e.sh index e5b5cc105e..032a21c8dc 100755 --- a/hack/test/e2e.sh +++ b/hack/test/e2e.sh @@ -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 "Secure boot enabled" +} + function install_and_run_cilium_cni_tests { get_kubeconfig diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go index 4141c0c544..3c01dc83f8 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/bootloader.go @@ -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. @@ -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. @@ -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 } diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go index 9f6fe4d084..81e95b47e0 100644 --- a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/grub/grub.go @@ -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) diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/efivars.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/efivars.go new file mode 100644 index 0000000000..683ea4ea08 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/efivars.go @@ -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) +} diff --git a/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/sdboot.go b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/sdboot.go new file mode 100644 index 0000000000..0835df7b04 --- /dev/null +++ b/internal/app/machined/pkg/runtime/v1alpha1/bootloader/sdboot/sdboot.go @@ -0,0 +1,201 @@ +// 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 provides the interface to the Systemd-Boot bootloader: config management, installation, etc. +package sdboot + +import ( + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/ecks/uefi/efi/efivario" + "github.com/siderolabs/go-blockdevice/blockdevice" + + "github.com/siderolabs/talos/internal/app/machined/pkg/runtime/v1alpha1/bootloader/assets" + "github.com/siderolabs/talos/pkg/machinery/constants" + "github.com/siderolabs/talos/pkg/version" +) + +// Config describe sd-boot state. +type Config struct { + Default string + Fallback string +} + +func isUEFIBoot() bool { + // https://renenyffenegger.ch/notes/Linux/fhs/sys/firmware/efi/index + _, err := os.Stat("/sys/firmware/efi") + + return err == nil +} + +// IsBootedUsingSDBoot returns true if the system is booted using sd-boot. +func IsBootedUsingSDBoot() bool { + // https://www.freedesktop.org/software/systemd/man/systemd-stub.html#EFI%20Variables + // https://www.freedesktop.org/software/systemd/man/systemd-stub.html#StubInfo + _, err := os.Stat(fmt.Sprintf("/sys/firmware/efi/efivars/StubInfo-%s", SystemdBootGUIDString)) + + return err == nil +} + +// New creates a new sdboot bootloader config. +func New() *Config { + return &Config{} +} + +// Probe for existing sd-boot bootloader. +func Probe(disk string) (*Config, error) { + // if not UEFI boot, nothing to do + if !isUEFIBoot() { + return nil, nil + } + + if !IsBootedUsingSDBoot() { + return nil, nil + } + + // TODO: read /boot/EFI and find if sd-boot is already being used + + // here we need to read the EFI vars to see if we have any defaults + // and populate config accordingly + // https://www.freedesktop.org/software/systemd/man/systemd-boot.html#LoaderEntryDefault + // this should be set on install/upgrades + + efiCtx := efivario.NewDefaultContext() + + bootedEntry, err := ReadVariable(efiCtx, LoaderEntrySelectedName) + if err != nil { + return nil, err + } + + log.Printf("booted entry: %q", bootedEntry) + + // TODO: verify that bootedEntry is in the EFI partition + + return &Config{ + Default: bootedEntry, + }, nil +} + +// UEFIBoot returns true if bootloader is UEFI-only. +func (c *Config) UEFIBoot() bool { + return true +} + +// Install the bootloader. +// +// Assumes that EFI partition is already mounted. +// Writes down the UKI and updates the EFI variables. +// +//nolint:gocyclo +func (c *Config) Install(bootDisk, arch, cmdline string) error { + var sdbootFilename string + + switch arch { + case "amd64": + sdbootFilename = "BOOTX64.efi" + case "arm64": + sdbootFilename = "BOOTAA64.efi" + default: + return fmt.Errorf("unsupported architecture: %s", arch) + } + + // list existing UKIs, and clean up all but the current one (used to boot) + files, err := filepath.Glob(filepath.Join(constants.EFIMountPoint, "EFI", "Linux", "Talos-*.efi")) + if err != nil { + return err + } + + // writing UKI by version-based filename here + ukiPath := fmt.Sprintf("%s-%s.efi", "Talos", version.Tag) + + for _, file := range files { + if strings.EqualFold(filepath.Base(file), c.Default) { + if !strings.EqualFold(c.Default, ukiPath) { + // set fallback to the current default unless it matches the new install + c.Fallback = c.Default + } + + continue + } + + log.Printf("removing old UKI: %s", file) + + if err = os.Remove(file); err != nil { + return err + } + } + + assets := assets.Assets{ + { + Source: fmt.Sprintf(constants.UKIAssetPath, arch), + Destination: filepath.Join(constants.EFIMountPoint, "EFI", "Linux", ukiPath), + }, + { + Source: fmt.Sprintf(constants.SDBootAssetPath, arch), + Destination: filepath.Join(constants.EFIMountPoint, "EFI", "boot", sdbootFilename), + }, + } + if err = assets.Install(); err != nil { + return err + } + + blk, err := getBlockDeviceName(bootDisk) + if err != nil { + return err + } + + loopDevice := strings.HasPrefix(blk, "/dev/loop") + + // don't update EFI variables if we're installing to a loop device + if !loopDevice { + efiCtx := efivario.NewDefaultContext() + + // set the new entry as a default one + if err := WriteVariable(efiCtx, LoaderEntryDefaultName, ukiPath); err != nil { + return err + } + + // set default 5 second boot timeout + if err := WriteVariable(efiCtx, LoaderConfigTimeoutName, "5"); err != nil { + return err + } + } + + return nil +} + +// PreviousLabel returns the label of the previous bootloader version. +func (c *Config) PreviousLabel() string { + return c.Fallback +} + +// Revert the bootloader to the previous version. +func (c *Config) Revert() error { + // TODO: use c.Default as the current entry, list other UKIs, find the one which is not c.Default, and update EFI var + panic("not implemented") +} + +func getBlockDeviceName(bootDisk string) (string, error) { + dev, err := blockdevice.Open(bootDisk, blockdevice.WithMode(blockdevice.ReadonlyMode)) + if err != nil { + return "", err + } + + //nolint:errcheck + defer dev.Close() + + // verify that BootDisk has EFI partition + _, err = dev.GetPartition(constants.EFIPartitionLabel) + if err != nil { + return "", err + } + + blk := dev.Device().Name() + + return blk, nil +} diff --git a/internal/pkg/install/install.go b/internal/pkg/install/install.go index eda676fcc7..e729642b8c 100644 --- a/internal/pkg/install/install.go +++ b/internal/pkg/install/install.go @@ -137,6 +137,13 @@ func RunInstallerContainer(disk, platform, ref string, cfg configcore.Config, cf ) } + // mount the efivars into the container if the efivars directory exists + if _, err = os.Stat(constants.EFIVarsMountPoint); err == nil { + mounts = append(mounts, + specs.Mount{Type: "efivarfs", Source: "efivarfs", Destination: constants.EFIVarsMountPoint, Options: []string{"rw", "nosuid", "nodev", "noexec", "relatime"}}, + ) + } + // TODO(andrewrynhard): To handle cases when the newer version changes the // platform name, this should be determined in the installer container. config := constants.ConfigNone diff --git a/internal/pkg/mount/pseudo.go b/internal/pkg/mount/pseudo.go index cb98c5c650..13a8cdf659 100644 --- a/internal/pkg/mount/pseudo.go +++ b/internal/pkg/mount/pseudo.go @@ -5,7 +5,11 @@ package mount import ( + "os" + "golang.org/x/sys/unix" + + "github.com/siderolabs/talos/pkg/machinery/constants" ) // PseudoMountPoints returns the mountpoints required to boot the system. @@ -29,5 +33,11 @@ func PseudoSubMountPoints() (mountpoints *Points, err error) { pseudo.Set("hugetlb", NewMountPoint("hugetlbfs", "/dev/hugepages", "hugetlbfs", 0, "")) pseudo.Set("securityfs", NewMountPoint("securityfs", "/sys/kernel/security", "securityfs", unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV|unix.MS_RELATIME, "")) + if _, err := os.Stat(constants.EFIVarsMountPoint); err == nil { + // mount EFI vars if they exist + // TODO: frezbo: maybe mount ro and rw when needed to write? better security? + pseudo.Set("efivars", NewMountPoint("efivarfs", constants.EFIVarsMountPoint, "efivarfs", unix.MS_NOSUID|unix.MS_NOEXEC|unix.MS_NODEV|unix.MS_RELATIME, "")) + } + return pseudo, nil } diff --git a/pkg/machinery/constants/constants.go b/pkg/machinery/constants/constants.go index 8f8d3ac0e9..e45a478a6d 100644 --- a/pkg/machinery/constants/constants.go +++ b/pkg/machinery/constants/constants.go @@ -138,6 +138,10 @@ const ( // the boot path. EFIMountPoint = BootMountPoint + "/EFI" + // EFIVarsMountPoint is mount point for efivars filesystem type. + // https://www.kernel.org/doc/html/next/filesystems/efivarfs.html + EFIVarsMountPoint = "/sys/firmware/efi/efivars" + // BIOSGrubPartitionLabel is the label of the partition used by grub's second // stage bootloader. BIOSGrubPartitionLabel = "BIOS" @@ -507,6 +511,18 @@ const ( // RootfsAsset defines a well known name for our rootfs filename. RootfsAsset = "rootfs.sqsh" + // UKIAsset defines a well known name for our UKI filename. + UKIAsset = "vmlinuz.efi.signed" + + // UKIAssetPath is the path to the UKI in the installer. + UKIAssetPath = "/usr/install/%s/" + UKIAsset + + // SDBootAsset defines a well known name for our SDBoot filename. + SDBootAsset = "systemd-boot.efi.signed" + + // SDBootAssetPath is the path to the SDBoot in the installer. + SDBootAssetPath = "/usr/install/%s/" + SDBootAsset + // DefaultCertificateValidityDuration is the default duration for a certificate. DefaultCertificateValidityDuration = x509.DefaultCertificateValidityDuration diff --git a/pkg/provision/providers/qemu/arch.go b/pkg/provision/providers/qemu/arch.go index ffeeb9317f..a0efe6f77e 100644 --- a/pkg/provision/providers/qemu/arch.go +++ b/pkg/provision/providers/qemu/arch.go @@ -5,6 +5,7 @@ package qemu import ( + "os" "os/exec" "path/filepath" ) @@ -93,7 +94,18 @@ func (arch Arch) PFlash(uefiEnabled, secureBootEnabled bool, extraUEFISearchPath return nil } - uefiSourcePaths := []string{"/usr/share/ovmf/OVMF.fd", "/usr/share/OVMF/OVMF.fd", "/usr/share/OVMF/OVMF_CODE_4M.fd", "/usr/share/OVMF/OVMF_CODE_4M.secboot.fd"} + uefiSourcePaths := []string{ + "/usr/share/ovmf/OVMF.fd", + "/usr/share/OVMF/OVMF.fd", + "/usr/share/OVMF/OVMF_CODE_4M.fd", + "/usr/share/OVMF/OVMF_CODE_4M.secboot.fd", + } + + if _, err := os.Stat("/usr/share/qemu/edk2-x86_64-secure-code.fd"); err == nil { + // alpine uses this path + uefiSourcePaths = append(uefiSourcePaths, "/usr/share/qemu/edk2-x86_64-secure-code.fd") + } + for _, p := range extraUEFISearchPaths { uefiSourcePaths = append(uefiSourcePaths, filepath.Join(p, "OVMF.fd")) } diff --git a/pkg/provision/providers/qemu/launch.go b/pkg/provision/providers/qemu/launch.go index fe2213571d..1cc97be862 100644 --- a/pkg/provision/providers/qemu/launch.go +++ b/pkg/provision/providers/qemu/launch.go @@ -299,6 +299,8 @@ func launchVM(config *LaunchConfig) error { fmt.Sprintf("file=%s,level=20", filepath.Join(config.TPM2Config.StateDir, "swtpm.log")), }...) + log.Printf("starting swtpm: %s", cmd.String()) + if err := cmd.Start(); err != nil { return err } diff --git a/pkg/provision/providers/qemu/pflash.go b/pkg/provision/providers/qemu/pflash.go index 72b11e0978..e3bf2d7377 100644 --- a/pkg/provision/providers/qemu/pflash.go +++ b/pkg/provision/providers/qemu/pflash.go @@ -9,6 +9,7 @@ import ( "io" "os" "os/exec" + "strings" "github.com/siderolabs/talos/pkg/provision/providers/vm" ) @@ -69,6 +70,18 @@ func (p *provisioner) createPFlashImages(state *vm.State, nodeName string, pflas if secureBootEnabled { flashVarsPath := state.GetRelativePath(fmt.Sprintf("%s-flash_vars.fd", nodeName)) + ovmfVars := "/usr/share/OVMF/OVMF_VARS_4M.fd" + + for _, pflash := range pflashSpec { + for _, sourcePath := range pflash.SourcePaths { + if strings.Contains(sourcePath, "edk2-x86_64-secure-code.fd") { + // alpine + ovmfVars = "/usr/share/OVMF/OVMF_VARS.fd" + + break + } + } + } cmd := exec.Command("ovmfctl", []string{ "--no-microsoft", @@ -84,7 +97,7 @@ func (p *provisioner) createPFlashImages(state *vm.State, nodeName string, pflas "4e32566d-8e9e-4f52-81d3-5bb9715f9727", secureBootEnrollCert, "--input", - "/usr/share/OVMF/OVMF_VARS_4M.fd", + ovmfVars, "--output", flashVarsPath, }...)