From 58ef8c87fb24fbf43589a23a9a4bb06de767202b Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Fri, 1 May 2026 15:30:10 -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 | 169 ++++++++++++++++++ 1 file changed, 169 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..2c5ad22 --- /dev/null +++ b/content/blog/2026-apr-27-sealed-images-security-chain.md @@ -0,0 +1,169 @@ ++++ +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 + +Many Linux systems in use today don't use +Secure Boot or any sort of boot-time verification whatsoever. And +even on systems that do use Secure Boot, the verification stops at +the kernel. The bootloader confirms the kernel is signed, and then +the kernel loads an operating system that nobody has verified at all. +Every library loaded, every binary executed, every configuration file +read after that point runs on trust. + +This is the gap. The boot chain is 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) 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. + +## The composefs digest + +Here's where things get interesting. + +When a sealed image is built, the entire operating system filesystem +is processed into a composefs image. composefs represents the filesystem as an +[EROFS](https://docs.kernel.org/filesystems/erofs.html) metadata image +which describes every file, directory, symlink, and permission in the +tree. The actual file contents are stored separately in a +content-addressed object store. + +The EROFS image itself has an +[fs-verity](https://docs.kernel.org/filesystems/fsverity.html) digest: +a single cryptographic hash that covers the complete filesystem +description. Every filename, every permission bit, every symlink +target, every file content hash is captured by this one value. + +This digest is then 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. + +## fs-verity + +fs-verity is a Linux 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. + +For sealed images, every file in the content-addressed object store +has fs-verity enabled. The composefs EROFS image records the expected +fs-verity digest for each file it references. At mount time, the +kernel is told to enforce verification via the overlayfs +`verity=require` mount option. This means every file access goes +through two layers of verification: the EROFS image says "this file +should have digest X," and the kernel confirms that the actual file +on disk matches. + +## 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. + +The seal does not cover `/etc` (writable configuration) or `/var` +(runtime state). This is deliberate. The seal protects the OS you +built. Configuration management and runtime security tools protect +the rest. + +## 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 12da34184b0c9c2ef44e95d8229f36f60d3574a3 Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Fri, 1 May 2026 15:30:14 -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. Assisted-by: OpenCode (claude-opus-4-6) --- ...026-apr-28-sealed-images-key-management.md | 230 ++++++++++++++++++ 1 file changed, 230 insertions(+) create mode 100644 content/blog/2026-apr-28-sealed-images-key-management.md 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..6579be7 --- /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://github.com/rhuefi/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 36dc17dfef8220edde7e3846c30f71f6469b97a7 Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Fri, 1 May 2026 15:30:18 -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 | 197 ++++++++++++++++++ 1 file changed, 197 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..b3ac406 --- /dev/null +++ b/content/blog/2026-apr-29-sealed-images-building.md @@ -0,0 +1,197 @@ ++++ +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. + +After this, the stage 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 4690d625a398bc5179fdbddce65a364a51a4fc93 Mon Sep 17 00:00:00 2001 From: John Eckersberg Date: Fri, 1 May 2026 15:30:22 -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 | 161 ++++++++++++++++++ 1 file changed, 161 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..7cc70e3 --- /dev/null +++ b/content/blog/2026-apr-30-sealed-images-deploying.md @@ -0,0 +1,161 @@ ++++ +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://github.com/rhuefi/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. This is required because fs-verity is only 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 + +``` +$ mount | grep " / " +composefs /sysroot ro,relatime,lowerdir=...,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.