From cd3ef0fd96ac745acc4a472594494b14ffa7b61a Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Tue, 5 May 2026 15:00:55 -0400 Subject: [PATCH 1/4] blog: add sealed images security chain overview First post in a series about bootc sealed images, covering the full trust chain from Secure Boot firmware through per-file fs-verity verification at runtime. Assisted-by: OpenCode (claude-opus-4-6) --- ...026-apr-27-sealed-images-security-chain.md | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 content/blog/2026-apr-27-sealed-images-security-chain.md diff --git a/content/blog/2026-apr-27-sealed-images-security-chain.md b/content/blog/2026-apr-27-sealed-images-security-chain.md new file mode 100644 index 0000000..affb123 --- /dev/null +++ b/content/blog/2026-apr-27-sealed-images-security-chain.md @@ -0,0 +1,168 @@ ++++ +title = "Sealed images: the security chain from firmware to filesystem" +date = 2026-04-27 +slug = "2026-apr-27-sealed-images-security-chain" + +[extra] +author = "jeckersb" ++++ + +# Sealed images: the security chain from firmware to filesystem + +This is the first in a series of posts about bootc sealed images. In +this post we'll take a tour through the full security chain that makes +sealed images work, from the moment your system powers on through +every file read at runtime. Future posts will cover key management, +building sealed images, and deploying them to various environments. + +## The gap + +Typically a default Linux install comes "unlocked", allowing you to +be root and make persistent changes. While many distributions +support Secure Boot, it's typically only the kernel that's signed. +The cryptographic chain of trust from firmware doesn't cover the +root filesystem or applications. Every library loaded, every binary +executed, every configuration file read after the kernel boots runs +on trust. + +This is the gap. The boot chain may be verified. The operating +system is not. + +Sealed images close this gap by extending cryptographic verification +from the firmware all the way through every file in the operating +system. Not as a one-time check at boot, but continuously, at every +file read, for the entire lifetime of the system. + +Let's walk through how each piece fits together. + +## Secure Boot + +Secure Boot is the foundation of the chain. It's a UEFI firmware +feature that ensures only signed code runs during the boot process. + +The firmware maintains a database of trusted keys. When the system +powers on, the firmware loads the bootloader and checks its signature +against those keys. If the signature is valid, the bootloader runs. +If not, the system refuses to boot. + +This is well-established technology and not new to sealed images. +What matters for our purposes is that Secure Boot gives us a root of +trust: a guarantee that the first piece of software to execute after +the firmware is something we explicitly chose to trust. + +## Unified Kernel Images + +Traditionally, the boot chain after Secure Boot looks something like +this: the signed bootloader loads a kernel, an initramfs, and a kernel +command line, often from separate files on a boot partition. The +bootloader may verify the kernel's signature, but the initramfs and +command line are typically not covered by any signature. + +A [Unified Kernel Image (UKI)](https://uapi-group.org/specifications/specs/unified_kernel_image/) +changes this by bundling the kernel, the initramfs, and the kernel +command line into a single EFI binary. The entire bundle is signed +as one unit. This means the firmware (or a signed bootloader like +systemd-boot) can verify the whole thing in one step. + +This is important for sealed images because the kernel command line +is no longer an unverified input. Anything embedded in the command +line is covered by the same signature that covers the kernel itself. +As we'll see next, this is where the composefs digest lives. + +## composefs: EROFS + overlayfs + fs-verity + +Here's where things get interesting. composefs brings together +three kernel technologies to provide cryptographic verification of +the entire filesystem. + +When a sealed image is built, the operating system filesystem is +processed into an +[EROFS](https://docs.kernel.org/filesystems/erofs.html) metadata +image. This image describes every file, directory, symlink, and +permission in the tree, but it does not contain the actual file +contents. Instead, each file entry in the EROFS image records the +[fs-verity](https://docs.kernel.org/filesystems/fsverity.html) +digest of that file's contents. The actual file data is stored +separately in a content-addressed object store, where each file has +fs-verity enabled by the kernel. + +At mount time, the kernel stitches these pieces together using +[overlayfs](https://docs.kernel.org/filesystems/overlayfs.html). +The EROFS image serves as a metadata layer, and the object store +provides the file data. The overlayfs `verity=require` mount option +tells the kernel to enforce that every file served through the mount +has an fs-verity digest matching what the EROFS metadata expects. + +fs-verity itself is a kernel feature that provides per-file integrity +verification. When fs-verity is enabled on a file, the kernel +computes a cryptographic hash of the file's contents and stores a +hash tree alongside the file on disk. From that point on, every +read is verified at the block level -- the kernel checks only the +blocks being read, not the entire file, and caches the results so +repeated reads don't pay the cost again. + +A key behavior to understand is what happens when verification +fails: the kernel returns an I/O error. The corrupted or tampered +data is never served to any process. The `open()` call on the file +succeeds (the file exists, after all), but `read()` on the corrupted +portion returns `EIO`. The data is simply unreadable. + +The EROFS metadata image itself also has an fs-verity digest: a +single cryptographic hash that covers the complete filesystem +description. This is the composefs digest. It is embedded in the +UKI's kernel command line: + +``` +composefs=abc123def456... (128-character SHA-512 hex digest) +``` + +Because the UKI is signed, this digest is now part of the verified +boot chain. The firmware verifies the UKI signature, and the UKI +carries within it a cryptographic commitment to the exact filesystem +that should be mounted as the operating system. + +## Putting it all together + +The full chain looks like this. Each step must succeed before the +next can proceed: + +1. The firmware verifies the signed bootloader. +2. The bootloader verifies the signed Unified Kernel Image (UKI). +3. The initramfs (inside the verified UKI) reads the composefs + digest from the kernel command line (also inside the verified UKI). +4. The initramfs mounts the composefs image and verifies the EROFS + image's fs-verity digest matches the digest from the command line. +5. Every subsequent file read from the operating system is + individually verified by the kernel against the fs-verity digest + recorded in the EROFS metadata. + +There is no point after firmware where unverified code executes or +unverified data is served. The chain is continuous. + +## What sealed images cover + +It's worth being precise about scope. The seal covers every file in +the immutable operating system image: the base OS, any packages you +added, your monitoring agents, security tools, application binaries. +Everything that was part of the image when it was built is sealed. + +By default, `/etc` and `/var` are mutable and persistent. It is +possible to make `/etc` transient today, and this is a good idea if +it matches your use case. More suggestions on this will be in the +documentation. + +## What's next + +This post covered the architecture. In the next posts, we'll get +hands-on: + +- **Key management**: generating Secure Boot keys, enrolling them, + and understanding the signing chain +- **Building sealed images**: writing a Containerfile, producing a + signed UKI, and verifying the result +- **Deploying sealed images**: installing to bare metal, virtual + machines, and cloud environments + +The signing chain is fully under your control. You generate the keys, +you sign the images, you decide what your systems trust. The next +post will show you how. From 0b656cab87b03b462f9c2fabaae0235ef964f541 Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Tue, 5 May 2026 15:00:55 -0400 Subject: [PATCH 2/4] blog: add sealed images key management post Second post in the sealed images series, covering the UEFI Secure Boot key hierarchy, key generation, signing boot artifacts, and key enrollment methods. Also adds a link_checker skip for freedesktop.org which returns 418 to bots. Assisted-by: OpenCode (claude-opus-4-6) --- config.toml | 6 +- ...026-apr-28-sealed-images-key-management.md | 230 ++++++++++++++++++ 2 files changed, 235 insertions(+), 1 deletion(-) create mode 100644 content/blog/2026-apr-28-sealed-images-key-management.md diff --git a/config.toml b/config.toml index e749eb9..25729f8 100644 --- a/config.toml +++ b/config.toml @@ -28,7 +28,11 @@ feed_filenames = ["rss.xml"] #mode #output_dir #preserve_dotfiles_in_output -#link_checker +[link_checker] +# freedesktop.org returns 418 to bots but works fine in browsers +skip_prefixes = [ + "https://www.freedesktop.org/", +] #slugify #search generate_sitemap = true diff --git a/content/blog/2026-apr-28-sealed-images-key-management.md b/content/blog/2026-apr-28-sealed-images-key-management.md new file mode 100644 index 0000000..e3392ef --- /dev/null +++ b/content/blog/2026-apr-28-sealed-images-key-management.md @@ -0,0 +1,230 @@ ++++ +title = "Sealed images: key management for Secure Boot" +date = 2026-04-28 +slug = "2026-apr-28-sealed-images-key-management" + +[extra] +author = "jeckersb" ++++ + +# Sealed images: key management for Secure Boot + +In the [previous post](@/blog/2026-apr-27-sealed-images-security-chain.md), +we walked through the security chain that sealed images provide, from +firmware through runtime file verification. That chain depends on +cryptographic keys at every step. In this post, we'll cover the keys +involved, how to generate them, and how they get enrolled into your +system's firmware. + +## The UEFI Secure Boot key hierarchy + +UEFI Secure Boot uses a hierarchy of three key types, each serving a +distinct role: + +**Platform Key (PK)** -- The root of the Secure Boot trust hierarchy. +There is exactly one PK. The PK controls who can modify the Secure +Boot key databases. It is used to sign updates to the KEK database. +When the PK is enrolled, the firmware transitions from Setup Mode to +User Mode, enabling Secure Boot enforcement. + +**Key Exchange Key (KEK)** -- One or more KEKs can be enrolled. A KEK +is authorized to sign updates to the signature database (db). The +KEK exists to allow delegation: the PK owner can authorize other +parties to manage the set of trusted signing keys without giving them +full control over the Secure Boot configuration. + +**Signature Database (db)** -- The db contains the keys or hashes +that the firmware uses to verify boot binaries. When the firmware +loads a bootloader or UKI, it checks the binary's signature against +the keys in the db. If the signature matches a db entry, the binary +is allowed to execute. + +In practice, many organizations will only need to generate a single +set of these keys. The db key is the one that does the day-to-day +work: it signs your UKIs and bootloader. The PK and KEK exist +primarily to control who can modify the db. + +If you already have Secure Boot keys provisioned in your +infrastructure, you don't need to generate new ones. You only need +access to a db key (or the ability to add your signing key to the +existing db) in order to sign your UKIs. The rest of this post +assumes you're starting from scratch, but the signing steps apply +regardless of where your keys came from. + +## Generating keys + +The following commands generate a complete set of Secure Boot keys. +These examples use `openssl` directly, but you should use whatever +key management practices are appropriate for your organization. +Hardware security modules (HSMs), vault systems, or other key +management infrastructure may be more appropriate for production +use. + +First, generate a GUID to identify the key owner. This is embedded +in the key enrollment data and can be any unique identifier: + +``` +$ uuidgen --random > GUID.txt +``` + +Generate the Platform Key: + +``` +$ openssl req -newkey rsa:2048 -nodes -keyout PK.key \ + -new -x509 -sha256 -days 3650 \ + -subj '/CN=My Platform Key/' -out PK.crt +$ openssl x509 -outform DER -in PK.crt -out PK.cer +``` + +Generate the Key Exchange Key: + +``` +$ openssl req -newkey rsa:2048 -nodes -keyout KEK.key \ + -new -x509 -sha256 -days 3650 \ + -subj '/CN=My Key Exchange Key/' -out KEK.crt +$ openssl x509 -outform DER -in KEK.crt -out KEK.cer +``` + +Generate the Signature Database key: + +``` +$ openssl req -newkey rsa:2048 -nodes -keyout db.key \ + -new -x509 -sha256 -days 3650 \ + -subj '/CN=My Signature Database Key/' -out db.crt +$ openssl x509 -outform DER -in db.crt -out db.cer +``` + +Each command produces three files: a private key (`.key`), a PEM +certificate (`.crt`), and a DER-encoded certificate (`.cer`). The +private keys are what you need to protect. The certificates are +public and are what gets enrolled into the firmware. + +## Signing boot artifacts + +With a db key in hand, you can sign the two boot binaries that need +Secure Boot verification: the bootloader (systemd-boot) and the +Unified Kernel Image. + +Signing systemd-boot: + +``` +$ sbsign --key db.key --cert db.crt \ + --output systemd-bootx64.efi.signed \ + /usr/lib/systemd/boot/efi/systemd-bootx64.efi +``` + +The UKI signing process is more involved because the UKI embeds the +composefs digest, which must be computed from the filesystem first. +We'll cover building sealed images (including UKI generation and +signing) in detail in the next post. For now, the important thing +to know is that `ukify` (the tool that assembles UKIs) accepts the +same `--secureboot-private-key` and `--secureboot-certificate` +options and uses `sbsign` under the hood to sign the final EFI +binary. + +## Enrolling keys into firmware + +The keys need to be enrolled into the UEFI firmware's Secure Boot +database before the firmware will use them for verification. There +are several ways to do this depending on your environment. + +### Manual enrollment via firmware setup + +Most UEFI firmware implementations provide a setup menu (accessed +during early boot, typically via a key like F2 or Del) where Secure +Boot keys can be managed. The DER-encoded certificates (`.cer` +files) can be enrolled from a USB drive or the EFI System Partition. +This is straightforward for individual systems but does not scale +well. + +### Programmatic enrollment for virtual machines + +For virtual machines using OVMF (the open source UEFI firmware for +QEMU/KVM), keys can be enrolled directly into the firmware variable +store using the `virt-fw-vars` tool from the +[virt-firmware](https://gitlab.com/kraxel/virt-firmware) project: + +``` +$ GUID=$(cat GUID.txt) +$ virt-fw-vars \ + --input /usr/share/edk2/ovmf/OVMF_VARS.fd \ + --secure-boot \ + --set-pk $GUID PK.crt \ + --add-kek $GUID KEK.crt \ + --add-db $GUID db.crt \ + -o OVMF_VARS_ENROLLED.fd +``` + +This produces a firmware variable store with your keys pre-enrolled. +When QEMU starts with this variable store, Secure Boot is active and +will only accept binaries signed with your db key. This is +particularly useful for testing and for VM-based deployments where +you control the firmware image. + +### Auto-enrollment via systemd-boot + +systemd-boot supports automatic key enrollment when the firmware is +in Setup Mode (no Platform Key enrolled). This works by placing +signed UEFI authenticated variable files (`.auth` files) on the EFI +System Partition. + +Creating `.auth` files requires converting your certificates into +UEFI signature lists and signing them with the appropriate parent +key: + +``` +$ GUID=$(cat GUID.txt) +$ attr=NON_VOLATILE,RUNTIME_ACCESS,BOOTSERVICE_ACCESS,TIME_BASED_AUTHENTICATED_WRITE_ACCESS + +# Create EFI signature lists +$ sbsiglist --owner $GUID --type x509 --output PK.esl PK.cer +$ sbsiglist --owner $GUID --type x509 --output KEK.esl KEK.cer +$ sbsiglist --owner $GUID --type x509 --output db.esl db.cer + +# Sign the variables (PK and KEK signed by PK, db signed by KEK) +$ sbvarsign --attr $attr --key PK.key --cert PK.crt --output PK.auth PK PK.esl +$ sbvarsign --attr $attr --key PK.key --cert PK.crt --output KEK.auth KEK KEK.esl +$ sbvarsign --attr $attr --key KEK.key --cert KEK.crt --output db.auth db db.esl +``` + +These `.auth` files can then be placed on the ESP where systemd-boot +expects them. bootc can automate this: if you place the `.auth` +files in your container image at +`/usr/lib/bootc/install/secureboot-keys//`, bootc will copy +them to the ESP during installation. On the next boot, systemd-boot +can enroll them into the firmware. + +The enrollment behavior is controlled by the `secure-boot-enroll` +setting in `loader.conf`. See the +[systemd-boot documentation](https://www.freedesktop.org/software/systemd/man/latest/loader.conf.html) +for details on the available modes. + +## Key security considerations + +A few things worth keeping in mind: + +**Protect your private keys.** The `.key` files are what allow +signing boot artifacts. Anyone with access to your db private key +can sign binaries that your firmware will trust. Store private keys +with appropriate access controls, and consider using an HSM or key +management system for production deployments. + +**Key rotation.** The examples above use 10-year expiry (`-days +3650`), which is generous. Plan for key rotation before your +certificates expire. The UEFI key hierarchy makes this manageable: +you can use your KEK to authorize a new db key without needing to +re-enroll the PK. + +**Separation of concerns.** In a CI/CD pipeline, the signing step +should ideally be isolated from the build step. The build process +produces an unsigned UKI, and a separate signing service (with access +to the private key) signs it. This limits the blast radius if the +build environment is compromised. + +## What's next + +With keys generated and an understanding of how they fit into the +trust chain, the next post will walk through building a sealed image +end-to-end: writing a Containerfile, computing the composefs digest, +generating and signing the UKI, and producing a deployable container +image. From 45a24995202574eab6ed3773901281465d66b66d Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Tue, 5 May 2026 15:00:55 -0400 Subject: [PATCH 3/4] blog: add sealed images building post Third post in the sealed images series, covering the multi-stage container build process for producing a sealed, signed bootc image. References the rhel-bootc-examples repository for the complete working example. Assisted-by: OpenCode (claude-opus-4-6) --- .../2026-apr-29-sealed-images-building.md | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 content/blog/2026-apr-29-sealed-images-building.md diff --git a/content/blog/2026-apr-29-sealed-images-building.md b/content/blog/2026-apr-29-sealed-images-building.md new file mode 100644 index 0000000..6eaedf2 --- /dev/null +++ b/content/blog/2026-apr-29-sealed-images-building.md @@ -0,0 +1,200 @@ ++++ +title = "Sealed images: building a sealed image" +date = 2026-04-29 +slug = "2026-apr-29-sealed-images-building" + +[extra] +author = "jeckersb" ++++ + +# Sealed images: building a sealed image + +In the [first post](@/blog/2026-apr-27-sealed-images-security-chain.md) +we covered the security chain behind sealed images, and in the +[second post](@/blog/2026-apr-28-sealed-images-key-management.md) we +generated the Secure Boot keys needed to sign our boot artifacts. +Now it's time to put it all together and build a sealed image. + +A complete working example is available in the +[rhel-bootc-examples](https://github.com/redhat-cop/rhel-bootc-examples) +repository under the +[sealing](https://github.com/redhat-cop/rhel-bootc-examples/tree/main/sealing) +directory. This post walks through the key concepts behind that +example and explains why the build is structured the way it is. + +## The chicken-and-egg problem + +Building a sealed image has a complication that isn't immediately +obvious. Recall from the first post that the composefs digest is +embedded in the UKI's kernel command line, and the UKI lives in the +image under `/boot`. So we have a dependency cycle: we need the +filesystem to compute the composefs digest, but we need the digest +to produce the UKI, and we need the UKI to finalize the filesystem. + +The solution is a multi-stage container build. We build the base +filesystem first, without any UKI. Then in a separate stage, we +compute the composefs digest from that filesystem, generate and +sign the UKI, and finally layer the UKI back on top of the base. +The UKI lives under `/boot`, which is not part of the composefs- +managed root filesystem, so adding it doesn't change the digest. + +## The Containerfile + +The +[Containerfile](https://github.com/redhat-cop/rhel-bootc-examples/blob/main/sealing/Containerfile) +in the examples repository uses four stages. Let's walk through +each one. + +### Stage 1: rootfs-builder + +```dockerfile +FROM quay.io/centos-bootc/centos-bootc:stream10 AS rootfs-builder + +RUN dnf install -y \ + epel-release \ + systemd-boot-unsigned \ + systemd-ukify \ + sbsigntools + +RUN dnf remove -y bootupd +``` + +This starts from a CentOS Stream 10 bootc base image and installs +the tooling we need: `systemd-ukify` to build the UKI, +`sbsigntools` to sign it, and `systemd-boot-unsigned` to provide +the bootloader binary. We remove `bootupd` because this example +uses systemd-boot rather than bootupd + GRUB. + +This stage also signs systemd-boot with our db key: + +```dockerfile +RUN --mount=type=secret,id=secureboot_key \ + --mount=type=secret,id=secureboot_cert <`. +5. Signs the UKI with the db key via `sbsign`. + +The result is a single `.efi` file that embeds a cryptographic +commitment to the exact filesystem it was built from, signed by +a key we control. + +A second `RUN` step then discovers the kernel version and places +the UKI at the expected path under `/boot/EFI/Linux/`. + +### Stage 4: the final image + +```dockerfile +FROM base +COPY --from=kernel /boot /boot +``` + +The final image takes the flattened base and overlays the `/boot` +directory from the kernel stage. This gives us a complete image: +the sealed root filesystem plus the signed UKI that references it. + +## Building the image + +With the Containerfile and keys in place, building the image is a +single `podman build` command: + +``` +$ podman build \ + --secret id=secureboot_key,src=target/keys/sb-db.key \ + --secret id=secureboot_cert,src=target/keys/sb-db.crt \ + -t localhost/sealed-host:latest \ + . +``` + +The two `--secret` flags make the db private key and certificate +available to the build stages that need them, without ever +persisting them in the image. + +If you're using the examples repository, the +[Justfile](https://github.com/redhat-cop/rhel-bootc-examples/blob/main/sealing/Justfile) +wraps this for convenience: + +``` +$ just keygen # generate keys (one-time) +$ just build-host # build the sealed image +``` + +## Secret handling in CI + +The examples repository includes a +[GitHub Actions workflow](https://github.com/redhat-cop/rhel-bootc-examples/blob/main/sealing/.github/workflows/build-sealed.yml) +that demonstrates how to handle key material in CI. The db private +key is stored as a GitHub Actions secret and written to a temporary +file during the build. + +For pull request builds, where the secret is not available, the +workflow generates an ephemeral key pair on the fly. This allows +PRs to validate that the build works without requiring access to +production key material. + +## What's next + +At this point we have a sealed, signed container image. The UKI +inside is signed with our Secure Boot key and embeds a composefs +digest that covers every file in the operating system. In the +next post, we'll deploy this image to a system and verify the +seal is active. From dc70190015006b59626b039cbaf98b8ac24608a7 Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Tue, 5 May 2026 15:00:55 -0400 Subject: [PATCH 4/4] blog: add sealed images deploying post Fourth post in the sealed images series, covering deployment via bcvk and verification that the full security chain is active. Assisted-by: OpenCode (claude-opus-4-6) --- .../2026-apr-30-sealed-images-deploying.md | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 content/blog/2026-apr-30-sealed-images-deploying.md diff --git a/content/blog/2026-apr-30-sealed-images-deploying.md b/content/blog/2026-apr-30-sealed-images-deploying.md new file mode 100644 index 0000000..672b015 --- /dev/null +++ b/content/blog/2026-apr-30-sealed-images-deploying.md @@ -0,0 +1,163 @@ ++++ +title = "Sealed images: deploying and verifying a sealed image" +date = 2026-04-30 +slug = "2026-apr-30-sealed-images-deploying" + +[extra] +author = "jeckersb" ++++ + +# Sealed images: deploying and verifying a sealed image + +In the [previous post](@/blog/2026-apr-29-sealed-images-building.md) +we built a sealed, signed container image. Now it's time to deploy +it to a virtual machine and verify that the full security chain is +active. + +## bcvk: a tool for running bootc VMs + +[bcvk](https://github.com/bootc-dev/bcvk) (bootc virtualization kit) +is a tool that makes it easy to run bootc container images as virtual +machines. Under the hood, it calls `bootc install to-disk` to create +a bootable disk image, then manages the VM lifecycle via libvirt. +For sealed images, bcvk also handles Secure Boot key enrollment into +the VM's UEFI firmware automatically using +[virt-firmware](https://gitlab.com/kraxel/virt-firmware). + +bcvk is available as a package on Fedora and EPEL: + +``` +$ dnf install bcvk +``` + +## Deploying the image + +With the sealed image built from the previous post and keys generated +from the key management post, deploying is a single command: + +``` +$ bcvk libvirt run \ + --ssh-wait \ + --name sealed-demo \ + --filesystem=ext4 \ + --secure-boot-keys target/keys \ + localhost/sealed-host:latest +``` + +Let's break down what's happening here: + +- `--filesystem=ext4` tells `bootc install` to use ext4 for the root + filesystem. The default root filesystem may be XFS, which does not + support fs-verity, so we explicitly select ext4 here. (fs-verity + is supported on ext4 and btrfs.) +- `--secure-boot-keys target/keys` points bcvk to the directory + containing our PK, KEK, and db certificates. bcvk uses + `virt-fw-vars` to enroll these keys into a copy of the OVMF + firmware variable store, so the VM boots with Secure Boot enabled + and configured to trust our signing key. +- `--ssh-wait` tells bcvk to wait until SSH is available before + returning, so we know the system has fully booted. + +If you're using the +[examples repository](https://github.com/redhat-cop/rhel-bootc-examples/tree/main/sealing), +this is wrapped up in a convenient `just` recipe: + +``` +$ just bcvk-ssh +``` + +This builds the image, boots the VM, waits for it to reach +`multi-user.target`, and opens an interactive SSH session. + +## Verifying the seal + +Once the system is booted and we have an SSH session, we can verify +that the full security chain is active. + +### Check the kernel command line + +``` +$ cat /proc/cmdline +composefs=3a7f... (128-character SHA-512 hex digest) rw +``` + +The presence of `composefs=` in the kernel command line +confirms that the initramfs received the composefs digest from the +signed UKI. This is the digest that was computed during the build +and embedded in the UKI's command line section. + +### Check the root mount + +``` +$ findmnt -t overlay / +TARGET SOURCE FSTYPE OPTIONS +/ overlay[/sysroot/state..] overlay ro,relatime,...,verity=require,... +``` + +The key thing to look for is `verity=require` in the mount options. +This confirms that the kernel is enforcing fs-verity verification +on every file access through the composefs mount. Any file in the +operating system whose content doesn't match its expected fs-verity +digest will produce an I/O error when read. + +### Check Secure Boot status + +``` +$ mokutil --sb-state +SecureBoot enabled +``` + +This confirms that the firmware verified the bootloader and UKI +signatures before allowing them to execute. + +### What this tells us + +These three checks together confirm the full chain from the +[first post](@/blog/2026-apr-27-sealed-images-security-chain.md): + +1. Secure Boot verified the bootloader and UKI (`mokutil --sb-state`) +2. The UKI delivered the composefs digest to the initramfs + (`/proc/cmdline`) +3. The kernel is enforcing per-file verification against that digest + (`verity=require`) + +## What happens if something is tampered with? + +It's worth understanding why tampering with a sealed system is +difficult in practice. + +The operating system files are stored in a content-addressed object +store on disk. Each object has fs-verity enabled, which means the +kernel has recorded a cryptographic hash of its contents. The files +are immutable: you cannot write to an fs-verity protected file. Any +attempt to open a verity-protected file for writing will fail. + +Even if an attacker were to bypass the filesystem and corrupt data +directly on the underlying block device, the kernel would detect +the corruption. When a process attempts to read the tampered file, +the kernel verifies the data against the stored hash before +returning it. If the data doesn't match, the read fails with +`EIO`. The corrupted data is never served to any process. + +This is a meaningful distinction from a system where tampered files +are silently served. On a sealed system, corruption doesn't go +unnoticed -- it causes an immediate, visible failure. + +## Conclusion + +Over the course of this series, we've walked through the complete +lifecycle of a sealed image: + +1. [The security chain](@/blog/2026-apr-27-sealed-images-security-chain.md) + from firmware to filesystem that makes sealed images possible +2. [Key management](@/blog/2026-apr-28-sealed-images-key-management.md) + for generating and enrolling Secure Boot keys +3. [Building a sealed image](@/blog/2026-apr-29-sealed-images-building.md) + with a multi-stage container build +4. Deploying and verifying the seal (this post) + +The complete working example is available in the +[rhel-bootc-examples](https://github.com/redhat-cop/rhel-bootc-examples) +repository under the +[sealing](https://github.com/redhat-cop/rhel-bootc-examples/tree/main/sealing) +directory.