diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 9b7fa3d93..90d00d060 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -157,6 +157,27 @@ jobs: with: sarif_file: 'sarif' category: ${{ matrix.flavor }} + build-uki: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: | + git fetch --prune --unshallow + - name: Install earthly + uses: Luet-lab/luet-install-action@v1 + with: + repository: quay.io/kairos/packages + packages: utils/earthly + - name: Build uki image 🔧 + run: | + # Do fedora as its the smaller uki possible + earthly +uki --FLAVOR=fedora + - name: Release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: | + build/efi # build-vm-images: # needs: build # runs-on: macos-12 diff --git a/Earthfile b/Earthfile index 8bde70201..1ca0c2e34 100644 --- a/Earthfile +++ b/Earthfile @@ -247,6 +247,7 @@ base-image: ARG MODEL ARG FLAVOR ARG VARIANT + ARG BUILD_INITRD="true" IF [ "$BASE_IMAGE" = "" ] # Source the flavor-provided docker file FROM DOCKERFILE --build-arg MODEL=$MODEL -f images/Dockerfile.$FLAVOR . @@ -330,16 +331,17 @@ base-image: RUN find /usr/lib/modules -type f -name "*.ko" -execdir zstd --rm -9 {} \+ END - - IF [ "$FLAVOR" = "debian" ] - RUN rm -rf /boot/initrd.img-* - END + IF [ "$BUILD_INITRD" = "true" ] + IF [ "$FLAVOR" = "debian" ] + RUN rm -rf /boot/initrd.img-* + END - IF [ -e "/usr/bin/dracut" ] - # Regenerate initrd if necessary - RUN --no-cache kernel=$(ls /lib/modules | head -n1) && depmod -a "${kernel}" - RUN --no-cache kernel=$(ls /lib/modules | head -n1) && dracut -f "/boot/initrd-${kernel}" "${kernel}" && ln -sf "initrd-${kernel}" /boot/initrd + IF [ -e "/usr/bin/dracut" ] + # Regenerate initrd if necessary + RUN --no-cache kernel=$(ls /lib/modules | head -n1) && depmod -a "${kernel}" + RUN --no-cache kernel=$(ls /lib/modules | head -n1) && dracut -f "/boot/initrd-${kernel}" "${kernel}" && ln -sf "initrd-${kernel}" /boot/initrd + END END # Set /boot/vmlinuz pointing to our kernel so kairos-agent can use it @@ -366,10 +368,12 @@ base-image: END END + RUN rm -rf /tmp/* image: - FROM +base-image + ARG BUILD_INITRD="true" + FROM +base-image --BUILD_INITRD=$BUILD_INITRD ARG FLAVOR ARG VARIANT ARG MODEL @@ -395,6 +399,125 @@ image-rootfs: FROM +image SAVE ARTIFACT --keep-own /. rootfs +uki-artifacts: + FROM +image --BUILD_INITRD=false + RUN /usr/bin/immucore version + RUN ln -s /usr/bin/immucore /init + RUN find . \( -path ./sys -prune -o -path ./run -prune -o -path ./dev -prune -o -path ./tmp -prune -o -path ./proc -prune \) -o -print | cpio -R root:root -H newc -o | gzip -2 > /tmp/initramfs.cpio.gz + RUN echo "console=tty1 console=ttyS0 net.ifnames=1 rd.immucore.debug rd.immucore.uki selinux=0" > /tmp/Cmdline + RUN basename $(ls /boot/vmlinuz-* |grep -v rescue | head -n1)| sed --expression "s/vmlinuz-//g" > /tmp/Uname + SAVE ARTIFACT /boot/vmlinuz Kernel + SAVE ARTIFACT /etc/os-release Osrelease + SAVE ARTIFACT /tmp/Cmdline Cmdline + SAVE ARTIFACT /tmp/Uname Uname + SAVE ARTIFACT /tmp/initramfs.cpio.gz Initrd + +# Base image for uki operations so we only run the install once +uki-tools-image: + FROM fedora:38 + # objcopy from binutils and systemd-stub from systemd + RUN dnf install -y binutils systemd-boot mtools efitools sbsigntools shim openssl + +uki: + FROM +uki-tools-image + WORKDIR build + COPY +uki-artifacts/Kernel Kernel + COPY +uki-artifacts/Initrd Initrd + COPY +uki-artifacts/Osrelease Osrelease + COPY +uki-artifacts/Uname Uname + COPY +uki-artifacts/Cmdline Cmdline + ARG KVERSION=$(cat Uname) + RUN objcopy /usr/lib/systemd/boot/efi/linuxx64.efi.stub \ + --add-section .osrel=Osrelease --set-section-flags .osrel=data,readonly \ + --add-section .cmdline=Cmdline --set-section-flags .cmdline=data,readonly \ + --add-section .initrd=Initrd --set-section-flags .initrd=data,readonly \ + --add-section .uname=Uname --set-section-flags .uname=data,readonly \ + --add-section .linux=Kernel --set-section-flags .linux=code,readonly \ + $ISO_NAME.unsigned.efi \ + --change-section-vma .osrel=0x17000 \ + --change-section-vma .cmdline=0x18000 \ + --change-section-vma .initrd=0x19000 \ + --change-section-vma .uname=0x5a0ed000 \ + --change-section-vma .linux=0x5a0ee000 + SAVE ARTIFACT Uname Uname + SAVE ARTIFACT $ISO_NAME.unsigned.efi uki.efi AS LOCAL build/$ISO_NAME.unsigned-$KVERSION.efi + + +uki-signed: + FROM +uki-tools-image + # Platform key + RUN openssl req -new -x509 -subj "/CN=Kairos PK/" -days 3650 -nodes -newkey rsa:2048 -sha256 -keyout PK.key -out PK.crt + # CER keys are for FW install + RUN openssl x509 -in PK.crt -out PK.cer -outform DER + # Key exchange + RUN openssl req -new -x509 -subj "/CN=Kairos KEK/" -days 3650 -nodes -newkey rsa:2048 -sha256 -keyout KEK.key -out KEK.crt + # CER keys are for FW install + RUN openssl x509 -in KEK.crt -out KEK.cer -outform DER + # Signature DB + RUN openssl req -new -x509 -subj "/CN=Kairos DB/" -days 3650 -nodes -newkey rsa:2048 -sha256 -keyout DB.key -out DB.crt + # CER keys are for FW install + RUN openssl x509 -in DB.crt -out DB.cer -outform DER + COPY +uki/uki.efi uki.efi + COPY +uki/Uname Uname + ARG KVERSION=$(cat Uname) + + RUN sbsign --key DB.key --cert DB.crt --output uki.signed.efi uki.efi + + + SAVE ARTIFACT /boot/efi/EFI/fedora/mmx64.efi MokManager.efi + SAVE ARTIFACT PK.key PK.key AS LOCAL build/PK.key + SAVE ARTIFACT PK.crt PK.crt AS LOCAL build/PK.crt + SAVE ARTIFACT PK.cer PK.cer AS LOCAL build/PK.cer + SAVE ARTIFACT KEK.key KEK.key AS LOCAL build/KEK.key + SAVE ARTIFACT KEK.crt KEK.crt AS LOCAL build/KEK.crt + SAVE ARTIFACT KEK.cer KEK.cer AS LOCAL build/KEK.cer + SAVE ARTIFACT DB.key DB.key AS LOCAL build/DB.key + SAVE ARTIFACT DB.crt DB.crt AS LOCAL build/DB.crt + SAVE ARTIFACT DB.cer DB.cer AS LOCAL build/DB.cer + SAVE ARTIFACT uki.signed.efi uki.efi AS LOCAL build/$ISO_NAME.signed-$KVERSION.efi + +# This target will prepare a disk.img ready with the uki artifact on it for qemu. Just attach it to qemu and mark you vm to boot from that disk +# here we take advantage of the uefi fallback method, which will load an efi binary in /EFI/BOOT/BOOTX64.efi if there is nothing +# else that it can boot from :D Just make sure to have your disk.img set as boot device in qemu. +prepare-uki-disk-image: + FROM +uki-tools-image + ARG SIGNED_EFI=false + IF [ "$SIGNED_EFI" = "true" ] + COPY +uki-signed/uki.efi . + COPY +uki-signed/PK.key . + COPY +uki-signed/PK.crt . + COPY +uki-signed/PK.cer . + COPY +uki-signed/KEK.key . + COPY +uki-signed/KEK.crt . + COPY +uki-signed/KEK.cer . + COPY +uki-signed/DB.key . + COPY +uki-signed/DB.crt . + COPY +uki-signed/DB.cer . + COPY +uki-signed/MokManager.efi . + ELSE + COPY +uki/uki.efi . + END + RUN dd if=/dev/zero of=disk.img bs=1G count=1 + RUN mformat -i disk.img -F :: + RUN mmd -i disk.img ::/EFI + RUN mmd -i disk.img ::/EFI/BOOT + RUN mcopy -i disk.img uki.efi ::/EFI/BOOT/BOOTX64.efi + IF [ "$SIGNED_EFI" = "true" ] + RUN mcopy -i disk.img PK.key ::/EFI/BOOT/PK.key + RUN mcopy -i disk.img PK.crt ::/EFI/BOOT/PK.crt + RUN mcopy -i disk.img PK.cer ::/EFI/BOOT/PK.cer + RUN mcopy -i disk.img KEK.key ::/EFI/BOOT/KEK.key + RUN mcopy -i disk.img KEK.crt ::/EFI/BOOT/KEK.crt + RUN mcopy -i disk.img KEK.cer ::/EFI/BOOT/KEK.cer + RUN mcopy -i disk.img DB.key ::/EFI/BOOT/DB.key + RUN mcopy -i disk.img DB.crt ::/EFI/BOOT/DB.crt + RUN mcopy -i disk.img DB.cer ::/EFI/BOOT/DB.cer + RUN mcopy -i disk.img MokManager.efi ::/EFI/BOOT/mmx64.efi + END + RUN mdir -i disk.img ::/EFI/BOOT + SAVE ARTIFACT disk.img AS LOCAL build/disk.img + + ### ### Artifacts targets (ISO, netboot, ARM) ### diff --git a/UKI-experimental.md b/UKI-experimental.md new file mode 100644 index 000000000..ab50a68fd --- /dev/null +++ b/UKI-experimental.md @@ -0,0 +1,92 @@ +# UKI: Unified Kernel Image + + +It's basically a kernel, initrd and cmdline for the kernel all lumped up together in an efi binary. Mixing it with something like systemd-stub +means that you can boot from the EFI shell directly into the system. + +You can add more stuff to it like the os-release info, the kernel version (uname), splash image, Devicetree , etc... + +This way you got everything in one nice package and can sign the whole thing for secureboot or calculate the hashes for measured boot. + + +Usually under secureboot the initrd is not signed (as its generated locally), so once the kernel is run initrd signature is not verified. Nor you can measure it with TPM PCRs + +UKI bundles the kernel with initrd and everything else, so you can sign the whole thing AND pre-calculate the hashes for TPM PCRs in advance. + + +Good writeup: https://0pointer.net/blog/brave-new-trusted-boot-world.html + + +### So why not a bit more? + +So why not store the whole system in the initramfs? + +In this branch on the earthfile there is a new target called uki. This will generate an efi with the whole kairos system under the initramfs. +This uses immucore to mount and set up the whole system. + +There is an extra target called `prepare-uki-disk-image` which will generate a disk.img with the efi file inside in the proper place, so you +can just attach that image to a qemu vm and boot from there. An extra arg `SIGNED_EFI` will provide the same image but with a signed efi and all the keys needed +to insert hem into the uefo firmware and test secureboot. + +The only special thing the target does is use objcopy to add sections to the systemd-stub pointing to the correct data: + +```bash +RUN objcopy /usr/lib/systemd/boot/efi/linuxx64.efi.stub \ + --add-section .osrel=Osrelease --set-section-flags .osrel=data,readonly \ + --add-section .cmdline=Cmdline --set-section-flags .cmdline=data,readonly \ + --add-section .initrd=Initrd --set-section-flags .initrd=data,readonly \ + --add-section .uname=Uname --set-section-flags .uname=data,readonly \ + --add-section .linux=Kernel --set-section-flags .linux=code,readonly \ + $ISO_NAME.unsigned.efi \ + --change-section-vma .osrel=0x17000 \ + --change-section-vma .cmdline=0x18000 \ + --change-section-vma .initrd=0x19000 \ + --change-section-vma .uname=0x5a0ed000 \ + --change-section-vma .linux=0x5a0ee000 +``` + +Where: +* Kernel is the kernel that will be booted. + * Initrd is the initramfs that will be booted by the kernel. Currently, a dump of the docker-rootfs...rootfs + * Uname the output of `uname -r` (Optional content) + * Osrelease is the /etc/os-release file from the kairos rootfs (Optional content) + * Cmdline is the line to be passed to the kernel (Optional content, but needed in our case) + + +# Running the efi locally with qemu + +For ease of use there is a target in the earthly file that will generate a disk.img with the efi inside. +Run `earthly +prepare-disk-image` and you will get a `build/disk.img` ready to be consumed + +To run it under qemu use the following arguments: + +```bash +qemu-system-x86_64 -bios $EFI_FIRMWARE -accel kvm -cpu host -m $MEMORY -machine pc \ +-drive file=disk.img,if=none,index=0,media=disk,format=raw,id=disk1 -device virtio-blk-pci,drive=disk1,bootindex=0 \ +-boot menu=on +``` + +Where `$EFI_FIRMWARE` is the OVMF efi firmware and `$MEMORY` is at least 4000. + + +Note that you can also build the uki image signed by passing the `--SIGNED_EFI=true` to earthly. That would produce the same +`build/disk.img` but with some extra files inside, like the certificates needed to be added to the firmware and the MokManager util to install those certificates. + +With those certs and MokManager is possible to install the generated certs to test booting with SecureBoot enabled. + +Note that the `$EFI_FIRMWARE` needs to be set to the OVMF SecureBoot enabled file to test SecureBoot. + +For example under Fedora, the normal firmware with no SecureBoot is found at `/usr/share/edk2/ovmf/OVMF_CODE.fd` while +the SecureBoot enabled one is `/usr/share/edk2/ovmf/OVMF_CODE.secboot.fd` + +Good links: + + - https://man.archlinux.org/man/systemd-stub.7 + - https://wiki.osdev.org/UEFI#UEFI_applications_in_detail + - https://github.com/uapi-group/specifications/blob/main/specs/unified_kernel_image.md + - https://man.archlinux.org/man/systemd-measure.1.en + - https://manuais.iessanclemente.net/images/a/a6/EFI-ShellCommandManual.pdf + - https://0pointer.net/blog/brave-new-trusted-boot-world.html + + + diff --git a/overlay/files/system/oem/00_rootfs.yaml b/overlay/files/system/oem/00_rootfs.yaml index a2a9e7272..72d5568a1 100644 --- a/overlay/files/system/oem/00_rootfs.yaml +++ b/overlay/files/system/oem/00_rootfs.yaml @@ -13,7 +13,7 @@ stages: providers: ["aws", "gcp", "openstack", "cdrom"] path: "/oem" rootfs: - - if: '[ ! -f "/run/cos/recovery_mode" ]' + - if: '[ ! -f "/run/cos/recovery_mode" ] && [ ! -e "/run/cos/uki_mode" ]' name: "Layout configuration" environment_file: /run/cos/cos-layout.env environment: diff --git a/overlay/files/system/oem/10_accounting.yaml b/overlay/files/system/oem/10_accounting.yaml index 301101589..b0dcd93ba 100644 --- a/overlay/files/system/oem/10_accounting.yaml +++ b/overlay/files/system/oem/10_accounting.yaml @@ -16,8 +16,8 @@ stages: homedir: "/home/kairos" groups: - "admin" - - name: "Set user password if running in live" - if: "[ -e /run/cos/live_mode ]" + - name: "Set user password if running in live or uki" + if: "[ -e /run/cos/live_mode ] || [ -e /run/cos/uki_mode ]" users: kairos: passwd: "kairos" diff --git a/overlay/files/system/oem/11_persistency.yaml b/overlay/files/system/oem/11_persistency.yaml index 846050d46..a7b48f82d 100644 --- a/overlay/files/system/oem/11_persistency.yaml +++ b/overlay/files/system/oem/11_persistency.yaml @@ -1,8 +1,44 @@ name: "Configure persistent dirs bind-mounts" stages: rootfs.after: - - if: '[ ! -f "/run/cos/recovery_mode" ]' - name: "Layout configuration" + - if: '[ -e "/run/cos/uki_mode" ]' + # omit the persistent partition on uki mode + # And mount all persistent mounts under the overlay + name: "Layout configuration for UKI" + environment_file: /run/cos/cos-layout.env + environment: + RW_PATHS: "/var /etc /srv /usr" + OVERLAY: "tmpfs:25%" + PERSISTENT_STATE_PATHS: >- + /var + /etc + /etc/systemd + /etc/modprobe.d + /etc/rancher + /etc/sysconfig + /etc/runlevels + /etc/ssh + /etc/ssl/certs + /etc/iscsi + /etc/cni + /etc/kubernetes + /home + /opt + /root + /var/snap + /usr/libexec + /var/log + /var/lib/rancher + /var/lib/kubelet + /var/lib/snapd + /var/lib/wicked + /var/lib/longhorn + /var/lib/cni + /usr/share/pki/trust + /usr/share/pki/trust/anchors + /var/lib/ca-certificates + - if: '[ ! -f "/run/cos/recovery_mode" ] && [ ! -e "/run/cos/uki_mode" ]' + name: "Layout configuration for active/passive" environment_file: /run/cos/cos-layout.env environment: VOLUMES: "LABEL=COS_OEM:/oem LABEL=COS_PERSISTENT:/usr/local" @@ -56,7 +92,7 @@ stages: echo PERSISTENT_STATE_PATHS=\"${PERSISTENT_STATE_PATHS}\" >> /run/cos/cos-layout.env - if: | cat /proc/cmdline | grep -q "kairos.boot_live_mode" - name: "Layout configuration" + name: "Layout configuration for boot_live_mode" environment_file: /run/cos/cos-layout.env environment: VOLUMES: "LABEL=COS_OEM:/oem LABEL=COS_PERSISTENT:/usr/local"