02 · the image
The image
Composed from Fedora packages into a from-scratch rootfs, layered with source-built kernel modules, and carrying only anchors that are safe to publish.
The base variant is composed from Fedora packages and nothing else. No inherited
bootc parent stands behind it, so the
shipped rootfs holds exactly what the compose put there, and its
Containerfile
is public. The layered variants add source-built kernel modules
on top.
The compose
The builder only produces the composed rootfs; nothing else enters the image.
rpm-ostree compose rootfs runs against
manifests/base.yaml
under container=systemd-nspawn, which skips an unshare-net selftest that
fails under rootless podman. bootc container lint runs in CI on the finished
image, and any warning fails the build. The treefile fixes the image’s shape first:
no documentation, no weak dependencies, read-only executables, and a normalized
rpmdb so the same input composes to the same bytes.
Two settings carry all the way downstream.
postprocess.yaml
writes /usr/lib/ostree/prepare-root.conf with composefs enabled and the sysroot
read-only. cairn pulls in no upstream minimal/ostree.yaml, so without that write
the image would boot a read-write sysroot, and the sealed root that
/architecture/sealing/ rests on starts at this line.
System ids freeze at a committed reference: manifests/passwd and
manifests/group are seeded through rpm-ostree’s check-passwd and
check-groups before any package installs, so every package adopts the frozen
id (records D2 and D3). The reserved blocks, and why an instance pins no band, are D4 and D23 in
docs/decisions.md.
The variants
A ZFS host and a GPU host share one composed base, so a foundation fix reaches both on rebuild.
Every variant publishes a per-commit tag. base and base-zfs-nvidia each
earn :stable and :stable-sealed by booting their own sealed
artifact; base-zfs:stable moves with base-zfs-nvidia’s.
Kernel modules are compiled from source against the exact kernel the compose
installed, never DKMS and never akmods.
modules/zfs
builds OpenZFS against the matching kernel-devel because upstream ships no
Fedora repository, and
modules/nvidia
does the same for NVIDIA’s
open-gpu-kernel-modules.
Both builders install into /staged; the consuming layer takes it with one COPY.
The copy cannot reach the compose-built initramfs, and both guards fail the build rather than a host.
Both guards are in the tree: the single-kernel sweep in
images/base-zfs/Containerfile,
and
cairn-verify,
which checks the shipped files against a manifest derived at build time from the
builders’ own install records, so a stray dnf install cannot clobber a
source-built module unseen. Whether a loaded module is trusted (which
kernel keyring its signer resolves in) is a separate gate at
/architecture/pipeline/.
What the image carries
An image is a public artifact, so it carries only what is safe to publish.
The first column is publishable on purpose, the second cannot live in a public artifact, and the third is the host's own.
The Secure Boot auto-enroll payloads sit at
/usr/lib/bootc/install/secureboot-keys/auto/. The auto basename is the
trigger systemd-boot watches, and bootc’s install path copies that tree verbatim
to the ESP. They hold public certificates only, so baking them under the sealed
composefs digest gives the boot chain its own enrollment material
(D19).
The kmod-signing anchor /usr/share/cairn/kmod-signer.der is the same
certificate that signs systemd-boot and every UKI, so no second signing identity
exists (D29),
and its private half lives in a masked CI variable that
never reaches an image layer. Who minted them and how to check them is
the trust chain; every anchor is at /keys/.
Only the os-release PRETTY_NAME line is rewritten, because a sealed UKI’s
.osrel section is what systemd-boot’s menu shows; ID and
VERSION_ID stay.
Package exclusions are /etc/dnf/dnf.conf.d/ drop-ins numbered by the layer
that added them, so a derived image’s dnf install cannot pull back what a
lower layer excluded. cairn ships the nftables package and unit with no ruleset
and leaves it disabled, so a host has no firewall until its instance gives it
one, a trade I accepted, mitigated by key-only SSH.
/usr/local is part of the read-only image: install software in a
Containerfile or container. tun loads at boot. Rootless tun under Fedora
44’s container policy needs setsebool -P container_use_devices on; the
denial logs no AVC.
To test for a bootc host on either backend, run this, not grep -w composefs=,
which the digest after = defeats:
[ -e /run/ostree-booted ] || grep -qE '(^| )composefs=' /proc/cmdline
In a unit: ConditionPathExists=|/run/ostree-booted and
ConditionKernelCommandLine=|composefs.
Annotated at the step
Every operation in every image Containerfile carries a cf:<slug> block: What,
Why and Upstream always, Validated where a live check backs the step, and
Decision where the step enacts a record. When cairn diverges from
upstream extension points, the block says so at
the step, as the systemd-nspawn
selftest skip does in cf:compose.