diff --git a/.drone.jsonnet b/.drone.jsonnet index 88d8dd928..a67792332 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -193,10 +193,12 @@ local Pipeline(name, steps=[], depends_on=[], with_docker=true, disable_clone=fa local external_artifacts = Step('external-artifacts', depends_on=[setup_ci]); local generate = Step('generate', target='generate docs', depends_on=[setup_ci]); local check_dirty = Step('check-dirty', depends_on=[generate, external_artifacts]); -local build = Step('build', target='talosctl-all kernel initramfs installer imager talos _out/integration-test-linux-amd64', depends_on=[check_dirty], environment={ IMAGE_REGISTRY: local_registry, PUSH: true }); +local uki_certs = Step('uki-certs', depends_on=[setup_ci], environment={ PLATFORM: 'linux/amd64' }); +local build = Step('build', target='talosctl-all kernel initramfs installer imager talos _out/integration-test-linux-amd64', depends_on=[check_dirty, uki_certs], environment={ IMAGE_REGISTRY: local_registry, PUSH: true }); local lint = Step('lint', depends_on=[build]); local talosctl_cni_bundle = Step('talosctl-cni-bundle', depends_on=[build, lint]); local iso = Step('iso', target='iso', depends_on=[build], environment={ IMAGE_REGISTRY: local_registry }); +local iso_uki = Step('iso-uki', target='iso-uki', depends_on=[build], environment={ IMAGE_REGISTRY: local_registry }); local images_essential = Step('images-essential', target='images-essential', depends_on=[iso], environment={ IMAGE_REGISTRY: local_registry }); local unit_tests = Step('unit-tests', target='unit-tests unit-tests-race', depends_on=[build, lint]); local e2e_docker = Step('e2e-docker-short', depends_on=[build, unit_tests], target='e2e-docker', environment={ SHORT_INTEGRATION_TEST: 'yes', IMAGE_REGISTRY: local_registry }); @@ -281,7 +283,7 @@ local save_artifacts = { 'az storage blob upload-batch --overwrite -s _out -d ${CI_COMMIT_SHA}${DRONE_TAG//./-}', ], volumes: volumes.ForStep(), - depends_on: [build.name, images_essential.name, iso.name, talosctl_cni_bundle.name], + depends_on: [build.name, images_essential.name, iso.name, iso_uki.name, talosctl_cni_bundle.name], }; local load_artifacts = { @@ -359,10 +361,12 @@ local default_steps = [ external_artifacts, generate, check_dirty, + uki_certs, build, lint, talosctl_cni_bundle, iso, + iso_uki, images_essential, unit_tests, save_artifacts, @@ -708,6 +712,8 @@ local release = { '_out/scaleway-arm64.raw.xz', '_out/talos-amd64.iso', '_out/talos-arm64.iso', + '_out/talos-uki-amd64.iso', + '_out/talos-uki-arm64.iso', '_out/talosctl-cni-bundle-amd64.tar.gz', '_out/talosctl-cni-bundle-arm64.tar.gz', '_out/talosctl-darwin-amd64', diff --git a/Dockerfile b/Dockerfile index 1d1cb6793..ddcba2150 100644 --- a/Dockerfile +++ b/Dockerfile @@ -676,9 +676,46 @@ COPY --from=rootfs / / LABEL org.opencontainers.image.source https://github.com/siderolabs/talos ENTRYPOINT ["/sbin/init"] +FROM --platform=${BUILDPLATFORM} 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 +WORKDIR /build +COPY --from=pkg-sd-stub-amd64 / _out/ +COPY --from=pkg-sd-boot-amd64 / _out/ +COPY --from=pkg-kernel-amd64 /boot/vmlinuz _out/vmlinuz-amd64 +COPY --from=initramfs-archive-amd64 /initramfs.xz _out/initramfs-amd64.xz +COPY _out/uki-certs _out/uki-certs +RUN ukify + +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 +WORKDIR /build +COPY --from=pkg-sd-stub-arm64 / _out/ +COPY --from=pkg-sd-boot-arm64 / _out/ +COPY --from=pkg-kernel-arm64 /boot/vmlinuz _out/vmlinuz-arm64 +COPY --from=initramfs-archive-arm64 /initramfs.xz _out/initramfs-arm64.xz +COPY _out/uki-certs _out/uki-certs +RUN ukify \ + -sd-stub _out/linuxaa64.efi.stub \ + -sd-boot _out/systemd-bootaa64.efi \ + -kernel _out/vmlinuz-arm64 \ + -initrd _out/initramfs-arm64.xz + +FROM scratch AS uki-arm64 +COPY --from=uki-build-arm64 /build/_out/systemd-bootaa64.efi.signed /systemd-boot.efi.signed +COPY --from=uki-build-arm64 /build/_out/vmlinuz.efi.signed /vmlinuz.efi.signed + +FROM --platform=${BUILDPLATFORM} uki-${TARGETARCH} AS uki + # The installer target generates an image that can be used to install Talos to # various environments. - FROM base AS installer-build ARG GO_BUILDFLAGS ARG GO_LDFLAGS @@ -695,12 +732,16 @@ COPY --from=pkg-grub-amd64 /usr/lib/grub /usr/lib/grub COPY --from=pkg-kernel-amd64 /boot/vmlinuz /usr/install/amd64/vmlinuz COPY --from=pkg-kernel-amd64 /dtb /usr/install/amd64/dtb COPY --from=initramfs-archive-amd64 /initramfs.xz /usr/install/amd64/initramfs.xz +COPY --from=uki-amd64 /systemd-boot.efi.signed /usr/install/amd64/systemd-boot.efi.signed +COPY --from=uki-amd64 /vmlinuz.efi.signed /usr/install/amd64/vmlinuz.efi.signed FROM scratch AS install-artifacts-arm64 COPY --from=pkg-grub-arm64 /usr/lib/grub /usr/lib/grub COPY --from=pkg-kernel-arm64 /boot/vmlinuz /usr/install/arm64/vmlinuz COPY --from=pkg-kernel-arm64 /dtb /usr/install/arm64/dtb COPY --from=initramfs-archive-arm64 /initramfs.xz /usr/install/arm64/initramfs.xz +COPY --from=uki-arm64 /systemd-boot.efi.signed /usr/install/arm64/systemd-boot.efi.signed +COPY --from=uki-arm64 /vmlinuz.efi.signed /usr/install/arm64/vmlinuz.efi.signed COPY --from=pkg-u-boot-arm64 / /usr/install/arm64/u-boot COPY --from=pkg-raspberrypi-firmware-arm64 / /usr/install/arm64/raspberrypi-firmware @@ -720,6 +761,7 @@ ENV SOURCE_DATE_EPOCH ${SOURCE_DATE_EPOCH} RUN apk add --no-cache --update --no-scripts \ bash \ cpio \ + dosfstools \ efibootmgr \ kmod \ mtools \ @@ -797,8 +839,33 @@ COPY --from=iso-arm64-build /out / FROM --platform=${BUILDPLATFORM} iso-${TARGETARCH} AS iso -# The test target performs tests on the source code. +FROM imager as iso-uki-amd64-build +ARG SOURCE_DATE_EPOCH +ENV SOURCE_DATE_EPOCH ${SOURCE_DATE_EPOCH} +RUN /bin/installer \ + iso \ + --uki \ + --arch amd64 \ + --output /out +FROM imager as iso-uki-arm64-build +ARG SOURCE_DATE_EPOCH +ENV SOURCE_DATE_EPOCH ${SOURCE_DATE_EPOCH} +RUN /bin/installer \ + iso \ + --uki \ + --arch arm64 \ + --output /out + +FROM scratch as iso-uki-amd64 +COPY --from=iso-uki-amd64-build /out / + +FROM scratch as iso-uki-arm64 +COPY --from=iso-uki-arm64-build /out / + +FROM --platform=${BUILDPLATFORM} iso-uki-${TARGETARCH} AS iso-uki + +# The test target performs tests on the source code. FROM base AS unit-tests-runner RUN unlink /etc/ssl COPY --from=rootfs / / @@ -878,44 +945,6 @@ RUN --mount=type=cache,target=/.cache GOOS=linux GOARCH=amd64 GOAMD64=${GOAMD64} FROM scratch AS module-sig-verify-linux COPY --from=module-sig-verify-linux-build /src/module-sig-verify/module-sig-verify /module-sig-verify-linux-amd64 -FROM --platform=${BUILDPLATFORM} 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 -WORKDIR /build -COPY --from=pkg-sd-stub-amd64 / _out/ -COPY --from=pkg-sd-boot-amd64 / _out/ -COPY --from=pkg-kernel-amd64 /boot/vmlinuz _out/vmlinuz-amd64 -COPY --from=initramfs-archive-amd64 /initramfs.xz _out/initramfs-amd64.xz -COPY _out/uki-certs _out/uki-certs -RUN ukify - -FROM scratch AS uki-amd64 -COPY --from=uki-build-amd64 /build/_out/systemd-bootx64.efi.signed /systemd-bootx64.efi.signed -COPY --from=uki-build-amd64 /build/_out/vmlinuz.efi /vmlinuz-amd64.signed.efi - -FROM --platform=${BUILDPLATFORM} tools AS uki-build-arm64 -WORKDIR /build -COPY --from=pkg-sd-stub-arm64 / _out/ -COPY --from=pkg-sd-boot-arm64 / _out/ -COPY --from=pkg-kernel-arm64 /boot/vmlinuz _out/vmlinuz-arm64 -COPY --from=initramfs-archive-arm64 /initramfs.xz _out/initramfs-arm64.xz -COPY _out/uki-certs _out/uki-certs -RUN ukify \ - -sd-stub _out/linuxaa64.efi.stub \ - -sd-boot _out/systemd-bootaa64.efi \ - -kernel _out/vmlinuz-arm64 \ - -initrd _out/initramfs-arm64.xz - -FROM scratch AS uki-arm64 -COPY --from=uki-build-arm64 /build/_out/systemd-bootaa64.efi.signed /systemd-bootaa64.efi.signed -COPY --from=uki-build-arm64 /build/_out/vmlinuz.efi /vmlinuz-arm64.signed.efi - -FROM --platform=${BUILDPLATFORM} uki-${TARGETARCH} AS uki - # The lint target performs linting on the source code. FROM base AS lint-go COPY .golangci.yml . diff --git a/Makefile b/Makefile index b1361d3bc..a22bcdb0b 100644 --- a/Makefile +++ b/Makefile @@ -324,6 +324,14 @@ iso: ## Builds the ISO and outputs it to the artifact directory. docker run --rm -e SOURCE_DATE_EPOCH=$(SOURCE_DATE_EPOCH) -i $(REGISTRY_AND_USERNAME)/imager:$(IMAGE_TAG) iso --arch $$arch --tar-to-stdout $(IMAGER_ARGS) | tar xz -C $(ARTIFACTS) ; \ done +.PHONY: iso-uki +iso-uki: ## Builds UEFI only ISO which uses UKI and outputs it to the artifact directory. + @docker pull $(REGISTRY_AND_USERNAME)/imager:$(IMAGE_TAG) + @for platform in $(subst $(,),$(space),$(PLATFORM)); do \ + arch=`basename "$${platform}"` ; \ + docker run --rm -e SOURCE_DATE_EPOCH=$(SOURCE_DATE_EPOCH) -i $(REGISTRY_AND_USERNAME)/imager:$(IMAGE_TAG) iso --arch $$arch --uki --tar-to-stdout $(IMAGER_ARGS) | tar xz -C $(ARTIFACTS) ; \ + done + .PHONY: talosctl-cni-bundle talosctl-cni-bundle: ## Creates a compressed tarball that includes CNI bundle for talosctl. @$(MAKE) local-$@ DEST=$(ARTIFACTS) diff --git a/cmd/installer/cmd/iso.go b/cmd/installer/cmd/iso.go index c0eae80dc..ef240e5e4 100644 --- a/cmd/installer/cmd/iso.go +++ b/cmd/installer/cmd/iso.go @@ -26,8 +26,11 @@ import ( "github.com/siderolabs/talos/pkg/machinery/kernel" ) -//go:embed grub.iso.cfg -var isoGrubCfg []byte +var ( + //go:embed grub.iso.cfg + isoGrubCfg []byte + uki bool +) // isoCmd represents the iso command. var isoCmd = &cobra.Command{ @@ -42,6 +45,7 @@ var isoCmd = &cobra.Command{ } func init() { + isoCmd.Flags().BoolVar(&uki, "uki", false, "Create UKI ISO") isoCmd.Flags().StringVar(&outputArg, "output", "/out", "The output path") isoCmd.Flags().BoolVar(&tarToStdout, "tar-to-stdout", false, "Tar output and send to stdout") rootCmd.AddCommand(isoCmd) @@ -53,36 +57,56 @@ func runISOCmd() error { return err } + out := fmt.Sprintf("/tmp/talos-%s.iso", options.Arch) + + if uki { + out = fmt.Sprintf("/tmp/talos-uki-%s.iso", options.Arch) + + if err := createUKIISO(out); err != nil { + return err + } + } else { + if err := createISO(out); err != nil { + return err + } + } + + from, err := os.Open(out) + if err != nil { + return err + } + //nolint:errcheck + defer from.Close() + + to, err := os.OpenFile(filepath.Join(outputArg, filepath.Base(out)), os.O_RDWR|os.O_CREATE, 0o666) + if err != nil { + return err + } + //nolint:errcheck + defer to.Close() + + _, err = io.Copy(to, from) + if err != nil { + return err + } + + if tarToStdout { + if err := tarOutput(); err != nil { + return err + } + } + + return nil +} + +func createISO(out string) error { files := map[string]string{ fmt.Sprintf("/usr/install/%s/vmlinuz", options.Arch): "/mnt/boot/vmlinuz", fmt.Sprintf("/usr/install/%s/initramfs.xz", options.Arch): "/mnt/boot/initramfs.xz", } - for src, dest := range files { - if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { - return err - } - - log.Printf("copying %s to %s", src, dest) - - from, err := os.Open(src) - if err != nil { - return err - } - //nolint:errcheck - defer from.Close() - - to, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0o666) - if err != nil { - return err - } - //nolint:errcheck - defer to.Close() - - _, err = io.Copy(to, from) - if err != nil { - return err - } + if err := copyFiles(files); err != nil { + return err } log.Println("creating grub.cfg") @@ -145,33 +169,48 @@ func runISOCmd() error { log.Println("creating ISO") - out := fmt.Sprintf("/tmp/talos-%s.iso", options.Arch) + return pkg.CreateISO(out, "/mnt") +} - if err = pkg.CreateISO(out, "/mnt"); err != nil { +func createUKIISO(out string) error { + files := map[string]string{ + fmt.Sprintf("/usr/install/%s/systemd-boot.efi.signed", options.Arch): "/mnt/systemd-boot.efi.signed", + fmt.Sprintf("/usr/install/%s/vmlinuz.efi.signed", options.Arch): "/mnt/vmlinuz.efi.signed", + } + + if err := copyFiles(files); err != nil { return err } - from, err := os.Open(out) - if err != nil { - return err - } - //nolint:errcheck - defer from.Close() + log.Println("creating UKI ISO") - to, err := os.OpenFile(filepath.Join(outputArg, filepath.Base(out)), os.O_RDWR|os.O_CREATE, 0o666) - if err != nil { - return err - } - //nolint:errcheck - defer to.Close() + return pkg.CreateUKIISO(out, "/mnt", options.Arch) +} - _, err = io.Copy(to, from) - if err != nil { - return err - } +func copyFiles(files map[string]string) error { + for src, dest := range files { + if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil { + return err + } - if tarToStdout { - if err := tarOutput(); err != nil { + log.Printf("copying %s to %s", src, dest) + + from, err := os.Open(src) + if err != nil { + return err + } + //nolint:errcheck + defer from.Close() + + to, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE, 0o666) + if err != nil { + return err + } + //nolint:errcheck + defer to.Close() + + _, err = io.Copy(to, from) + if err != nil { return err } } diff --git a/cmd/installer/pkg/iso.go b/cmd/installer/pkg/iso.go index d3514ed94..43884d4ae 100644 --- a/cmd/installer/pkg/iso.go +++ b/cmd/installer/pkg/iso.go @@ -7,9 +7,18 @@ package pkg import ( "fmt" "os" + "path/filepath" "time" "github.com/siderolabs/go-cmd/pkg/cmd" + + "github.com/siderolabs/talos/pkg/machinery/constants" + "github.com/siderolabs/talos/pkg/makefs" +) + +const ( + // UKIISOSize is the size of the UKI ISO. + UKIISOSize = 120 * 1024 * 1024 ) // CreateISO creates an iso by invoking the `grub-mkrescue` command. @@ -42,3 +51,86 @@ func CreateISO(iso, dir string) error { return nil } + +// CreateUKIISO creates an iso using a UKI and UEFI only. +// nolint:gocyclo +func CreateUKIISO(iso, dir, arch string) error { + isoDir := filepath.Join(dir, "iso") + + if err := os.MkdirAll(isoDir, 0o755); err != nil { + return err + } + + defer os.RemoveAll(isoDir) // nolint:errcheck + + efiBootImg := filepath.Join(isoDir, "efiboot.img") + + f, err := os.Create(efiBootImg) + if err != nil { + return err + } + + if err := f.Truncate(UKIISOSize); err != nil { + return err + } + + defer f.Close() // nolint:errcheck + + fopts := []makefs.Option{ + makefs.WithLabel(constants.EFIPartitionLabel), + makefs.WithReproducible(true), + } + + if err := makefs.VFAT(efiBootImg, fopts...); err != nil { + return err + } + + if _, err := cmd.Run("mmd", "-i", efiBootImg, "::EFI"); err != nil { + return err + } + + if _, err := cmd.Run("mmd", "-i", efiBootImg, "::EFI/BOOT"); err != nil { + return err + } + + if _, err := cmd.Run("mmd", "-i", efiBootImg, "::EFI/Linux"); err != nil { + return err + } + + efiBootPath := "::EFI/BOOT/BOOTX64.efi" + + if arch == "arm64" { + efiBootPath = "::EFI/BOOT/BOOTAA64.EFI" + } + + if _, err := cmd.Run("mcopy", "-i", efiBootImg, filepath.Join(dir, "systemd-boot.efi.signed"), efiBootPath); err != nil { + return err + } + + if _, err := cmd.Run("mcopy", "-i", efiBootImg, filepath.Join(dir, "vmlinuz.efi.signed"), "::EFI/Linux/talos-A.efi"); err != nil { + return err + } + + // fixup directory timestamps recursively + if err := TouchFiles(dir); err != nil { + return err + } + + if _, err := cmd.Run( + "xorriso", + "-as", + "mkisofs", + "-V", + "Talos Secure Boot ISO", + "-e", + "efiboot.img", + "-no-emul-boot", + "-o", + iso, + isoDir, + ); err != nil { + return err + } + + return nil +} diff --git a/hack/ukify/main.go b/hack/ukify/main.go index 657f5b271..377d7536e 100644 --- a/hack/ukify/main.go +++ b/hack/ukify/main.go @@ -298,7 +298,13 @@ func run() error { return err } - return buildUKI(sdStub, output, sections) + if err := buildUKI(sdStub, output, sections); err != nil { + return err + } + + _, err = sbSign(output) + + return err } func main() { diff --git a/pkg/makefs/makefs.go b/pkg/makefs/makefs.go index 332df8ab0..bab8d37b6 100644 --- a/pkg/makefs/makefs.go +++ b/pkg/makefs/makefs.go @@ -10,8 +10,9 @@ type Option func(*Options) // Options for makefs. type Options struct { - Label string - Force bool + Label string + Force bool + Reproducible bool } // WithLabel sets the label for the filesystem to be created. @@ -28,6 +29,14 @@ func WithForce(force bool) Option { } } +// WithReproducible sets the reproducible flag for the filesystem to be created. +// This should only be used when creating filesystems on raw disk images. +func WithReproducible(reproducible bool) Option { + return func(o *Options) { + o.Reproducible = reproducible + } +} + // NewDefaultOptions builds options with specified setters applied. func NewDefaultOptions(setters ...Option) Options { var opt Options diff --git a/pkg/makefs/vfat.go b/pkg/makefs/vfat.go index 53b48c15c..18be20c12 100644 --- a/pkg/makefs/vfat.go +++ b/pkg/makefs/vfat.go @@ -18,6 +18,10 @@ func VFAT(partname string, setters ...Option) error { args = append(args, "-F", "32", "-n", opts.Label) } + if opts.Reproducible { + args = append(args, "--invariant") + } + args = append(args, partname) _, err := cmd.Run("mkfs.vfat", args...)