| This is an experimental feature and is subject to change. The specifics of this feature should not be relied upon, although user feedback is greatly appreciated! |
Building Sealed Fedora/CentOS bootc Container Images
By default, Fedora bootc images utilize composefs in an "unsigned" mode. However, it is possible to build images which are fully cryptographically signed and validated. This document explains how to transform a typical unsigned bootc image into a sealed version.
Key Concepts
Before diving into the "how", we’ll dive into some details on how an image is transformed into a sealed image.
Overview
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 fsverity for content-addressed filesystem verification
-
Secure Boot: Cryptographic signatures on both the UKI and systemd-boot loader
How Sealed Images Work
A sealed image includes several key components:
-
A Unified Kernel Image (UKI) that combines:
-
Linux kernel
-
initramfs
-
Boot parameters (kernel command line)
-
-
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: [source,bash]
composefs=${COMPOSEFS_FSVERITY} root=UUID=...
This enables the boot chain to verify the integrity of the root filesystem at boot time, ensuring that only the exact content that was signed can be loaded.
Building Sealed Images
Sealed image support is currently experimental. The tooling
and workflows described here are subject to change. In the future,
this functionality is planned to be integrated into
bootc-base-imagectl with commands like bootc-base-imagectl seal
(see bootc issue
#1498). Currently, you must manually implement the build process.
|
The workflows and examples shown below are derived from upstream bootc tooling:
-
tests/build-sealed - The primary, more idiomatic approach used in bootc’s own testing
-
tmt/tests/examples/bootc-uki - An alternative approach with explicit dracut module examples
Both approaches are valid. The build-sealed approach is generally
recommended as it uses cleaner APIs and better separation of concerns,
while the bootc-uki approach provides more explicit examples of
dracut configuration requirements.
Build Approaches
There are two main workflows for building sealed images:
-
Monolithic multi-stage container build: All steps happen within a single container build, including key generation and signing. This is the simplest approach for testing and evaluation.
-
Modular workflow with external signing: The build process is broken into stages, allowing you to extract artifacts (such as unsigned UKIs) and sign them using external infrastructure before continuing. This is necessary for production environments with dedicated signing infrastructure.
Approach 1: Monolithic Multi-Stage Container Build
This approach performs all steps within a single podman build or
docker build invocation. It follows the pattern established in
tests/build-sealed
and
Dockerfile.cfsuki.
Overview of the Process
-
Compute the composefs fsverity digest for the unsealed base image
-
Generate or provide Secure Boot keys
-
Build a multi-stage Containerfile that:
-
Uses a separate buildroot for build tools (ukify, pesign/sbsign)
-
Builds and signs the UKI with the composefs digest embedded
-
Assembles the final image with the signed UKI
-
Step 1: Generate Secure Boot Keys
For testing purposes, generate self-signed Secure Boot keys using openssl:
mkdir -p target/test-secureboot
cd target/test-secureboot
# Generate a unique identifier for your keys
systemd-id128 new -u > GUID.txt
# Generate Platform Key (PK)
openssl req -quiet -newkey rsa:4096 -nodes -keyout PK.key \
-new -x509 -sha256 -days 3650 \
-subj '/CN=Test Platform Key/' -out PK.crt
openssl x509 -outform DER -in PK.crt -out PK.cer
# Generate Key Exchange Key (KEK)
openssl req -quiet -newkey rsa:4096 -nodes -keyout KEK.key \
-new -x509 -sha256 -days 3650 \
-subj '/CN=Test Key Exchange Key/' -out KEK.crt
openssl x509 -outform DER -in KEK.crt -out KEK.cer
# Generate Signature Database key (db)
openssl req -quiet -newkey rsa:4096 -nodes -keyout db.key \
-new -x509 -sha256 -days 3650 \
-subj '/CN=Test Signature Database key/' -out db.crt
openssl x509 -outform DER -in db.crt -out db.cer
cd ../..
| These keys are for testing only. For production use, follow your organization’s key management practices. |
Step 2: Compute Composefs Digest
Compute the composefs fsverity digest for your base image using the
bootc container compute-composefs-digest command:
input_image="quay.io/example/my-bootc:latest"
graphroot=$(podman system info -f '{{.Store.GraphRoot}}')
cfs_digest=$(podman run --rm --privileged --read-only \
--security-opt=label=disable -v /sys:/sys:ro --net=none \
-v ${graphroot}:/run/host-container-storage:ro --tmpfs /var \
"$input_image" bootc container compute-composefs-digest)
echo "Composefs digest: ${cfs_digest}"
This command mounts the container storage into the running container and computes the fsverity digest that will be embedded in the UKI.
Step 3: Create the Multi-Stage Containerfile
Create a Containerfile that builds the sealed image. This follows the pattern in Dockerfile.cfsuki:
# The unsealed base image
ARG base=quay.io/example/my-bootc:latest
# Buildroot for UKI build tools
ARG buildroot=quay.io/centos/centos:stream10
FROM $base AS base
# Install build tools in a separate buildroot
FROM $buildroot as buildroot-base
RUN <<EORUN
set -xeuo pipefail
# systemd-udev provides systemd-measure for PCR measurements
dnf install -y systemd-ukify systemd-udev pesign openssl \
systemd-boot-unsigned
dnf clean all
EORUN
# Build and sign the UKI
FROM buildroot-base as kernel
ARG COMPOSEFS_FSVERITY
RUN --mount=type=secret,id=key \
--mount=type=secret,id=cert \
--mount=type=bind,from=base,target=/target \
<<EOF
set -eux
test -n "${COMPOSEFS_FSVERITY}"
cmdline="composefs=${COMPOSEFS_FSVERITY} console=ttyS0,115200n8 \
enforcing=0 rw"
# pesign uses NSS database - create it from input cert/key
mkdir pesign
certutil -N -d pesign --empty-password
openssl pkcs12 -export -password 'pass:' \
-inkey /run/secrets/key -in /run/secrets/cert -out db.p12
pk12util -i db.p12 -W '' -d pesign
subject=$(openssl x509 -in /run/secrets/cert -subject | \
grep '^subject=CN=' | sed 's/^subject=CN=//')
kver=$(cd /target/usr/lib/modules && echo *)
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 pesign \
--secureboot-certificate-dir "pesign" \
--secureboot-certificate-name "${subject}" \
--measure \
--json pretty \
--output "/boot/$kver.efi"
# Sign systemd-boot as well
sdboot="/usr/lib/systemd/boot/efi/systemd-bootx64.efi"
pesign \
--certdir "pesign" \
--certificate "${subject}" \
--in "${sdboot}" \
--out "${sdboot}.signed" \
--sign
mv "${sdboot}.signed" "${sdboot}"
EOF
# Assemble the final image
FROM base as final
RUN --mount=type=bind,from=kernel,target=/run/kernel <<EOF
set -xeuo pipefail
kver=$(cd /usr/lib/modules && echo *)
mkdir -p /boot/EFI/Linux
target=/boot/EFI/Linux/$kver.efi
cp /run/kernel/boot/$kver.efi $target
# Remove the individual kernel/initramfs now that we have a UKI
rm -v /usr/lib/modules/${kver}/{vmlinuz,initramfs.img}
# Symlink from modules directory to the UKI
ln -sr $target /usr/lib/modules/${kver}/$(basename $kver.efi)
# Validate the sealed image
bootc container lint --fatal-warnings
EOF
FROM base as final-final
COPY --from=final /boot /boot
LABEL containers.bootc=sealed
Key aspects of this Containerfile:
-
Separate buildroot: Build tools (ukify, pesign) are installed in a separate container to avoid bloating the final image
-
Bind mount: The base image is mounted into the build container rather than being used directly
-
pesign: Used for signing (alternatively, you can use sbsign as shown in the bootc-uki tests)
-
Cleanup: The original vmlinuz and initramfs.img are removed after the UKI is created
-
Validation:
bootc container lintensures the sealed image is correctly formed -
Label: The
containers.bootc=sealedlabel marks this as a sealed image
Step 4: Build the Sealed Image
Build the sealed image, passing the composefs digest and Secure Boot keys:
output_image="quay.io/example/my-bootc:sealed"
secureboot="target/test-secureboot"
podman build -t $output_image \
--build-arg=COMPOSEFS_FSVERITY=${cfs_digest} \
--build-arg=base=${input_image} \
--secret=id=key,src=${secureboot}/db.key \
--secret=id=cert,src=${secureboot}/db.crt \
-f Dockerfile.cfsuki .
The resulting image now contains a signed UKI at /boot/EFI/Linux/
with the composefs digest embedded in its kernel command line.
Alternative: Using sbsign Instead of pesign
If you prefer to use sbsign instead of pesign (as shown in
tmt/tests/examples/bootc-uki),
you can modify the kernel stage:
FROM buildroot-base as kernel
ARG COMPOSEFS_FSVERITY
RUN --mount=type=secret,id=key \
--mount=type=secret,id=cert \
--mount=type=bind,from=base,target=/target \
<<EOF
set -eux
test -n "${COMPOSEFS_FSVERITY}"
cmdline="composefs=${COMPOSEFS_FSVERITY} console=ttyS0,115200n8 \
enforcing=0 rw"
kver=$(cd /target/usr/lib/modules && echo *)
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 "/run/secrets/key" \
--secureboot-certificate "/run/secrets/cert" \
--measure \
--output "/boot/$kver.efi"
# Sign systemd-boot with sbsign
sbsign \
--key "/run/secrets/key" \
--cert "/run/secrets/cert" \
"/usr/lib/systemd/boot/efi/systemd-bootx64.efi" \
--output "/boot/systemd-bootx64.efi"
EOF
Approach 2: Modular Workflow with External Signing
Many production environments use dedicated signing infrastructure that cannot be integrated into a container build. This workflow breaks the build process into discrete steps, allowing you to extract the unsigned UKI, sign it externally, and then continue the build.
The commands shown below (bootc-base-imagectl build-uki,
bootc-base-imagectl seal-with-uki) do not currently exist but
represent the planned interface discussed in
bootc issue #1498. For
now, you must implement these steps manually.
|
Conceptual Workflow
The modular workflow would work as follows:
-
Build unsigned UKI: Compute the composefs digest and build an unsigned UKI
# Planned command (not yet implemented) bootc-base-imagectl build-uki \ --from quay.io/example/my-img:unsealed \ --output my-image.uki \ --additional-kargs "console=ttyS0,115200" -
Sign externally: Take the unsigned UKI and sign it using your organization’s signing infrastructure
# Your custom signing process corporate-signing-tool sign \ --input my-image.uki \ --output my-image-signed.uki \ --key-id production-secureboot-key -
Complete the seal: Take the signed UKI and create the final sealed image
# Planned command (not yet implemented) bootc-base-imagectl seal-with-uki \ --from quay.io/example/my-img:unsealed \ --to quay.io/example/my-img:sealed \ --uki my-image-signed.uki
Manual Implementation
Until bootc-base-imagectl supports these operations, you can
implement this workflow manually by modifying the multi-stage build:
-
Build an unsigned UKI by modifying the kernel stage to omit signing:
FROM buildroot-base as kernel ARG COMPOSEFS_FSVERITY RUN --mount=type=bind,from=base,target=/target <<EOF set -eux test -n "${COMPOSEFS_FSVERITY}" cmdline="composefs=${COMPOSEFS_FSVERITY} console=ttyS0,115200n8 \ enforcing=0 rw" kver=$(cd /target/usr/lib/modules && echo *) 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" \ --output "/boot/$kver-unsigned.efi" EOFBuild this intermediate image and extract the unsigned UKI.
-
Sign the UKI using your external infrastructure
-
Create the final sealed image with the signed UKI:
cat > Containerfile.sealed <<EOF FROM quay.io/example/my-bootc:latest RUN <<EORUN kver=\$(cd /usr/lib/modules && echo *) mkdir -p /boot/EFI/Linux rm -v /usr/lib/modules/\${kver}/{vmlinuz,initramfs.img} EORUN COPY my-image-signed.uki /boot/EFI/Linux/<kernel-version>.efi RUN <<EORUN kver=\$(cd /usr/lib/modules && echo *) ln -sr /boot/EFI/Linux/\$kver.efi /usr/lib/modules/\${kver}/\$(basename \$kver.efi) bootc container lint --fatal-warnings EORUN LABEL containers.bootc=sealed EOF podman build -t quay.io/example/my-img:sealed \ -f Containerfile.sealed .
Additional Considerations
Dracut Configuration
The base bootc images typically include the necessary dracut modules for composefs support. However, if you need to customize the initramfs, refer to the dracut configuration examples in tmt/tests/examples/bootc-uki/extra, which includes:
-
extra/usr/lib/dracut/dracut.conf.d/37composefs.conf- Dracut configuration for composefs -
extra/usr/lib/dracut/modules.d/37bootc/- Custom dracut module for bootc initramfs setup
Complete Examples
For complete, working examples of the sealed image build process, refer to:
-
tests/build-sealed - Shell script wrapper for the build process
-
Dockerfile.cfsuki - The multi-stage Containerfile
-
tmt/tests/examples/bootc-uki - Alternative approach with explicit dracut examples
Want to help? Learn how to contribute to Fedora Docs ›