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:

  1. A Unified Kernel Image (UKI) that combines:

    • Linux kernel

    • initramfs

    • Boot parameters (kernel command line)

  2. The composefs fsverity digest embedded in the kernel command line

  3. 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.

Requirements

Sealed images require:

  • Secure Boot support in the target system firmware

  • A filesystem with fsverity support (e.g., ext4, btrfs) for the root partition

  • The composefs backend for bootc (experimental feature)

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:

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:

  1. 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.

  2. 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

  1. Compute the composefs fsverity digest for the unsealed base image

  2. Generate or provide Secure Boot keys

  3. 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 lint ensures the sealed image is correctly formed

  • Label: The containers.bootc=sealed label 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:

  1. 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"
  2. 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
  3. 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:

  1. 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"
    EOF

    Build this intermediate image and extract the unsigned UKI.

  2. Sign the UKI using your external infrastructure

  3. 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

Installing Sealed Images

Sealed images require special installation flags:

bootc install to-disk /dev/sdX \
    --composefs-backend \
    --boot=uki \
    --filesystem=ext4

The --composefs-backend flag enables the experimental composefs backend, and --boot=uki specifies that the system uses UKI-based booting.

Complete Examples

For complete, working examples of the sealed image build process, refer to: