Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Sealed images

How Sealed Images Work

A sealed image is a cryptographically signed and verified bootc image that provides end-to-end integrity protection. This is achieved through:

  • Unified Kernel Images (UKIs): Combining kernel, initramfs, and boot parameters into a single signed binary
  • Composefs integration: Using composefs with fs-verity for content-addressed filesystem verification
  • Secure Boot: Cryptographic signatures on both the UKI and systemd-boot loader

A sealed image includes:

  1. composefs digest: A SHA-512 hash of the entire root filesystem, computed at build time
  2. Unified Kernel Image (UKI): A single EFI binary containing the kernel, initramfs, and kernel command line with the composefs digest embedded
  3. Secure Boot signature: The UKI is signed with your private key

At boot time, the composefs digest in the kernel command line (e.g. composefs.digest=v1-sha512-12:<digest>) is verified against the mounted root filesystem. This creates a chain of trust from firmware to userspace, ensuring the system will only boot if the root filesystem matches exactly what was signed.

Building Sealed Images

Prerequisites

For sealed images, the container must:

  • Include a kernel and initramfs in /usr/lib/modules/<kver>/
  • Have systemd-boot available (and NOT have bootupd)
  • Not include a pre-built UKI (the build process generates one)

Sealed images also require:

  • Secure Boot support in the target system firmware
  • A filesystem with fs-verity support (e.g., ext4, btrfs) for the root partition

Using without Secure Boot

You can use a sealed UKI without Secure Boot enabled. The composefs and mounting code is fully orthogonal to Secure Boot - the fs-verity digest of the root filesystem and all of its contents will still be validated at runtime, which does provide an increased level of integrity.

However: nothing validates that root digest itself, meaning any locally running code can replace the UKI (e.g. after a container breakout) and fully control the next boot.

It is intentional to support booting with Secure Boot disabled, because a valid use case is to temporarily disable it in order to test a change locally on e.g. one machine, then re-enable it later. However at the current time it is not yet streamlined to regenerate the UKI locally.

This is independent of --allow-missing-verity, which instead makes fs-verity on the root filesystem optional.

Build Pattern: Split the Kernel, Then Generate the UKI in a Separate Stage

Building a sealed image involves three stages: build the rootfs, split the kernel and initramfs out of it, and generate the signed UKI from the split rootfs in a tools stage:

# Build your rootfs with all packages and configuration
FROM <base-image> as rootfs
RUN apt|dnf|zypper install ... && bootc container lint --fatal-warnings

# Split the kernel and initramfs out of the rootfs. This moves
# /usr/lib/modules/<kver>/{vmlinuz,initramfs.img} into /kernel/<kver>/,
# since for a sealed image they end up embedded in the UKI instead.
FROM rootfs as split
RUN mkdir /kernel && bootc container split-kernel-and-rootfs --rootfs / --output /kernel

# Generate the sealed UKI in a tools stage
FROM <tools-image> as sealed-uki
RUN --mount=type=bind,from=split,target=/target \
    --mount=type=bind,from=split,source=/kernel,target=/kernel \
    --mount=type=secret,id=secureboot_key \
    --mount=type=secret,id=secureboot_cert <<EORUN
set -euo pipefail

mkdir -p /out
kver=$(ls /kernel)

# `bootc container ukify` computes the composefs digest of /target, reads
# extra kernel arguments from /target/usr/lib/bootc/kargs.d, and invokes the
# real `ukify` binary with the digest embedded in the cmdline. Everything
# after `--` is passed straight through to ukify.
bootc container ukify \
  --rootfs /target \
  --kernel-dir "/kernel/${kver}" \
  -- \
  --output "/out/${kver}.efi" \
  --signtool sbsign \
  --secureboot-private-key /run/secrets/secureboot_key \
  --secureboot-certificate /run/secrets/secureboot_cert
EORUN

# Final image: the split rootfs (kernel/initramfs already removed) plus the signed UKI
FROM split
COPY --from=sealed-uki /out/*.efi /boot/EFI/Linux/

This pattern works because:

  1. bootc container split-kernel-and-rootfs removes the raw kernel and initramfs from the rootfs ahead of time, so the final image never carries a duplicate copy of them (they end up embedded in the UKI instead)
  2. bootc container ukify handles computing the composefs digest and assembling the kernel command line, so you only need to pass ukify-specific options (like signing) after --
  3. The final stage copies the signed UKI into the already-split rootfs

The bootc container ukify Command

bootc container ukify --rootfs <PATH> [OPTIONS] -- [UKIFY_ARGS...]

This is the recommended way to build a UKI for a bootc image. It computes the composefs digest of --rootfs (using the lower-level compute-composefs-digest primitive described below), reads extra kernel arguments from /usr/lib/bootc/kargs.d, and invokes the system ukify binary with the resulting cmdline. Anything after -- is forwarded to ukify unchanged (e.g. --output, --signtool, signing key/cert options).

Options:

  • --rootfs <PATH>: Root filesystem to operate on (default: /)
  • --kernel-dir <PATH>: Directory containing vmlinuz/initramfs.img, named /parent/<kernel-version>. Needed when the kernel has already been split out of --rootfs, e.g. via split-kernel-and-rootfs
  • --allow-missing-verity: Make fs-verity validation optional, for filesystems that don't support it (e.g. XFS)
  • --erofs-version <v1|v2>: v1 (the default) writes a V1 argument followed by a V2 fallback; v2 writes only V2. See EROFS formats.
  • --write-dumpfile-to <PATH>: Write a composefs dumpfile for debugging

The bootc container compute-composefs-digest Command

bootc container compute-composefs-digest [PATH]

A lower-level primitive, used internally by ukify above, that computes just the composefs digest for a filesystem without building a UKI. The digest is a 128-character SHA-512 hex string that uniquely identifies the filesystem contents. Useful for scripting or debugging outside of the UKI build flow.

Options:

  • PATH: Path to the filesystem root (default: /target)
  • --erofs-version <v1|v2>: EROFS format for the computed digest (default: v1)
  • --write-dumpfile-to <PATH>: Generate a dumpfile for debugging

See also bootc-container-compute-composefs-digest(8).

Final Image Structure

The sealed image should have:

  • The signed UKI at /boot/EFI/Linux/<kver>.efi
  • A signed systemd-boot at /boot/EFI/BOOT/BOOTX64.EFI and /boot/EFI/systemd/systemd-bootx64.efi
  • The raw vmlinuz and initramfs.img removed from /usr/lib/modules/<kver>/ (they're now embedded in the UKI)

External Signing Workflow

For production environments with dedicated signing infrastructure:

  1. Build unsigned UKI: Compute digest and create an unsigned UKI (omit --signtool from ukify)
  2. Sign externally: Take the unsigned UKI to your signing infrastructure
  3. Complete the seal: Inject the signed UKI into the final image

This workflow is planned for streamlining in future releases (see #1498).


The Linux Foundation® (TLF) has registered trademarks and uses trademarks. For a list of TLF trademarks, see Trademark Usage.