diff --git a/cmd/talosctl/cmd/mgmt/cluster/create.go b/cmd/talosctl/cmd/mgmt/cluster/create.go index e0f67825cb..23b997deb5 100644 --- a/cmd/talosctl/cmd/mgmt/cluster/create.go +++ b/cmd/talosctl/cmd/mgmt/cluster/create.go @@ -122,6 +122,7 @@ var ( nodeInitramfsPath string nodeISOPath string nodeUSBPath string + nodeUKIPath string nodeDiskImagePath string nodeIPXEBootScript string applyConfigEnabled bool @@ -231,6 +232,9 @@ func downloadBootAssets(ctx context.Context) error { { path: &nodeUSBPath, }, + { + path: &nodeUKIPath, + }, { path: &nodeDiskImagePath, }, @@ -471,6 +475,7 @@ func create(ctx context.Context) error { InitramfsPath: nodeInitramfsPath, ISOPath: nodeISOPath, USBPath: nodeUSBPath, + UKIPath: nodeUKIPath, IPXEBootScript: nodeIPXEBootScript, DiskImagePath: nodeDiskImagePath, @@ -1255,6 +1260,7 @@ func init() { createCmd.Flags().StringVar(&nodeVmlinuzPath, "vmlinuz-path", helpers.ArtifactPath(constants.KernelAssetWithArch), "the compressed kernel image to use") createCmd.Flags().StringVar(&nodeISOPath, "iso-path", "", "the ISO path to use for the initial boot (VM only)") createCmd.Flags().StringVar(&nodeUSBPath, "usb-path", "", "the USB stick image path to use for the initial boot (VM only)") + createCmd.Flags().StringVar(&nodeUKIPath, "uki-path", "", "the UKI image path to use for the initial boot (VM only)") createCmd.Flags().StringVar(&nodeInitramfsPath, "initrd-path", helpers.ArtifactPath(constants.InitramfsAssetWithArch), "initramfs image to use") createCmd.Flags().StringVar(&nodeDiskImagePath, "disk-image-path", "", "disk image to use") createCmd.Flags().StringVar(&nodeIPXEBootScript, "ipxe-boot-script", "", "iPXE boot script (URL) to use") diff --git a/internal/pkg/secureboot/uki/generate.go b/internal/pkg/secureboot/uki/generate.go index a4d5623e0e..1ea5851567 100644 --- a/internal/pkg/secureboot/uki/generate.go +++ b/internal/pkg/secureboot/uki/generate.go @@ -185,10 +185,14 @@ func (builder *Builder) generatePCRPublicKey() error { } func (builder *Builder) generateKernel() error { - path := filepath.Join(builder.scratchDir, "kernel") + path := builder.KernelPath - if err := builder.peSigner.Sign(builder.KernelPath, path); err != nil { - return err + if builder.peSigner != nil { + path := filepath.Join(builder.scratchDir, "kernel") + + if err := builder.peSigner.Sign(builder.KernelPath, path); err != nil { + return err + } } builder.sections = append(builder.sections, diff --git a/internal/pkg/secureboot/uki/uki.go b/internal/pkg/secureboot/uki/uki.go index b917fc0d2e..f762c24be8 100644 --- a/internal/pkg/secureboot/uki/uki.go +++ b/internal/pkg/secureboot/uki/uki.go @@ -13,6 +13,7 @@ import ( "github.com/siderolabs/talos/internal/pkg/secureboot" "github.com/siderolabs/talos/internal/pkg/secureboot/measure" "github.com/siderolabs/talos/internal/pkg/secureboot/pesign" + "github.com/siderolabs/talos/pkg/imager/utils" ) // section is a UKI file section. @@ -67,14 +68,65 @@ type Builder struct { unsignedUKIPath string } -// Build the UKI file. +// Build the unsigned UKI file. // // Build process is as follows: +// - build ephemeral sections (uname, os-release), and other proposed sections +// - assemble the final UKI file starting from sd-stub and appending generated section. +func (builder *Builder) Build(printf func(string, ...any)) error { + var err error + + builder.scratchDir, err = os.MkdirTemp("", "talos-uki") + if err != nil { + return err + } + + defer func() { + if err = os.RemoveAll(builder.scratchDir); err != nil { + log.Printf("failed to remove scratch dir: %v", err) + } + }() + + if err := utils.CopyFiles(printf, utils.SourceDestination(builder.SdBootPath, builder.OutSdBootPath)); err != nil { + return err + } + + printf("generating UKI sections") + + // generate and build list of all sections + for _, generateSection := range []func() error{ + builder.generateOSRel, + builder.generateCmdline, + builder.generateInitrd, + builder.generateSplash, + builder.generateUname, + builder.generateSBAT, + // append kernel last to account for decompression + builder.generateKernel, + } { + if err = generateSection(); err != nil { + return fmt.Errorf("error generating sections: %w", err) + } + } + + printf("assembling UKI") + + // assemble the final UKI file + if err = builder.assemble(); err != nil { + return fmt.Errorf("error assembling UKI: %w", err) + } + + return utils.CopyFiles(printf, utils.SourceDestination(builder.unsignedUKIPath, builder.OutUKIPath)) +} + +// BuildSigned the UKI file. +// +// BuildSigned process is as follows: // - sign the sd-boot EFI binary, and write it to the OutSdBootPath // - build ephemeral sections (uname, os-release), and other proposed sections // - measure sections, generate signature, and append to the list of sections // - assemble the final UKI file starting from sd-stub and appending generated section. -func (builder *Builder) Build(printf func(string, ...any)) error { +func (builder *Builder) BuildSigned(printf func(string, ...any)) error { var err error builder.scratchDir, err = os.MkdirTemp("", "talos-uki") diff --git a/pkg/imager/imager.go b/pkg/imager/imager.go index b3b3e22ff9..75f7a136df 100644 --- a/pkg/imager/imager.go +++ b/pkg/imager/imager.go @@ -103,8 +103,8 @@ func (i *Imager) Execute(ctx context.Context, outputPath string, report *reporte Status: reporter.StatusSucceeded, }) - // 4. Build UKI if Secure Boot is enabled. - if i.prof.SecureBootEnabled() { + // 4. Build UKI unless the output is a kernel or cmdline. + if i.prof.Output.Kind != profile.OutKindKernel && i.prof.Output.Kind != profile.OutKindCmdline { if err = i.buildUKI(ctx, report); err != nil { return "", err } @@ -406,18 +406,8 @@ func (i *Imager) buildCmdline() error { func (i *Imager) buildUKI(ctx context.Context, report *reporter.Reporter) error { printf := progressPrintf(report, reporter.Update{Message: "building UKI...", Status: reporter.StatusRunning}) - i.sdBootPath = filepath.Join(i.tempDir, "systemd-boot.efi.signed") - i.ukiPath = filepath.Join(i.tempDir, "vmlinuz.efi.signed") - - pcrSigner, err := i.prof.Input.SecureBoot.PCRSigner.GetSigner(ctx) - if err != nil { - return fmt.Errorf("failed to get PCR signer: %w", err) - } - - securebootSigner, err := i.prof.Input.SecureBoot.SecureBootSigner.GetSigner(ctx) - if err != nil { - return fmt.Errorf("failed to get SecureBoot signer: %w", err) - } + i.sdBootPath = filepath.Join(i.tempDir, "systemd-boot.efi") + i.ukiPath = filepath.Join(i.tempDir, "vmlinuz.efi") builder := uki.Builder{ Arch: i.prof.Arch, @@ -428,14 +418,36 @@ func (i *Imager) buildUKI(ctx context.Context, report *reporter.Reporter) error InitrdPath: i.initramfsPath, Cmdline: i.cmdline, - SecureBootSigner: securebootSigner, - PCRSigner: pcrSigner, - OutSdBootPath: i.sdBootPath, OutUKIPath: i.ukiPath, } - if err := builder.Build(printf); err != nil { + buildFunc := builder.Build + + if i.prof.SecureBootEnabled() { + i.sdBootPath = filepath.Join(i.tempDir, "systemd-boot.efi.signed") + i.ukiPath = filepath.Join(i.tempDir, "vmlinuz.efi.signed") + + builder.OutSdBootPath = i.sdBootPath + builder.OutUKIPath = i.ukiPath + + pcrSigner, err := i.prof.Input.SecureBoot.PCRSigner.GetSigner(ctx) + if err != nil { + return fmt.Errorf("failed to get PCR signer: %w", err) + } + + securebootSigner, err := i.prof.Input.SecureBoot.SecureBootSigner.GetSigner(ctx) + if err != nil { + return fmt.Errorf("failed to get SecureBoot signer: %w", err) + } + + builder.SecureBootSigner = securebootSigner + builder.PCRSigner = pcrSigner + + buildFunc = builder.BuildSigned + } + + if err := buildFunc(printf); err != nil { return err } diff --git a/pkg/imager/profile/default.go b/pkg/imager/profile/default.go index b780ff3ef8..35e405e945 100644 --- a/pkg/imager/profile/default.go +++ b/pkg/imager/profile/default.go @@ -55,6 +55,14 @@ var Default = map[string]Profile{ }, }, }, + "metal-uki": { + Platform: constants.PlatformMetal, + SecureBoot: pointer.To(false), + Output: Output{ + Kind: OutKindUKI, + OutFormat: OutFormatRaw, + }, + }, "secureboot-metal": { Platform: constants.PlatformMetal, SecureBoot: pointer.To(true), diff --git a/pkg/imager/profile/input.go b/pkg/imager/profile/input.go index 0c891ffa6d..137f7f46b5 100644 --- a/pkg/imager/profile/input.go +++ b/pkg/imager/profile/input.go @@ -209,15 +209,15 @@ func (i *Input) FillDefaults(arch, version string, secureboot bool) { i.BaseInstaller.ImageRef = fmt.Sprintf("%s:%s", images.DefaultInstallerImageRepository, version) } - if secureboot { - if i.SDStub == zeroFileAsset { - i.SDStub.Path = fmt.Sprintf(constants.SDStubAssetPath, arch) - } + if i.SDStub == zeroFileAsset { + i.SDStub.Path = fmt.Sprintf(constants.SDStubAssetPath, arch) + } - if i.SDBoot == zeroFileAsset { - i.SDBoot.Path = fmt.Sprintf(constants.SDBootAssetPath, arch) - } + if i.SDBoot == zeroFileAsset { + i.SDBoot.Path = fmt.Sprintf(constants.SDBootAssetPath, arch) + } + if secureboot { if i.SecureBoot == nil { i.SecureBoot = &SecureBootAssets{} } diff --git a/pkg/imager/profile/profile.go b/pkg/imager/profile/profile.go index 2cb244ed2d..1f276666da 100644 --- a/pkg/imager/profile/profile.go +++ b/pkg/imager/profile/profile.go @@ -124,9 +124,6 @@ func (p *Profile) Validate() error { return fmt.Errorf("customization of meta partition is not supported for %s output", p.Output.Kind) } case OutKindUKI: - if !p.SecureBootEnabled() { - return fmt.Errorf("!secureboot is not supported for %s output", p.Output.Kind) - } } return nil diff --git a/pkg/provision/providers/qemu/launch.go b/pkg/provision/providers/qemu/launch.go index ecc9077eb7..c8147123e1 100644 --- a/pkg/provision/providers/qemu/launch.go +++ b/pkg/provision/providers/qemu/launch.go @@ -49,6 +49,7 @@ type LaunchConfig struct { InitrdPath string ISOPath string USBPath string + UKIPath string ExtraISOPath string PFlashImages []string KernelArgs string @@ -468,6 +469,11 @@ func launchVM(config *LaunchConfig) error { "-device", "nec-usb-xhci,id=xhci", "-device", "usb-storage,bus=xhci.0,drive=stick,removable=on", ) + case config.UKIPath != "": + args = append(args, + "-kernel", config.UKIPath, + "-append", config.KernelArgs, + ) case config.KernelImagePath != "": args = append(args, "-kernel", config.KernelImagePath, diff --git a/pkg/provision/providers/qemu/node.go b/pkg/provision/providers/qemu/node.go index c4c4fe687c..9bdb8134ff 100644 --- a/pkg/provision/providers/qemu/node.go +++ b/pkg/provision/providers/qemu/node.go @@ -230,6 +230,7 @@ func (p *provisioner) createNode(state *vm.State, clusterReq provision.ClusterRe launchConfig.InitrdPath = strings.ReplaceAll(clusterReq.InitramfsPath, constants.ArchVariable, opts.TargetArch) launchConfig.ISOPath = strings.ReplaceAll(clusterReq.ISOPath, constants.ArchVariable, opts.TargetArch) launchConfig.USBPath = strings.ReplaceAll(clusterReq.USBPath, constants.ArchVariable, opts.TargetArch) + launchConfig.UKIPath = strings.ReplaceAll(clusterReq.UKIPath, constants.ArchVariable, opts.TargetArch) } launchConfig.StatePath, err = state.StatePath() diff --git a/pkg/provision/request.go b/pkg/provision/request.go index 828debe915..c9b324e0e0 100644 --- a/pkg/provision/request.go +++ b/pkg/provision/request.go @@ -35,6 +35,7 @@ type ClusterRequest struct { InitramfsPath string ISOPath string USBPath string + UKIPath string DiskImagePath string IPXEBootScript string diff --git a/website/content/v1.10/reference/cli.md b/website/content/v1.10/reference/cli.md index 9126e9007c..87ced955de 100644 --- a/website/content/v1.10/reference/cli.md +++ b/website/content/v1.10/reference/cli.md @@ -199,6 +199,7 @@ talosctl cluster create [flags] --skip-kubeconfig skip merging kubeconfig from the created cluster --talos-version string the desired Talos version to generate config for (if not set, defaults to image version) --talosconfig string The path to the Talos configuration file. Defaults to 'TALOSCONFIG' env variable if set, otherwise '$HOME/.talos/config' and '/var/run/secrets/talos.dev/config' in order. + --uki-path string the UKI image path to use for the initial boot (VM only) --usb-path string the USB stick image path to use for the initial boot (VM only) --use-vip use a virtual IP for the controlplane endpoint instead of the loadbalancer --user-disk strings list of disks to create for each VM in format: :::