BOOTC-SEALED-IMAGES(7)

πŸ“¦
version 1.17.1 Β· 3 versions
, in fedora 43, fedora 44

πŸ¦‹ BOOTC-SEALED-IMAGES(7) πŸ¦‹

okay bestie, sit down, hydrate, because bootc-sealed-images is one of those pages that sounds intimidating and is actually so satisfying once it clicks. it's from the Miscellaneous Information Manual, and it's all about sealing your images up with crypto so nobody can mess with them. let's go πŸ’…


✨ NAME ✨

bootc-sealed-images β€” sealed images, explained, with the whole vibe πŸ’–


🌸 DESCRIPTION 🌸

πŸ’« How Sealed Images Work

so a sealed image is a cryptographically signed and verified bootc image that gives you end-to-end integrity protection. like, from firmware all the way to userspace, everything is checked. no cap. it pulls that off with three things working together:

  • Unified Kernel Images (UKIs) β€” it smushes the kernel, the initramfs, and the boot parameters into ONE single signed binary. one file, one signature, iconic.
  • Composefs integration β€” using composefs with fs-verity for content-addressed filesystem verification. content-addressed means the contents themselves prove who they are, which is honestly main character energy.
  • Secure Boot β€” cryptographic signatures on both the UKI and the systemd-boot loader. double-signed, double-safe 🫢

and a sealed image literally includes these three things:

  1. composefs digest β€” a SHA-512 hash of the entire root filesystem, computed at build time. this is your fingerprint of the whole rootfs.
  2. Unified Kernel Image (UKI) β€” a single EFI binary containing the kernel, initramfs, and kernel command line, with the composefs digest embedded right in there.
  3. Secure Boot signature β€” the UKI gets signed with your private key. πŸ”

then at boot time, the composefs digest sitting in the kernel command line (like composefs.digest=v1-sha512-12:<digest>) gets verified against the mounted root filesystem. that's the chain of trust: firmware β†’ userspace, one unbroken link. the system will only boot if the root filesystem matches exactly what was signed. not approximately. exactly. periodt. πŸ’€


πŸ”₯ Building Sealed Images

πŸŽ€ Prerequisites

for sealed images, your container has got to:

  • include a kernel and initramfs in /usr/lib/modules/<kver>/
  • have systemd-boot available (and NOT have bootupd β€” that's a hard no, bestie)
  • not include a pre-built UKI (the build process is the one that generates it)

and 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 run a sealed UKI without Secure Boot turned on. 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. so you're still getting something real out of it, lowkey.

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. that's the trade. 😭

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. so it's a vibe, but a slightly manual one.

this is independent of --allow-missing-verity (see bootc-composefs(7)), which instead makes fs-verity on the root filesystem optional. different thing, bestie, don't mix them up.

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

building a sealed image is a three-stage glow-up: build the rootfs, split the kernel and initramfs out of it, then 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 β€” and I'm obsessed with the reasoning here:

  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). no wasted bytes, we love an efficient queen. ✨
  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 --. you don't have to think about it.
  3. The final stage copies the signed UKI into the already-split rootfs. clean, simple, done.

πŸ‘‘ 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. what it does: 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). pass-through, no meddling.

options, bestie:

  • --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 (see bootc-composefs(7))
  • --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. honestly, great for when you just want the number, y'know?

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) (see bootc-container-compute-composefs-digest(8)).

πŸ’œ Final Image Structure

your sealed image should have, at the end of it all:

  • 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 β€” this is not a bug, it's the whole point 🫢)

🌈 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 https://github.com/bootc-dev/bootc/issues/1498).


πŸ‘€ SEE ALSO πŸ‘€


⭐ VERSION ⭐

v1.17.1


and that's the whole page, bestie!! πŸŽ€ you now know more about sealed images than 99% of people you will ever meet. this was BOOTC-SEALED-IMAGES(7) from the Miscellaneous Information Manual, version v1.17.1 β€” go forth and seal responsibly, and remember: if the digest doesn't match, it does NOT boot. bye babe πŸ’–βœ¨πŸ¦‹

πŸ“¦ where this page comes from
  • fedora 43: bootc 1.17.1-1.fc43 Β· Apache-2.0 AND BSD-3-Clause AND MIT AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (Unlicense OR MIT)
  • fedora 44: bootc 1.17.1-1.fc44 Β· Apache-2.0 AND BSD-3-Clause AND MIT AND (Apache-2.0 OR BSL-1.0) AND (Apache-2.0 OR MIT) AND (Apache-2.0 WITH LLVM-exception OR Apache-2.0 OR MIT) AND (Unlicense OR MIT)
  • πŸ’– girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms