diff --git a/Dockerfile b/Dockerfile index ca821407f..cd95116c2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -32,10 +32,6 @@ WORKDIR /src # First we download all of our Rust dependencies RUN --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome cargo fetch -FROM buildroot as sdboot-content -# Writes to /out -RUN /src/contrib/packaging/configure-systemdboot download - # We always do a "from scratch" build # https://docs.fedoraproject.org/en-US/bootc/building-from-scratch/ # because this fixes https://github.com/containers/composefs-rs/issues/132 @@ -65,6 +61,11 @@ ENV container=oci STOPSIGNAL SIGRTMIN+3 CMD ["/sbin/init"] +# This layer contains things which aren't in the default image and may +# be used for sealing images in particular. +FROM base as tools +RUN --mount=type=bind,from=packaging,target=/run/packaging /run/packaging/initialize-sealing-tools + # ------------- # external dependency cutoff point: # NOTE: Every RUN instruction past this point should use `--network=none`; we want to ensure @@ -81,14 +82,35 @@ ENV SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} # Build RPM directly from source, using cached target directory RUN --network=none --mount=type=cache,target=/src/target --mount=type=cache,target=/var/roothome RPM_VERSION="${pkgversion}" /src/contrib/packaging/build-rpm -FROM buildroot as sdboot-signed +# This image signs systemd-boot using our key, and writes the resulting binary into /out +FROM tools as sdboot-signed # The secureboot key and cert are passed via Justfile # We write the signed binary into /out +# Note: /out already contains systemd-boot-unsigned RPM from initialize-sealing-tools RUN --network=none \ - --mount=type=bind,from=sdboot-content,target=/run/sdboot-package \ --mount=type=secret,id=secureboot_key \ - --mount=type=secret,id=secureboot_cert \ - /src/contrib/packaging/configure-systemdboot sign + --mount=type=secret,id=secureboot_cert < to use a different base -ARG base=localhost/bootc -FROM $base AS base - -FROM base as kernel -RUN <&2; exit 1 - ;; -esac diff --git a/contrib/packaging/configure-variant b/contrib/packaging/configure-variant index 487ea3076..8940286ef 100755 --- a/contrib/packaging/configure-variant +++ b/contrib/packaging/configure-variant @@ -14,20 +14,7 @@ fi # Handle variant-specific configuration case "${VARIANT}" in *-sdboot) - # Install systemd-boot and remove bootupd; - # We downloaded this in an earlier phase - sdboot="usr/lib/systemd/boot/efi/systemd-bootx64.efi" - sdboot_bn=$(basename ${sdboot}) - rpm -Uvh /run/sdboot-content/out/*.rpm - # And override with our signed binary - install -m 0644 /run/sdboot-signed/out/${sdboot_bn} /${sdboot} - # Uninstall bootupd - rpm -e bootupd - rm -rf /usr/lib/bootupd/updates - # Clean up package manager caches - dnf clean all - rm -rf /var/cache /var/lib/{dnf,rhsm} /var/log/* ;; # Future variants can be added here # For Debian support, this could check package manager type and use apt instead diff --git a/contrib/packaging/fedora-extra.txt b/contrib/packaging/fedora-extra.txt index 50bc48f0b..a9f66c015 100644 --- a/contrib/packaging/fedora-extra.txt +++ b/contrib/packaging/fedora-extra.txt @@ -7,5 +7,3 @@ git-core jq # We now always build a package in the container build rpm-build -# Used for signing -sbsigntools diff --git a/contrib/packaging/finalize-uki b/contrib/packaging/finalize-uki new file mode 100755 index 000000000..6de60c2cc --- /dev/null +++ b/contrib/packaging/finalize-uki @@ -0,0 +1,49 @@ +#!/bin/bash +# Finalize UKI installation: copy to /boot, remove raw kernel/initramfs, create symlinks +# +# For sealed UKI images, the kernel and initramfs are embedded inside the signed +# UKI PE binary. We remove the standalone vmlinuz/initramfs.img to: +# - Avoid duplication (they're inside the UKI) +# - Ensure tools use the UKI path +# - Make it clear this is a UKI-only boot configuration +# +# NOTE: The old Dockerfile.cfsuki had a bug where the final-final stage started +# FROM base instead of FROM final, then only copied /boot. This meant the +# vmlinuz/initramfs removal in the final stage was lost. Running this script +# in the actual final image stage fixes that issue. +# +# IMPORTANT: bcvk needs to be updated to find .efi files inside kernel version +# subdirectories (e.g., /usr/lib/modules//.efi) rather than at the +# top level of /usr/lib/modules/. See https://github.com/bootc-dev/bcvk/pull/144 +set -xeuo pipefail + +# Path to directory containing the generated UKI +uki_src=$1 +shift + +# Find the kernel version from the current system +kver=$(cd /usr/lib/modules && echo *) +if [ -z "$kver" ] || [ "$kver" = "*" ]; then + echo "Error: No kernel found" >&2 + exit 1 +fi + +# Create the EFI directory structure +mkdir -p /boot/EFI/Linux + +# The UKI in /boot is outside the composefs-verified tree, which is fine +# because the UKI itself is signed and verified by Secure Boot +target=/boot/EFI/Linux/${kver}.efi +cp "${uki_src}/${kver}.efi" "${target}" + +# Remove the raw kernel and initramfs since we're using a UKI now. +# NOTE: We intentionally keep these for now until bcvk is updated to extract +# kernel/initramfs from UKIs in subdirectories. Once bcvk PR #144 is fixed +# to look for .efi files in /usr/lib/modules//, we can uncomment this. +# rm -v "/usr/lib/modules/${kver}/vmlinuz" "/usr/lib/modules/${kver}/initramfs.img" + +# NOTE: We used to create a symlink from /usr/lib/modules/${kver}/${kver}.efi to the UKI +# for tooling compatibility. However, composefs-boot's find_uki_components() doesn't +# handle symlinks correctly and fails with "is not a regular file". The UKI is already +# found in /boot/EFI/Linux/, so the symlink is not needed. +# See: https://github.com/containers/composefs-rs/issues/XXX diff --git a/contrib/packaging/initialize-sealing-tools b/contrib/packaging/initialize-sealing-tools new file mode 100755 index 000000000..b1e037bed --- /dev/null +++ b/contrib/packaging/initialize-sealing-tools @@ -0,0 +1,15 @@ +#!/bin/bash +set -xeuo pipefail +. /usr/lib/os-release +case "${ID}${ID_LIKE:-}" in + *centos*|*rhel*) + # Enable EPEL for sbsigntools + dnf -y install epel-release + ;; +esac +dnf -y install systemd-ukify sbsigntools +# And in the sealing case, we're going to inject and sign systemd-boot +# into the target image. +mkdir -p /out +cd /out +dnf -y download systemd-boot-unsigned diff --git a/contrib/packaging/install-buildroot b/contrib/packaging/install-buildroot index cc133e970..1bde1a2d2 100755 --- a/contrib/packaging/install-buildroot +++ b/contrib/packaging/install-buildroot @@ -3,14 +3,18 @@ set -xeuo pipefail cd $(dirname $0) . /usr/lib/os-release -case $ID in - centos|rhel) +case "${ID}${ID_LIKE:-}" in + *centos*|*rhel*) + # We'll use crb at build time dnf config-manager --set-enabled crb - # Enable EPEL for sbsigntools - dnf -y install epel-release ;; - fedora) dnf -y install dnf-utils 'dnf5-command(builddep)';; esac +# Deal with dnf4 vs dnf5 +if test -x /usr/bin/dnf5; then + dnf -y install 'dnf5-command(builddep)' +else + dnf -y install 'dnf-command(builddep)' +fi # Handle version skew, xref https://gitlab.com/redhat/centos-stream/containers/bootc/-/issues/1174 dnf -y distro-sync ostree{,-libs} systemd # Install base build requirements diff --git a/contrib/packaging/seal-uki b/contrib/packaging/seal-uki new file mode 100755 index 000000000..253278212 --- /dev/null +++ b/contrib/packaging/seal-uki @@ -0,0 +1,43 @@ +#!/bin/bash +# Generate a sealed UKI with embedded composefs digest +set -xeuo pipefail + +# Path to the desired root filesystem +target=$1 +shift +# Write to this directory +output=$1 +shift +# Path to secrets directory +secrets=$1 +shift + +# Compute the composefs digest from the target rootfs +composefs_digest=$(bootc container compute-composefs-digest "${target}") + +# Build the kernel command line +# enforcing=0: https://github.com/bootc-dev/bootc/issues/1826 +# TODO: pick up kargs from /usr/lib/bootc/kargs.d +cmdline="composefs=${composefs_digest} console=ttyS0,115200n8 console=hvc0 enforcing=0 rw" + +# Find the kernel version +kver=$(cd "${target}/usr/lib/modules" && echo *) +if [ -z "$kver" ] || [ "$kver" = "*" ]; then + echo "Error: No kernel found" >&2 + exit 1 +fi + +mkdir -p "${output}" + +ukify build \ + --linux "${target}/usr/lib/modules/${kver}/vmlinuz" \ + --initrd "${target}/usr/lib/modules/${kver}/initramfs.img" \ + --uname="${kver}" \ + --cmdline "${cmdline}" \ + --os-release "@${target}/usr/lib/os-release" \ + --signtool sbsign \ + --secureboot-private-key "${secrets}/secureboot_key" \ + --secureboot-certificate "${secrets}/secureboot_cert" \ + --measure \ + --json pretty \ + --output "${output}/${kver}.efi" diff --git a/contrib/packaging/switch-to-sdboot b/contrib/packaging/switch-to-sdboot new file mode 100755 index 000000000..be030ec9f --- /dev/null +++ b/contrib/packaging/switch-to-sdboot @@ -0,0 +1,24 @@ +#!/bin/bash +# Switch the target root to use systemd-boot, using the content from SRC +# SRC should contain an "out" subdirectory with: +# - systemd-boot-unsigned RPM (*.rpm) +# - signed systemd-boot binary (systemd-boot*.efi) +set -xeuo pipefail + +src=$1/out +shift + +# Uninstall bootupd if present (we're switching to sd-boot managed differently) +if rpm -q bootupd &>/dev/null; then + rpm -e bootupd + rm -vrf /usr/lib/bootupd/updates +fi + +# First install the unsigned systemd-boot RPM to get the package in place +rpm -Uvh "${src}"/*.rpm + +# Now find where it installed the binary and override with our signed version +sdboot=$(ls /usr/lib/systemd/boot/efi/systemd-boot*.efi) +sdboot_bn=$(basename "${sdboot}") +# Override with our signed binary +install -m 0644 "${src}/${sdboot_bn}" "${sdboot}" diff --git a/docs/src/experimental-composefs.md b/docs/src/experimental-composefs.md index 37d94d7cd..7d2b87673 100644 --- a/docs/src/experimental-composefs.md +++ b/docs/src/experimental-composefs.md @@ -11,60 +11,30 @@ The composefs backend is an experimental alternative storage backend that uses [ **Status**: Experimental. The composefs backend is under active development and not yet suitable for production use. The feature is always compiled in as of bootc v1.10.1. -## Key Benefits +A key goal is custom "sealed" images, signed with your own Secure Boot keys. +This is based on [Unified Kernel Images](https://uapi-group.org/specifications/specs/unified_kernel_image/) +that embed a digest of the target container root filesystem, typically alongside a bootloader (such +as systemd-boot) also signed with your key. -- **Native container integration**: Direct use of container image formats without the ostree layer -- **UKI support**: First-class support for Unified Kernel Images (UKIs) and systemd-boot -- **Sealed images**: Enables building cryptographically sealed, securely-bootable images -- **Simpler architecture**: Reduces dependency on ostree as an implementation detail +### UKIs in bootc containers -## Building Sealed Images +There must be exactly one UKI placed in `/boot/EFI/Linux/.efi`. -### Using `just build-sealed` +### Bootloader support -This is an entrypoint focused on *bootc development* itself - it builds bootc -from source. +To use sealed images, ensure that the target container image has systemd-boot, +and does not have `bootupd`. -```bash -just build-sealed -``` +### Installation -We are working on documenting individual steps to build a sealed image outside of -this tooling. +There is a `--composefs-backend` option for `bootc install`; however, if +a UKI and systemd-boot are detected, it will automatically be used. -## How Sealed Images Work +### Developing and testing bootc with sealed composefs -A sealed image includes: -- A Unified Kernel Image (UKI) that combines kernel, initramfs, and boot parameters -- The composefs fsverity digest embedded in the kernel command line -- Secure Boot signatures on both the UKI and systemd-boot loader - -The UKI is placed in `/boot/EFI/Linux/` and includes the composefs digest in its command line: -``` -composefs=${COMPOSEFS_FSVERITY} root=UUID=... -``` - -This enables the boot chain to verify the integrity of the root filesystem. - -## Installation - -When installing a composefs-backend system, use: - -```bash -bootc install to-disk /dev/sdX -``` - -**Note**: Sealed images will require fsverity support on the target filesystem by default. - -## Testing Composefs - -To run the composefs integration tests: - -```bash -just test-composefs -``` - -This builds a sealed image and runs the composefs test suite using `bcvk` (bootc VM tooling). +Use `just variant=composefs-sealeduki-sdboot build` to build a local sealed +UKI, using Secure Boot keys generated in `target/test-secureboot`. This is +not a production path. ## Current Limitations diff --git a/hack/build-sealed b/hack/build-sealed deleted file mode 100755 index 22b668312..000000000 --- a/hack/build-sealed +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash -set -euo pipefail -# This should turn into https://github.com/bootc-dev/bootc/issues/1498 - -dn=$(cd $(dirname $0) && pwd) - -variant=$1 -shift -# The un-sealed container image we want to use -input_image=$1 -shift -# The output container image -output_image=$1 -shift - -runv() { - set -x - "$@" -} - -case $variant in - ostree) - # Nothing to do - echo "Not building a sealed image; forwarding tag" - runv podman tag $input_image $output_image - exit 0 - ;; - composefs-sealeduki*) - ;; - *) - echo "Unknown variant=$variant" 1>&2; exit 1 - ;; -esac - -cfs_digest=$(${dn}/compute-composefs-digest $input_image) -runv podman build -t $output_image \ - --build-arg=COMPOSEFS_FSVERITY=${cfs_digest} --build-arg=base=${input_image} "$@" -f Dockerfile.cfsuki . diff --git a/hack/lbi/usr/share/containers/systemd/curl-base.image b/hack/lbi/curl-base.image similarity index 100% rename from hack/lbi/usr/share/containers/systemd/curl-base.image rename to hack/lbi/curl-base.image diff --git a/hack/lbi/usr/share/containers/systemd/curl.container b/hack/lbi/curl.container similarity index 100% rename from hack/lbi/usr/share/containers/systemd/curl.container rename to hack/lbi/curl.container diff --git a/hack/lbi/usr/share/containers/systemd/jboss-webserver-5.image b/hack/lbi/jboss-webserver-5.image similarity index 100% rename from hack/lbi/usr/share/containers/systemd/jboss-webserver-5.image rename to hack/lbi/jboss-webserver-5.image diff --git a/hack/lbi/usr/share/containers/systemd/podman.image b/hack/lbi/podman.image similarity index 100% rename from hack/lbi/usr/share/containers/systemd/podman.image rename to hack/lbi/podman.image diff --git a/hack/provision-derived.sh b/hack/provision-derived.sh index 029f9fc4f..c7f165729 100755 --- a/hack/provision-derived.sh +++ b/hack/provision-derived.sh @@ -127,7 +127,7 @@ fi # for testing bootc install on Fedora CoreOS where these would conflict. if test -z "${SKIP_CONFIGS:-}"; then # For test-22-logically-bound-install - cp -a lbi/usr/. /usr + install -D -m 0644 -t /usr/share/containers/systemd/ lbi/* for x in curl.container curl-base.image podman.image; do ln -s /usr/share/containers/systemd/$x /usr/lib/bootc/bound-images.d/$x done