Peios Learn
Products
PePeios pkpekit PvProvium UDUniversal Directory TrTrail PrProject WiWispist
Using Peios Security Basics Technical Documentation Source
Using Peios Security Basics Technical Documentation Source
Peios

Boot and trust establishment

Single-page view · as markdown

Boot and trust establishment

Peios / Peios Security Fundamentals / Boot and trust establishment

Boot is the sequence by which Peios goes from "a kernel has been loaded and is starting" to "a fully-running system with real principals, real services, real policy". Most operating systems have a boot sequence, but Peios has a specific trust sequence — the kernel needs to establish how authority works on this running system from a starting point where no userspace exists yet, no authd is running, no directory has been consulted, and no tokens have been minted by anything but the kernel itself.

The chain of events is short and well-defined. The kernel sets up its own bootstrap tokens directly, attaches one to the first userspace process, and starts it. That first process is prelude, the PID 1 of an in-memory startup environment — the initramfs — whose job is to mount the system's real root filesystem and hand the machine over to it. peinit (signed at TCB, sitting at PID 1 of the real root) then takes over and begins launching services. Eventually authd starts and assumes responsibility for creating real identities for users and services. Each step relies on the previous; the chain has to be intact for the resulting system to be trustworthy.

This page covers the chain at a high level. Later pages cover each part in depth.

What boot establishes #

By the time boot is complete, the system has:

  • Two kernel-direct bootstrap tokens (SYSTEM and Anonymous) that exist for the lifetime of the running kernel.
  • A peinit process running as PID 1, signed at TCB level, holding the SYSTEM token's privileges plus knowledge of which services to launch.
  • An authd process running (started by peinit), populated with the deployment's policy (privileges, claims, CAAP), ready to authenticate users and services.
  • A set of service processes running under tokens authd minted, each with their own identity and authority.
  • A populated CAAP cache in the kernel (pushed by authd).
  • Mount policies applied (set by peinit at boot).
  • The handle model in steady state — services have opened the files they need; their fds have cached masks.

Once all of this is in place, the system is "up". Users can sign in (authd handles the authentication; produces tokens; peinit launches their session processes); access checks resolve normally; audit events flow through KMES; everything that the rest of these docs describes is operating.

The interesting question is what happens during boot, when each of those pieces is being established. The chain is a sequence of "this couldn't work before, but now it can":

  1. Before the kernel finishes init, nothing works. The kernel is itself.
  2. After kernel init, the kernel has constructed the SYSTEM and Anonymous tokens. The first userspace process — prelude, the initramfs PID 1 — starts running on the SYSTEM token.
  3. After prelude has run the initramfs, the system's real root filesystem is mounted. prelude switches / to it and execs the real init.
  4. After peinit takes over at PID 1 of the real root, the kernel has a userspace component that can launch other userspace components. peinit can fork-and-exec services.
  5. After authd starts, the system has a way to mint real identities for users and services. Until now, every process was running on SYSTEM (inherited from init); from this point on, services can run as their own identities, and users can sign in.
  6. After authd has populated the CAAP cache, central access policies actually apply. Objects referencing CAAP get evaluated against the policy rather than the recovery policy.
  7. After mount policies have been applied, FACS knows what to do on each filesystem.

Each transition is the "next thing that has to happen for the system to be coherent". They roughly correspond to the pages in this topic.

The bootstrap problem #

The deeper question: how does any of this work in the first place? authd creates tokens, but authd is itself a process — what token does it run on? peinit launches services, but peinit is also a process — what privilege does it hold to be allowed to mint child tokens? The system has to have authority and identity to do anything, but authority and identity are exactly what boot is supposed to establish.

The kernel solves this with a few "primordial" operations that don't go through the normal pipeline:

  • The SYSTEM token is constructed directly by the kernel, not via kacs_create_token. It's there from the moment the kernel finishes initialising, before any userspace process exists.
  • The SYSTEM token has every privilege enabled, integrity System, every group SID that any token might ever need. It's authority-maximal.
  • init runs on the SYSTEM token. Every child it forks inherits the SYSTEM token until something replaces it.
  • peinit is signed at TCB level (verified by the kernel at exec — the verification happens through the same signing path every binary does). Once peinit is running on the SYSTEM token, it has authority to do anything; once peinit is also signed at TCB level, it has PIP authority too.
  • authd is similarly signed at TCB and run by peinit with the SYSTEM token. Authd then uses its SeCreateTokenPrivilege (from the SYSTEM token) to mint other tokens for other processes.

The chain works because each link is held by the previous. The kernel builds the SYSTEM token (link 1); prelude, the initramfs PID 1, runs on it (link 2); peinit takes over from prelude when the system switches to the real root (link 3); peinit launches authd (link 4); authd creates real tokens (link 5). Each link can do its job because the previous link gave it the authority.

The risk: anything that breaks a link breaks the chain. A peinit not signed at TCB level wouldn't get PIP protection at exec; a compromised authd would mint malicious tokens. The signing-and-PIP machinery is what makes the chain trustworthy: each binary in the chain is one whose signature the kernel verifies, and that signature anchors the kernel's trust in what the binary does next.

The "everything is SYSTEM" phase #

There is a brief window during boot where every userspace process is running on the SYSTEM token. From prelude starting in the initramfs, through the early phases of peinit, up until peinit starts launching real services as themselves, every process is SYSTEM.

This is a feature, not a bug. The SYSTEM token is authority-maximal. Whatever early-boot work needs to happen — reading configuration, populating mount policies, launching authd itself — can be done without worrying about whether the running process has the right privileges. It does, because everything is SYSTEM.

The window is short. peinit aims to launch authd as quickly as possible because most of the system is not useful until authd is running. From the moment authd is up, identities can be real, services can run as themselves, users can sign in. The SYSTEM-everywhere phase is what enables the bootstrap, then ends as soon as the real identity machinery can take over.

Services that must not start before authd is ready include anything that handles user requests. A file-server that started in the SYSTEM phase would have its access checks running as SYSTEM, not as the principal it should be acting for — which is operationally wrong even though it's authoritatively allowed. peinit waits for authd to be ready before launching such services.

Services that may start before authd include things that only need their own state — the registry daemon, observability daemons. These can run as SYSTEM for the rest of the boot if they need to, then ideally drop privileges or get reassigned a more specific token once authd is up.

The exact ordering — which services need to wait for what — is part of peinit's startup configuration, not a kernel concern.

Where to start #

If you want the kernel-direct bootstrap tokens — what SYSTEM and Anonymous contain, why they're constructed without going through kacs_create_token, how they relate to the rest of the token model — read Bootstrap tokens.

If you want the initramfs stage — prelude, the in-memory startup environment, the /boot/initramfs/ directory it is built from, and the handoff to the real root — read The initramfs stage.

If you want how the initramfs is composed — boot hooks, the capabilities they declare, how their order is resolved, and how to write one — read Boot hooks.

If you want peinit's role — what makes it the right thing to be PID 1, the service-launching pattern, the lifecycle-manager role that falls out of PIP — read peinit at PID 1.

If you want the authd transition — what authd does at startup, the CAAP cache population, the SYSTEM-everywhere-handoff — read authd handoff.

If you want the kernel-level invariants that the boot chain depends on — the LSM stack, the build config flags, what the kernel refuses to do — read Kernel invariants.

The boot artifacts themselves are built by two command-line tools, the two halves of Peios' Dynamic Boot system. mkirf compiles the /boot/initramfs/ source tree into the deterministic initramfs image; mkuki then wraps that image, together with a kernel and command line, into the single UEFI unified kernel image the firmware boots. Both can run once or stay resident, keeping the boot image current as their inputs change.

Bootstrap tokens

Peios / Peios Security Fundamentals / Boot and trust establishment

The kernel constructs two tokens directly during initialisation, before any userspace process exists. These are the SYSTEM token and the Anonymous token. Both exist as singletons in the kernel from the moment kernel init completes; both persist for the lifetime of the running kernel; both are created without going through kacs_create_token (there is no userspace caller to make that syscall).

The two tokens have opposite roles. SYSTEM is the authority-maximal token — every privilege, every well-known administrative group, integrity System — that gets attached to init and bootstraps every other userspace token. Anonymous is the identity-minimal token — no real principal, no privileges, integrity Untrusted — that backs the Anonymous-level of impersonation.

This page covers what each contains and why they're constructed this way.

SYSTEM — authority-maximal bootstrap #

The SYSTEM token is what init runs on. Specifically:

FieldValue
user_sidS-1-5-18 (the SYSTEM well-known SID)
groupsBUILTIN\Administrators (with SE_GROUP_OWNER), Everyone, Authenticated Users, Local, and the bootstrap logon SID S-1-5-5-0-0
integrity_levelS-1-16-16384 (System integrity — the highest)
mandatory_policyNO_WRITE_UP
privilegesEvery privilege defined in the catalog, all present, all enabled
token_typePrimary
impersonation_levelAnonymous (conventional for primary tokens)
auth_id0 (the SYSTEM session)
default_daclGrants SYSTEM and BUILTIN\Administrators GENERIC_ALL; denies everyone else by absence

The SYSTEM token is the most powerful identity on a running Peios system. It has every privilege, full administrative-group membership, the highest integrity. The only thing it lacks is a specific real principal — it's a system-internal identity, not a user.

The SYSTEM token has session ID 0, which corresponds to the SYSTEM session (also kernel-constructed). The session is a peer of the token; both exist together from kernel init.

The default DACL on the SYSTEM token is restrictive in an important way: when SYSTEM creates a new object without specifying an explicit SD, the default DACL ends up on the object. Granting only SYSTEM and Administrators means objects SYSTEM creates are administrator-only by default. This is the right policy for things like service configuration files — they should not be readable by every user just because they were created during boot.

Anonymous — identity-minimal singleton #

The Anonymous token has the opposite shape. It's not used for running processes; it's used as the identity of Anonymous-level impersonation tokens (the lowest impersonation level — see Impersonation levels).

FieldValue
user_sidS-1-5-7 (the Anonymous well-known SID)
groupsEveryone only
integrity_levelS-1-16-0 (Untrusted — the lowest)
mandatory_policyNO_WRITE_UP
privilegesNone
token_typeImpersonation (it exists specifically for impersonation)
impersonation_levelAnonymous
auth_id998 (the Anonymous session)

The Anonymous token has the bare minimum to be a token at all. No real principal (the user SID is the well-known "Anonymous" placeholder). Just Everyone for groups — explicitly not Authenticated Users. No privileges. Untrusted integrity.

When a client connects to a server and sets the impersonation level to Anonymous (or the gates downgrade the impersonation level to Anonymous), the server's effective token becomes a derivation of this Anonymous token — essentially a copy with the relevant session bindings. The kernel's "Anonymous identity" is centrally defined here; the rest of the system inherits from it.

Why kernel-direct construction #

The two tokens are constructed by direct kernel initialisation, not via kacs_create_token. This matters because kacs_create_token requires SeCreateTokenPrivilege — which requires a caller — which requires a token — which is exactly what's being established. The bootstrap problem.

The kernel breaks the cycle by constructing these two tokens with internal code that doesn't go through the usual API. The kernel can do this because it is the kernel; there's no privilege check on itself.

The implications:

  • No record exists of who minted these tokens. The source field, normally set to whatever component called kacs_create_token, is set by the kernel to indicate "kernel-internal". The tokens were not minted by authd, peinit, or any other component.
  • They cannot be re-created at runtime. Even though both tokens have the same shape every time they're constructed (the values are kernel-internal constants), no userspace component can ask the kernel to construct another SYSTEM token at the same level. SeCreateTokenPrivilege lets userspace create new tokens; it doesn't grant access to construct kernel-internal singletons.
  • They are singletons. There is exactly one SYSTEM token and one Anonymous token at any moment in the kernel. Other tokens may reference them or be derived from them, but the canonical instances are unique.

The kernel-direct construction is the foundation: it gives the rest of the system a starting identity to build from. Once SYSTEM exists and is attached to init, everything that follows can use normal API calls — they have a token, they have privileges, they can mint other tokens.

What's attached to what #

At the end of kernel init:

  • SYSTEM token. Constructed; ready.
  • Anonymous token. Constructed; ready.
  • SYSTEM session (ID 0). Constructed; the SYSTEM token is bound to it.
  • Anonymous session (ID 998). Constructed; the Anonymous token is bound to it.
  • init process. About to start; will have the SYSTEM token attached to it.

Init starts and takes the SYSTEM token as its primary token. The first userspace process the kernel runs is prelude, the PID 1 of the initramfs — the in-memory startup environment that mounts the real root (see The initramfs stage). The SYSTEM token survives every exec along the way: prelude runs on it, and when prelude hands the machine to the real root it execs peinit, which takes over with the SYSTEM token as its own.

From this point on, the only entity with the SYSTEM token is peinit (and anything peinit chooses to fork before assigning each child its own token). When peinit launches authd, it does so by forking and then either letting the child inherit SYSTEM (briefly, until authd starts assigning a different token to itself) or by replacing the token with one peinit prepared.

The Anonymous token is not attached to any process. It exists as a kernel-internal object; impersonation tokens at the Anonymous level are derivations of it but the canonical instance lives in the kernel.

Lifecycle #

Both bootstrap tokens are never destroyed during the kernel's lifetime. They are reference-counted like any other tokens, but their references never drop to zero because:

  • The SYSTEM token has at least one attachment (initially init, then peinit, then whatever process inherits from peinit). Even after peinit assigns specific tokens to most child processes, peinit itself continues to run on SYSTEM.
  • The Anonymous token is a kernel-internal singleton. The kernel holds a reference to it; the reference is not dropped until kernel shutdown.

On reboot, the kernel re-constructs them at init. They are not persistent — there is no on-disk SYSTEM-token record — they are re-built fresh every boot. The fields are deterministic, so the new instances are equivalent to the previous ones structurally; the actual token instances are different objects.

The session IDs (0 for SYSTEM, 998 for Anonymous) are also fixed across boots. The logon SID derived from the SYSTEM session is S-1-5-5-0-0.

Why these specific shapes #

The SYSTEM token has every privilege and full administrative-group membership because it needs to be able to do anything. Boot involves operations that require a wide range of privileges — SeLoadDriverPrivilege to load kernel modules, SeBackupPrivilege / SeRestorePrivilege for early-system manipulation, SeShutdownPrivilege for orderly shutdown. Restricting SYSTEM would force peinit to know in advance which privileges it would need, which is brittle.

SeBackupPrivilege and SeRestorePrivilege are special — they're on the SYSTEM token at boot, but peinit typically calls FilterToken to remove them when assigning tokens to services that don't need them. SYSTEM having them is the starting point; the actual services usually run without them.

The Anonymous token has the opposite design: nothing it shouldn't need. The Anonymous identity is what gets used when a caller has not authenticated, and granting it any group beyond Everyone would risk accidentally giving anonymous callers access to resources whose DACLs name those groups. Empty privileges, Untrusted integrity, no Authenticated Users membership.

The two tokens together establish the trust range. SYSTEM is "the most trust the system can confer"; Anonymous is "the least trust meaningful". Every other token sits somewhere between, with parameters set by authd from the directory.

What the bootstrap tokens are not #

A few clarifications:

  • They are not user accounts. SYSTEM is the system's own identity; Anonymous is the placeholder for "no identity". Neither corresponds to a user that signed in.
  • They are not interchangeable with administrator tokens. A real user in the BUILTIN\Administrators group has a different token from SYSTEM — same group membership conceptually, but with the user's actual user_sid, the user's session, etc. SYSTEM and Administrators are not the same identity.
  • They don't persist across reboots. Every boot constructs fresh instances. Anything keyed to the SYSTEM token's instance ID (which is rare; nothing should be) would lose its reference at reboot.
  • They cannot be modified. Almost everything about them is immutable. AdjustPrivileges would technically be possible on SYSTEM (a token holder of SYSTEM could enable/disable privileges, or even remove them) but the kernel makes such adjustments a one-time event for the bootstrap token, and the field-level operations are constrained. In practice nothing modifies the SYSTEM token directly; peinit makes filtered copies for child tokens.

Where to go next #

For the first userspace process the SYSTEM token is handed to, read The initramfs stage.

For the moment real identities start replacing SYSTEM, read authd handoff.

For what a token is in general — types, fields, lifecycle — read Tokens.

The initramfs stage

Peios / Peios Security Fundamentals / Boot and trust establishment

The kernel cannot reach the real root filesystem on its own. By the time it has finished its own initialisation it can speak to a CPU and some memory, but the disk the system is installed on may sit behind a storage driver that is not yet loaded, a volume manager that has not been assembled, or an encrypted container that has not been unlocked. Mounting that filesystem is work, and it is work that has to be done in userspace — the kernel does not mount real roots by guesswork.

So there is a stage in between. The bootloader loads a second thing into memory alongside the kernel: a small, complete root filesystem called the initramfs. The kernel unpacks it into a RAM-backed filesystem, makes that filesystem /, and starts a PID 1 inside it. This in-memory system exists for one purpose — to get the real root mounted and then get out of the way. On Peios, the PID 1 of that in-memory system is prelude.

The initramfs stage sits between two of the other pages in this topic. Bootstrap tokens covers what the kernel constructs before any userspace exists; peinit at PID 1 covers the init system that runs on the real root. prelude is what runs in between — the first userspace process the system ever has, and the last thing that runs before the real system begins.

prelude in the trust chain #

prelude is the first userspace process on the machine. The kernel attaches the SYSTEM token to it — prelude is the "init" that Bootstrap tokens describes the SYSTEM token being handed to. From the moment prelude starts, the "everything is SYSTEM" phase of boot (covered in the overview) is underway: inside the initramfs there is exactly one identity, SYSTEM, and exactly one job, getting to the real root. There is no directory, no authd, no notion of separate principals — and no need for one. The initramfs is a single-purpose, single-identity environment.

The initramfs's integrity is the integrity of the initramfs image itself. The image is a sealed archive: once it is built, nothing edits it in place. It is read into RAM at boot, used once, and discarded at the handoff — there is nothing inside it to tamper with at runtime. Under Secure Boot (a later milestone), the firmware verifies the whole boot artifact — kernel and initramfs together — before the kernel is allowed to run at all, so the sealed image is also a verified one.

prelude — the initramfs PID 1 #

prelude is a small, dedicated init. It is not a general-purpose process manager and it is not peinit: peinit supervises services for the system's entire lifetime, whereas prelude runs one short, fixed sequence and then replaces itself with the real init. They are separate programs with separate jobs. The name reflects the relationship — prelude is the short piece that runs before peinit's main one.

What prelude itself does is deliberately minimal. It is the invariant skeleton of the boot — the part that is identical on every Peios machine. Preparing the kernel's virtual filesystems, running the hooks, checking that a root was mounted, switching to it: that is the whole of prelude. Everything that varies between machines — which storage driver is needed, whether there is disk encryption, what kind of filesystem the root is — is not in prelude at all. It is in boot hooks.

This division is the important design point. A machine that boots from a plain disk, a machine that boots from an encrypted volume, and a machine that boots over a network differ only in their hooks. prelude is the same binary on all three. It never has to change to support a new kind of deployment — a new deployment is a new hook, not a new prelude.

Where the initramfs comes from #

The initramfs is not a mysterious binary blob. It is compiled from an ordinary directory on the real root filesystem: /boot/initramfs/. Whatever is in that directory becomes the contents of the in-memory root. The directory is the source; the initramfs image is the build product.

The layout is straightforward:

/boot/initramfs/
    init                 symlink to the prelude binary — the initramfs PID 1
    usr/
        sbin/prelude     the prelude binary itself
        bin/
            dash         the shell; sh -> dash provides /usr/bin/sh
            peiosutils   one multi-call binary backing mount, chroot, ls, … via symlinks
    lib64 -> usr/lib/x86_64-linux-peios
                         the x86-64 ABI loader path; /bin, /sbin and /lib views
                         do not exist before the runtime topology is mounted
    usr/libexec/prelude/hooks.d/
        mount-root.sh    mounts a live medium's squashfs (live-boot)
        mount-root-disk.sh
                         mounts an installed root partition (disk-boot); both
                         contribute rootfs-ready, and `root=` on the cmdline
                         decides which one acts
        mount-rootfs-stratafs-base.sh
                         mounts the real root's StrataFS views after rootfs-ready
        ...
    lcl/libexec/prelude/hooks.d/
                         operator-placed hooks; a file here replaces a packaged
                         hook of the same name

It is a normal directory. You can list it, read it, and see exactly what the initramfs will contain — inspecting the boot environment is ls /boot/initramfs/, not unpacking an archive. This is intentional: the initramfs should feel like part of the filesystem an administrator already understands, not a separate, opaque build system.

Most of what lands in the directory is put there by packages. A boot feature — disk encryption, an exotic storage backend — is an ordinary peipkg; installing it drops its hook (and any helper binaries) into /boot/initramfs/, and removing the package takes them away. There is no separate "initramfs configuration" to edit and no central list of supported features: the directory is the configuration, and a package contributes to the boot simply by installing files into it. Boot hooks covers this in full.

Because hooks are shell scripts, the initramfs has to carry a shell. That shell is dash: it provides the sh capability prelude depends on and supplies /usr/bin/sh. Hooks use #!/usr/bin/sh, because they run before any root-level /bin StrataFS view exists. The ordinary utilities a hook invokes — mount, chroot, ls, and the rest — come from peiosutils, a single multi-call binary that backs each tool through a symlink (mount, chroot, and friends are all the one binary, invoked under different names). Driver- or filesystem-specific tools a particular hook needs — blkid, a mkfs helper — arrive with the feature package that ships that hook, not with the base initramfs. Module loading is the exception: modprobe comes from kmod and the modules themselves from kernel-modules-irf, both base packages rather than per-feature ones, because any hook that touches storage may need them.

Kernel modules in the initramfs #

The initramfs carries its own set of kernel modules, shipped by the kernel-modules-irf package. It is a separate root from the real one and shares nothing with it, so it needs its own copy of anything it uses — the same reason it needs its own mountpoints and its own loader link.

The set is deliberately a subset. The initramfs only has to reach the root filesystem, so it carries storage controllers and block devices, the input drivers a passphrase prompt needs, filesystem and crypto drivers, and network drivers for a root reached over the network. It does not carry graphics, sound, media or wireless: the real root can load those for itself once it is mounted, and everything in the initramfs is paid for on every boot — it is loaded whole into memory, and under a UKI it sits inside an image the firmware verifies in one piece.

The set is also generic: every machine gets the same modules, rather than a set tailored to the hardware it was built on. Two things make that the only workable choice. Composing a root resolves offline and runs no install-time side effects, so nothing in that path may inspect the hardware it is composing for. And because a UKI is signed as a single image, a per-machine initramfs would have to be assembled and signed on the target — which would mean a signing key on every installed machine. A generic initramfs is also what mainstream distributions default to; tailoring it is the opt-in, not the norm.

A module being available in the initramfs is not the same as it being loaded, and the kernel never loads a driver for hardware on its own: it enumerates the devices and emits a uevent for each one naming the module that can drive it, but binding a driver to a device is userspace's job. In the initramfs that job is done once, by the coldplug hook that the coldplug-irf package ships. It runs after the initramfs's own views are assembled and before any root-mount hook, reads the modalias of every device the kernel has enumerated under /sys/bus, and hands the whole set to modprobe in one call. A machine whose root sits behind a modular storage driver — NVMe, for instance — has that driver loaded by the time the root-mount hook goes looking for the disk. Aliases that match no module are the normal case and are ignored; a module that fails to load is reported and is not, on its own, a boot failure — whether the missing driver matters is for the root-mount hook to decide, because it is the one that knows which disk it needs.

The hook is a single pass and does not stay resident, which is enough for the initramfs: everything that matters to reaching the root is present before PID 1 runs. Devices that appear later, and the stable /dev/disk/by-* names, belong to the device manager on the real root.

What prelude does at boot #

When the kernel starts prelude, it runs one fixed sequence, in order:

  1. Prepare the environment. prelude mounts the kernel's virtual filesystems — /proc, /sys, /dev — so that it and the hooks can see processes, devices, and kernel state, and arranges the mount environment so the later root switch is unobstructed.
  2. Determine the target init. prelude reads the kernel command line for an init= value — the program to hand off to on the real root. The shipped image names /bin/peinit2. If the command line does not name one, prelude searches /bin/peinit2, /sbin/init, /bin/init, and finally /bin/sh. These are target-root runtime paths: the base-topology hook creates their StrataFS views before prelude hands off. Prelude's own initramfs rescue shell remains /usr/bin/sh, because that environment has no /bin view.
  3. Run the hooks. prelude creates an empty /mnt/rootfs directory — the mount point the real root will appear at — and runs the boot hooks in order. The hooks do the deployment-specific work: load drivers, unlock encryption, assemble volumes, and mount the real root onto /mnt/rootfs. The packaged mount-rootfs-stratafs-base.sh hook (from the fsbase package) then mounts the conventional runtime views inside that root. prelude does none of this itself; it runs the hook list. The order was decided when the initramfs was built (see below, and Boot hooks).
  4. Verify the real root. After the hooks have run, prelude checks that /mnt/rootfs actually has a filesystem mounted on it. If no hook mounted a root, prelude fails the boot rather than handing off to nothing. This is the one outcome prelude insists on: some hook must have produced a mounted root.
  5. Hand off. prelude carries the kernel virtual filesystems into the new root, frees the now-finished in-memory root to reclaim its space, switches / to the real root, and executes the target init. From that exec onward, the initramfs is gone and the real system is running.

prelude runs this sequence exactly once. It does not loop, supervise, or stay resident — the moment the real init is exec'd, prelude has ceased to exist; the exec replaces it. Its entire lifetime is the few seconds of the initramfs stage.

When the initramfs stage fails #

prelude does not limp. If any step fails — a hook exits with an error, no hook mounts the root, the target init cannot be found — prelude stops and halts the machine. It does not fall through to a half-configured system, and it does not try to work around a failed hook.

This is the right behaviour for the stage. The initramfs's only job is to deliver a correctly-mounted real root to a real init. A failure there means the system cannot be brought up safely; continuing would only produce a system in an undefined state. Halting makes the failure unambiguous — the console shows which step failed — and leaves the operator with a clean situation to diagnose rather than a subtly-broken running system. The one way to intervene rather than halt is the debug knobs below.

Debugging the initramfs stage #

Two kernel command-line knobs (familiar from dracut) turn the fixed sequence into something an operator can step through. prelude reads them from /proc/cmdline, the same place it reads init= — as PID 1 it has no argv, so the command line is its only input. The interactive shell they open is dash, run as sh -i on the console.

  • rd.shell — drop to a shell on failure instead of halting. If any boot step fails while rd.shell is set, prelude opens the shell so you can inspect the half-built initramfs: see what the hooks mounted, read the log, try a mount by hand. The failure still ends the boot — when you exit the shell, prelude halts as it would have anyway. Without rd.shell, a failure halts immediately.

  • rd.break — stop before any hook runs. Bare rd.break pauses just before the first hook and opens the shell; exit the shell and the boot continues normally. This is the point where the kernel virtual filesystems are up but nothing deployment-specific has happened yet.

  • rd.break=<hook> — stop just before a named hook. The value is matched against a hook's file name — or its full path — as it appears in the resolved boot sequence, so rd.break=mount-root.sh breaks immediately before that hook and lets everything ordered before it run first. The value is comma-separated and the flag may be repeated — rd.break=modules.sh,mount-root.sh — to break before several hooks.

The rd.break shells are true breakpoints, not failures: prelude forks them, so it stays PID 1 and the boot resumes exactly where it paused when the shell exits. Setting any rd.break also implies rd.shell — so if the boot goes on to fail, you get the failure shell too.

Keeping the initramfs current #

Because the directory is the source and the initramfs image is a build product, the two have to be kept in step. Whenever /boot/initramfs/ changes — a feature package is installed or removed, the kernel is updated, an administrator edits a hook — the image has to be rebuilt from the directory.

The tool that does this is mkirf — see mkirf for the full command reference. It reads /boot/initramfs/, resolves the order the hooks must run in, checks the directory is internally consistent, and writes the compressed initramfs image. Three properties of it matter to an operator:

  • It validates. mkirf will not produce an image from a directory that cannot boot. A hook set with an impossible ordering, a hook that depends on a capability nothing provides, a missing init — each of these stops the build with a clear error. A misconfigured boot is caught when the image is built, on a running system where the message is easy to read, rather than as a mystery failure at the next boot. Boot hooks covers exactly what is checked.
  • It is deterministic. The same directory always compiles to the same image, byte for byte — identities, timestamps, and ordering are all normalised. This is what makes "did anything actually change?" a meaningful question, and it underpins later work such as signed boot artifacts.
  • It keeps the directory pristine. mkirf reads the directory and writes the image elsewhere; it never writes back into /boot/initramfs/. The resolved hook order is recorded inside the image, not in the source. The directory you inspect is always exactly what packages and you have put there.

mkirf can run once and exit, or run in a watch mode where it stays resident and recompiles automatically whenever the directory changes. Watch mode is what lets /boot/initramfs/ behave like an ordinary part of the filesystem — edit a hook and the initramfs is current again — rather than a build artifact an administrator has to remember to regenerate.

When more than one kernel is installed, there is one initramfs per kernel: the drivers a kernel needs are specific to its version, so each kernel gets an image built against its own modules.

The handoff to the real root #

The final act of the initramfs stage is the handoff: prelude replaces the in-memory root with the real root and gives the machine to the real init.

Concretely, the real root has been mounted at /mnt/rootfs by a hook; prelude carries the kernel virtual filesystems across into it, switches / from the initramfs to /mnt/rootfs, and execs the target init. Switching the root is the operation usually called switch_root — after it, / is the real filesystem and the in-memory initramfs is gone, its RAM reclaimed.

The init prelude execs is the one identified back in step 2 — the init= value from the kernel command line, or the fallback search. On a complete Peios system that init is peinit, which takes over as PID 1 of the real root and brings up the rest of userspace. During earlier development, before peinit exists, the target is a simpler stand-in; the contract is the same either way — prelude delivers a mounted real root and a console, and execs whatever the real init is.

The handoff is a one-way door. Once / is the real root and the real init is running, the initramfs is not coming back; the in-memory stage exists only up to this moment. The SYSTEM token, though, survives the handoff — the real init inherits it across the exec, exactly as every program inherits its primary token across exec. That is the thread tying this page to Bootstrap tokens: the SYSTEM token the kernel attached to prelude is the same token peinit starts life holding.

What prelude does not do #

A few clarifications:

  • prelude does not mount the real root itself. A hook does. prelude provides the /mnt/rootfs mount point and verifies the result, but the mount is always a hook's job — that is what keeps prelude identical across deployments. See Boot hooks.
  • prelude does not decide hook order. The order is resolved when the initramfs is built, by mkirf, and recorded in the image. prelude reads the resolved list and runs it; it has no ordering logic of its own.
  • prelude does not supervise anything. It runs the hook sequence once and execs the real init. It has no steady state — contrast peinit, which runs as the system's lifecycle manager for the whole of its uptime.
  • prelude is not the real init. It is the initramfs init. peinit is the real-root init. They are separate binaries with separate jobs; prelude's last act is to exec the real init, not to become it.
  • prelude does not persist anything. Nothing it does is written to disk. The initramfs is in RAM, used once, and discarded. Persistent state begins on the real root, with peinit.

Where to go next #

For the deployment-specific scripts prelude runs, read Boot hooks.

For the init system prelude hands the machine to, read peinit at PID 1.

For the tool that compiles /boot/initramfs/ into the image, read mkirf.

Boot hooks

Peios / Peios Security Fundamentals / Boot and trust establishment

A boot hook is a shell script that runs inside the initramfs, before the real root is mounted — during the initramfs stage. Hooks are how Peios keeps prelude deployment-agnostic: every part of early boot that depends on this particular machine is a hook, and prelude runs the hooks without knowing what any of them do.

The work hooks do is the work of getting to the real root: loading the storage driver that makes the system's disk visible; unlocking an encrypted container; assembling an LVM volume group or a RAID array; and, finally, mounting the real root filesystem onto /mnt/rootfs. None of that is the same on two arbitrary machines, so none of it is built into prelude — it is all hooks. prelude supplies the fixed skeleton of the boot; hooks supply everything that varies.

Where hooks live #

Hooks live in two directories, which mirror the layout's vendor/operator split everywhere else:

DirectoryFor
/usr/libexec/prelude/hooks.d/hooks shipped by packages
/lcl/libexec/prelude/hooks.d/hooks an operator put there by hand

Every regular file directly in either directory is a hook, and prelude runs all of them. Neither is searched recursively — a subdirectory is not a hook — and the file extension does not matter. A hook is recognised by being a file in one of those directories, and it is run according to its #! shebang line, the same way any script is run.

A file name identifies a hook. The same name in both directories is one hook with two candidate bodies, not two hooks: the operator's copy wins and the packaged one is skipped, so a shipped hook can be replaced without deleting a package's payload. The build says which file lost, because a hook silently replaced by another is exactly the kind of thing that should not be quiet.

An older hooks/ directory at the initramfs root is still read, and every hook found there produces a build warning naming where it should move. That is deliberate: a hook in a directory nobody scans is not an error — it simply is not there, and the boot fails much later with nothing mounted — so the move cannot be a flag day. The warning going quiet is what says the migration is done. Hooks are #!/usr/bin/sh scripts, and the initramfs's /usr/bin/sh is provided by dash. The full package-storage path is required because hooks run before a /bin runtime view exists.

How hooks get there #

There are two ways a hook reaches the directory, and they are identical as far as prelude is concerned:

  • From a package. Most hooks arrive as part of a feature peipkg. Installing peios-luks (disk encryption) drops a hook that unlocks encrypted volumes; installing a filesystem feature drops a hook that mounts that kind of root. The package's payload simply includes a file under /usr/libexec/prelude/hooks.d/, and removing the package removes the hook. This is the feature-as-a-package model applied to boot: there is no edition of Peios that "has LUKS" and another that does not — there is a peios-luks package, and a machine either has it installed or it does not.
  • By hand. An administrator can write a hook and place it in /lcl/libexec/prelude/hooks.d/ directly. A site with an unusual storage arrangement, or a one-off need, does not have to build a package — a script in the directory is a hook.

Either way, the next time the initramfs is built the new hook is picked up. (See The initramfs stage for the build.)

The ordering problem #

Hooks have an order, and the order matters. A hook that mounts the root cannot run before the hook that loads the disk's storage driver; a hook that unlocks an encrypted volume cannot run before that volume's driver is loaded. Run them in the wrong order and the boot fails.

The obvious approach — number the files, 10-modules, 20-crypto, 30-mount, and run them in numeric order — is the approach Peios deliberately does not take. Numeric prefixes work right up until two packages, written by people who never spoke to each other, both pick 20. Then every package author has to know the whole number line, and the "order" is a fiction held together by convention. This is the historical sysvinit problem, and it does not scale.

Instead, a hook declares what it needs and what it offers, in terms of named capabilities, and the order is computed from those declarations. The hook that mounts the root says "I require crypto-unlocked"; the hook that unlocks encryption says "I provide crypto-unlocked"; the order follows. No hook needs to know any other hook's name or position — only the capabilities. Packages that never met each other compose correctly because they agree on capability names, not on numbers.

The metadata block #

A hook declares its capabilities in a metadata block at the top of the script: a fenced comment block, every line of it a comment, so the block is invisible to the shell when the script actually runs.

#!/usr/bin/sh
# /// hook
# contributes = ["rootfs-ready"]
# ///

# ... the hook's actual work follows ...
cryptsetup open /dev/sda2 root

The rules of the block are small and exact:

  • It opens with a line that is exactly # /// hook and closes with a line that is exactly # ///.
  • Every line in between is a comment line — # followed by content, or a bare #. This is what keeps the block inert: the shell sees only comments.
  • The content of those lines, with the # stripped, is up to four keys:
    • provides — capabilities this hook satisfies on its own. Several hooks may provide the same capability, and any one of them suffices; they are alternatives.
    • contributes — capabilities this hook is one part of. Every contributor must complete before the capability counts as satisfied.
    • requires — capabilities that must already be satisfied before this hook runs. If nothing supplies one, the build fails.
    • after — capabilities to be ordered after if anything supplies them. If nothing does, the hook simply runs; this is not an error.
  • Each key's value is a list of capability names in double quotes — provides = ["a", "b"]. An empty list is allowed, and a trailing comma is allowed.

Why there are four keys and not two #

The four are two ways to supply a capability crossed with two ways to consume one, and each pair exists to say something the other cannot.

contributes is how a hook runs before something. A hook is otherwise ordered only by what it consumes — so "run before the root is mounted" would require the root-mounting hooks to name your hook, which they cannot, because they were packaged before it existed. Declaring yourself part of a capability inverts the relationship: everything that consumes it now waits for you. Without this, the only way to be early is to win the file-name tie-break, which is the numeric-prefix problem wearing a disguise.

after is how a shared vocabulary survives a small image. A requires on a capability nothing supplies is a build error — deliberately, since it catches an initramfs that cannot possibly boot. But that makes any standard capability name dangerous: a hook mentioning network-up would break every image that has no networking. after expresses "if this happens at all, it happens before me", which is what most ordering against an optional milestone actually means.

provides and contributes differ in what "satisfied" means. With alternatives, one supplier doing the job is enough — which is exactly the live-boot/disk-boot pair below, where one mounts the root and the other stands aside. With contributors, the capability is not satisfied until every one of them has completed — three hooks each unlocking a layer of an encrypted stack, say. A capability must be one or the other; declaring it both ways is a build error, because there would be no answer to whether it is satisfied yet.

The format is a small, deliberate subset of TOML: enough to declare two lists of names, and no more. It is checked strictly when the initramfs is built — an unknown key, a list that is not well-formed, a block that is opened and never closed, two blocks in one file — each of these is a build error, not a quiet misread. A typo in a hook's metadata is caught at build time, with a message naming the hook and the line.

The metadata lives inside the hook script, not in a separate file, so a hook is a single self-contained thing: copy the script and its ordering travels with it.

The capability vocabulary #

Capabilities are just names, and a hook can in principle name its own. But the milestones every initramfs passes through are a small fixed vocabulary, so that hooks from unrelated packages line up against the same reference points:

CapabilityMeaning
initramfs-readyThe initramfs itself is usable — its filesystem topology assembled, its console set up.
rootfs-ready/mnt/rootfs is mounted and ready for read/write manipulation.
rootfs-strata-readyThe full StrataFS topology exists inside /mnt/rootfs.

The vocabulary is deliberately short. Names for things Peios does not yet do — device settling, volume assembly — are not reserved in advance: a capability that nothing supplies and nothing consumes is a name with no meaning behind it, and inventing one early only fixes a shape before we know it. Driver loading, which Peios does do, has no capability of its own for the same reason: the coldplug hook simply contributes to rootfs-ready, which is what it means for the root-mount hooks, and nothing has yet needed to order against it more precisely than that.

Once every hook has finished, prelude chroots into /mnt/rootfs. There is no "last point at which a hook can run" capability, because that is already every hook's guarantee.

initramfs-ready is the one capability with a rule #

A hook that must run before all the others cannot say so by ordinary means — it would need every other hook to name it, including hooks written by people who have never heard of it, whose forgetting would not fail the build but would quietly run their hook in an initramfs with no /bin.

So initramfs-ready carries one extra rule:

Every hook that does not supply initramfs-ready is implicitly ordered after it.

The exemption is derived, not declared — you are exempt exactly when you are part of the capability. There is no cycle to construct and nothing for a hook author to remember. A hook assembling the initramfs's own topology simply declares contributes = ["initramfs-ready"] and lands before everything.

Two things fall out of the existing rules rather than needing special cases. An initramfs with no contributors leaves the capability unsupplied, and an after on an unsupplied capability is vacuous — so a minimal initramfs just runs, with nothing to configure. And a hook carrying no metadata block does not gain the edge, because it already runs after every declaring hook; adding it would have pulled the escape hatch into the DAG.

mkirf materialises these edges into the sequence as ordinary after entries, rather than leaving prelude to know the rule. That keeps one implementation instead of two that have to agree, and makes the sequence file explain itself — in a rescue shell you can read why a hook ran where it did, instead of needing a rule that appears nowhere in the image.

More than one hook may supply the same capability #

Nothing limits a capability to a single supplier, and rootfs-ready is where that matters: getting a root filesystem ready is several jobs. A hook that unlocks an encrypted container, a hook that assembles a volume group, and a hook that performs the mount each contributes = ["rootfs-ready"], and the capability is not achieved until all of them have finished. None of them needs to know the others exist.

A capability does not have to be a hard, machine-specific thing. What matters for ordering is the declaration, not how much work the hook does to honour it. An encryption hook on a machine with no encrypted volumes has nothing to unlock and declines (exit 69) — which, for a capability it contributes to, completes its part: "nothing needed doing here" is a way of being done. The capability is still achieved, and everything waiting on it proceeds.

How the order is resolved #

When the initramfs is built, mkirf reads every hook's metadata and computes the running order:

  • A hook that consumes a capability (by requires or by after) runs after every hook that supplies it (by provides or by contributes). That single rule is the whole of the ordering.
  • Hooks with no ordering relationship between them run in a stable, predictable order — by file name — so the resolved sequence is the same every time the initramfs is built.

Note that ordering does not distinguish alternatives from contributors, nor hard requirements from soft ones: all four keys produce the same "supplier first" edge. What they change is validity — whether an unsupplied capability is an error — and what a capability being "satisfied" will mean to prelude at boot.

Three situations are build errors. They stop the build, and no initramfs is produced:

  • A cycle — hook A consumes something B supplies, and B consumes something A supplies. There is no order that satisfies both; the build reports the hooks caught in the cycle.
  • An unsatisfied requirement — a hook requires a capability that nothing installed supplies. Rather than build an initramfs that is guaranteed to fail at boot, mkirf reports the missing capability. An after on an unsupplied capability is explicitly fine.
  • A capability that is both provided and contributed to — one hook calls it an alternative, another calls itself a part of it. The two answers to "is it satisfied?" contradict each other, so mkirf names both sides and stops rather than picking one.

This is the payoff of declared capabilities: an impossible or incomplete hook set is a build error on a running system, with a readable message, never a boot that hangs with no explanation.

The resolved order is recorded inside the initramfs image, at /system/prelude/hooks.seq.<n>, and prelude reads it at boot. An operator does not write or edit those files — they are generated. The hook directories are the source of truth; the order is derived from what they contain.

The <n> is the format version, and mkirf writes every version it can express faithfully, prelude reading the newest it understands. That is not ceremony: mkirf ships in peiosutils and prelude in its own package, so an image can pair a newer writer with an older reader, and separate files are what let one image satisfy both. A sequence newer than prelude understands is a warning when a usable one sits beside it, and a boot failure when it is the only one present.

A missing sequence is always an error, never "no hooks to run" — mkirf writes one for every image, including an empty one for an image with no hooks at all. Were an absent file read as "nothing to do", a manifest that failed to generate would become a boot that silently ran no hooks, mounted no root, and reported the failure a long way from its cause.

Hooks with no metadata #

A hook is allowed to carry no metadata block at all. It is still a valid hook — it simply has no declared capabilities, and therefore no ordering constraints. Such a hook runs after all the hooks that do have constraints, in file-name order.

The build emits a warning for a hook with no block — because a hook that merely forgot its metadata looks identical to one that genuinely has none, and the warning makes the difference visible. A hook that is deliberately unconstrained can carry an empty block — a # /// hook line immediately followed by # /// — to say so on purpose, which suppresses the warning.

How prelude runs a hook #

At boot, prelude runs the hooks one at a time, each to completion before the next begins, scheduling from their declarations — repeated passes over the sequence, running whatever is ready, retrying anything that deferred once something else has progressed. Each hook runs as a separate process with a minimal environment — PATH=/usr/bin and TERM=linux — so the initramfs's shell and utilities (dash, peiosutils, and any tools a feature package added) are found without depending on a not-yet-mounted root-level view.

A hook reports what happened through its exit code, and there are four things it can say:

ExitMeaningWhat prelude does
0Satisfied — I did my part.Its provides are achieved; its contributes are one step closer to complete.
69Declined — not applicable on this machine.It is not counted toward the capabilities it declared.
75Deferred — I cannot run yet, and I have changed nothing.Re-queued and tried again once something else has made progress.
anything elseFailed.The boot stops and the machine halts.

The two middle codes are borrowed from sysexits.h (EX_UNAVAILABLE, EX_TEMPFAIL) rather than invented, for a practical reason: 1 and 2 are what any failing command returns and 126, 127 and 128+n are the shell's own, so a small dedicated range is the only place a deliberate signal cannot be mistaken for an accident. A hook killed by a signal is always a failure, whatever code it might have produced.

Declining is not the same as succeeding #

A hook that is not the right one for this machine declines (69) rather than exiting 0. The difference matters because provides means alternatives: a capability with several providers is achieved as soon as one of them is satisfied, and a provider that exits 0 is claiming to be that one.

This is what the live-boot/disk-boot pair does. Whichever hook root= does not select declines, and the other mounts the root. If every provider declines, the capability is never achieved and prelude stops with a message naming it — where previously a machine no hook would boot produced only the generic "nothing mounted the root" much later.

For contributes the sense is reversed: since every contributor must complete, a contributor that declines has completed — "nothing needed doing here" is a way of being done.

Deferring is a promise that nothing happened #

Deferred (75) means the hook's inputs are not ready yet. prelude runs hooks in repeated passes, retrying deferred ones once something else has progressed, and stops with a diagnosis if a whole pass completes nothing while work remains.

The contract that makes this safe is that a deferring hook has done nothing. Re-running it is free in a way that re-running a failed hook could never be — a failure may have left a half-assembled array, an opened device, or a burnt unlock attempt behind it. So:

  • Defer before you act, never after. Check whether you can do the job; if not, exit 75 immediately. Do not defer partway through.
  • Failure is still failure. If the work was attempted and went wrong, exit non-zero. Deferring a genuine error turns one clear failure into a stall reported as "nothing left to wait for".

This is what lets layered arrangements compose without absolute ordering: three hooks each contributing to an encrypted stack can be written without knowing which layer comes first, because the ones whose devices do not exist yet simply defer and are retried.

A few more consequences for anyone writing a hook:

  • Check, and exit non-zero on failure. A hook that mounts the root must exit non-zero if the mount failed. A hook that fails silently turns into prelude's generic "nothing mounted the root" failure later — which is harder to diagnose than the hook reporting its own error at the point it happened.
  • Hooks run as SYSTEM. Everything in the initramfs runs on the SYSTEM token (see Bootstrap tokens), so a hook has full authority. There is no identity model to work within inside the initramfs — that begins on the real root.

Writing a hook #

A complete, minimal hook — one that mounts an ext4 root from a known partition:

#!/usr/bin/sh
# /// hook
# contributes = ["rootfs-ready"]
# ///

# The device may not exist yet — another hook may still have to unlock or
# assemble it. Defer rather than fail: exit 75 promises we changed nothing,
# so prelude can retry us once something else has made progress.
[ -b /dev/mapper/root ] || exit 75

mount -t ext4 /dev/mapper/root /mnt/rootfs || exit 1

What this declares: it is one part of rootfs-ready, so anything waiting on a usable root filesystem is ordered after it. It does not name the hooks it depends on, because it does not know them.

A hook that unlocks the container it mounts, to pair with it:

#!/usr/bin/sh
# /// hook
# contributes = ["rootfs-ready"]
# ///

# Nothing encrypted on this machine — decline, which completes our part of
# the conjunction without claiming to have unlocked anything.
[ -e /dev/sda2 ] || exit 69

cryptsetup open /dev/sda2 root || exit 1

Installed together, these two compose without either one naming the other, and without an ordering declaration between them at all. If the mount hook runs first it finds no device and defers; the unlock hook then runs; the mount hook is retried and succeeds. Add a volume-assembly hook later — also just contributes = ["rootfs-ready"] — and it slots in the same way, because each hook knows only whether it can act yet.

That is the model: hooks are composed by capability, ordering is declared only where it is genuinely known, and the rest is settled at boot by hooks that can say "not yet".

What hooks are not #

A few clarifications:

  • Hooks do not run on the real root. They run inside the initramfs, before the handoff. Work that belongs to the running system — starting services, applying mount and access policy — is peinit's job, not a hook's.
  • A boot hook is not a service. "Hook" here means specifically an initramfs hook. The initramfs stage ends when prelude execs the real init; everything after that — services, supervision, restart policy — is peinit's domain and works nothing like a hook.
  • The generated order file is not edited by hand. It is the build's record of the resolved order. The hooks/ directory is the source; the order is computed from it, every time the image is built.
  • File names are not the ordering mechanism. A file name identifies a hook, and is the tie-break between hooks that have no capability relationship. Renaming a hook does not change where it runs relative to a hook it shares a capability with — the provides/requires declarations do that.

Where to go next #

For the stage that runs the hooks, read The initramfs stage.

For the build that validates hooks and resolves their order, read mkirf.

For what takes over once a hook has mounted the real root, read peinit at PID 1.

peinit at PID 1

Peios / Peios Security Fundamentals / Boot and trust establishment

peinit is the init system of a running Peios system — the process that takes over once the initramfs stage has mounted the real root. It runs at PID 1, signed at TCB level, holding the SYSTEM token. From there, peinit does the rest of the work needed to bring the system from a freshly-mounted real root to "fully-running with services and users".

The specific responsibilities are:

  • Run the compiled-in Phase 1 bootstrap: probe that the root is writable, mount the virtual filesystems, restore the random seed, ensure the machine-id, set the clock from the RTC, and start registryd.
  • Read the service catalog from the registry (Machine\System\Services\), validate the dependency graph, and start services in dependency order — authd among them.
  • Mint SYSTEM tokens for platform services itself; request every other identity from authd.
  • Continue running as the system's lifecycle manager — handling service crashes, system shutdown, eventual reboot.

This page covers each of these responsibilities, why peinit specifically is the right thing to be PID 1, and the patterns peinit uses for the work.

Note

This page is about peinit's place in the boot and trust chain — why it is PID 1, the identity it holds, and how it hands off to the rest of userspace. The full service-manager model — the service definition schema, the state machine, dependencies, supervision and restart policy, timers, jobs and operations, and the control interface — has its own topic. Start at peinit.

Why peinit at PID 1 #

PID 1 has special properties in Linux: it cannot be killed by ordinary signals, it inherits orphaned processes, it's the ancestor of everything else. Whichever process is PID 1 is operationally critical.

Peios chooses peinit specifically because:

  • It's signed at TCB level. When peinit is exec'd — by prelude, at the switch to the real root — the signature verification at exec sets pip_type = Protected, pip_trust = 8192. peinit is now PIP-protected at the highest level. No other process can signal it, debug it, or interfere with it.
  • It runs on the SYSTEM token. Inherited from init. peinit has every privilege; it can do anything that requires authority.
  • It's purpose-built for the bootstrap. Other init-style processes do general-purpose process management; peinit specifically knows about Peios's identity model, its registry-defined service catalog, and the service-launching pattern Peios uses.

The combination of "PIP-protected" and "all privileges" and "purpose-built" makes peinit the right thing to be PID 1. A general-purpose init that wasn't signed at TCB level would be a weak link — every TCB-level process (authd, loregd, eventd) is unreachable by their lifecycle manager, which means lifecycle management could only be done by another TCB-level process. That role is peinit.

The TCB lifecycle-manager pattern #

A consequence of PIP: only a process that PIP-dominates the TCB daemons can signal them. Since the TCB daemons are at pip_type = Protected, pip_trust = 8192, only another process at the same level (or higher, but there is no higher in v0.20) can dominate.

peinit is the only such process at the right time. peinit is signed at TCB level, so it dominates. authd, loregd, eventd, lpsd are also at TCB level, so they dominate each other — but they don't manage each other's lifecycles by convention. peinit is the conventional lifecycle manager.

This is documented in PIP in practice. The TCB-lifecycle-management pattern is a consequence of PIP; peinit happens to be the binary that fills the role.

What peinit does at startup #

peinit's startup is two phases. The authoritative, step-by-step account lives in the peinit topic at Boot and boot modes; what follows is the trust-chain summary.

Phase 1 — compiled-in bootstrap #

Phase 1 runs before any configuration is available, in a fixed order compiled into the binary:

  1. Assert PID 1. peinit refuses to run as anything else.
  2. Probe that the real root is writable, and ensure the virtual filesystems (/proc, /sys, /dev, …) are mounted.
  3. Restore the random seed, ensure the machine-id, set the clock from the RTC.
  4. Start registryd and probe the registry schema. The registry is the source of all subsequent configuration, so its daemon comes up before anything else.
  5. Provision boot paths and infrastructure — the control socket, the job filesystem, loopback networking.

Phase 2 — the service graph #

With the registry available, peinit reads the service catalog from Machine\System\Services\, validates the dependency graph, and starts services in dependency order. authd is an ordinary Critical service in this graph — started after the infrastructure it depends on (eudev, lpsd), gated by the same readiness mechanics as any other service, not by a special wait-for-authd phase.

For each service, the pattern is fork-install-exec:

  1. peinit obtains the service's token — minted by peinit itself for SYSTEM/platform services, requested from authd for every other identity (see Identity and privileges).
  2. peinit forks; the child inherits a SYSTEM-derived token.
  3. peinit installs the service-specific token on the child via KACS_IOC_INSTALL.
  4. The child execs the service binary. The kernel verifies the signature and sets the PIP fields.

Note

Four jobs are sometimes attributed to peinit that it does not do. It does not populate the machine-wide TLP cache from the registry, and it does not apply per-service mitigation flags via kacs_set_psb between fork and exec. Mount-policy application (kacs_set_mount_policy) is likewise not a peinit responsibility, and neither is boot-time CAAP distribution — that one belongs to authd, and is currently deferred.

Steady state #

After the graph is started, peinit transitions to its steady-state role: the lifecycle manager. It watches for service crashes (a child process dies; peinit receives SIGCHLD), restarts them per policy, handles shutdown signals (an administrator calling shutdown), and otherwise runs as a long-lived daemon.

The system is now "up" — services are running, authd is creating tokens for users who sign in, FACS and KACS are enforcing access control, audit events are flowing through KMES.

The fork-install-exec pattern #

The pattern is the most important thing peinit does, and it's worth pinning. The order of operations is deliberate:

  1. fork. Creates the child process. The child inherits the parent's token, file descriptors, and PSB. At this point the child is essentially a clone of peinit.
  2. Install token. The child gets its service-specific token via KACS_IOC_INSTALL. This is the moment its identity is set to be the right thing for the service.
  3. exec. The new program runs. The kernel verifies the binary's signature and sets the PIP fields.

The reason for this order: the service's token needs to be in place before the service's startup code runs — the service should be able to assume from its first instruction that it's running as the right identity. exec comes last because that is the moment the new program takes over; by then the identity has been decided.

The pattern is what every service is launched through. Variations are minor — the specific token differs per service — but the structural sequence is the same. (When per-service mitigations land in peinit — see the note above — they will slot in between install and exec, since mitigation flags must be on the PSB before the new binary runs.)

What peinit does not do #

A few clarifications:

  • peinit mints only SYSTEM tokens. Minting the SYSTEM token for platform services via kacs_create_token is peinit's normal, specified path — no authd interaction is needed for those. Every other identity is requested from authd, the source of truth for who-gets-what privileges.
  • peinit is not the only TCB process. authd, loregd, eventd, lpsd are also at TCB level. They have their own jobs.
  • peinit does not enforce policy. DACLs, conditional ACEs, mount policies — the kernel runs AccessCheck and makes every access decision; peinit just launches processes with the right identities.
  • peinit does not handle user sessions directly. Users sign in via authd; authd produces tokens; peinit launches the session's first process. The user's session is managed by authd, not peinit.
  • peinit's service management is peinit, not a separate daemon. Start order, dependencies, restart policy, the service state machine, and the control interface are peinit's own responsibility — there is no service-manager process living alongside it. That model is substantial enough to have its own topic; this page does not cover it. See peinit.

When peinit fails #

If peinit crashes or exits, the kernel typically panics — losing PID 1 is a system-fatal condition in Linux. Peios's peinit is built to be very stable for this reason; its work is narrow and bounded.

If peinit cannot complete a startup step (a service won't launch, authd won't start), the deployment-specific behaviour depends on the configured policy. Typical responses:

  • For a critical failure (authd won't start), boot fails. The system enters a recovery state — typically a console login as SYSTEM, with limited services running, enough for an administrator to diagnose and fix.
  • For a non-critical failure (a single service won't start), peinit logs the failure, leaves the service unstarted, and continues with the rest of boot.

The failure-mode behaviour is configurable but conservative by default. peinit doesn't try to "work around" failures by relaxing security; if the configured set of mitigations can't be applied, the service doesn't start rather than starting unhardened.

authd handoff

Peios / Peios Security Fundamentals / Boot and trust establishment

The single most consequential transition during boot is the moment authd comes online. Before authd, every process is running on the SYSTEM token (inherited from init through peinit); identity is uniform and authority-maximal. After authd, processes start running as their actual principals — services as their service identities, users as themselves after they sign in. authd is the bridge between "kernel-direct bootstrap identity" and "real identities derived from the directory".

This page covers what authd does at startup, how the handoff from SYSTEM-everywhere to real-identities-everywhere happens, and what authd is responsible for in steady state.

What authd is #

authd is the authentication daemon. Its core responsibilities:

  • Authenticate principals. When a user signs in, authd verifies their credentials against the directory (locally for standalone systems, against the domain's directory for domain-joined ones).
  • Mint tokens. Once a principal is authenticated, authd produces a token reflecting their identity, group memberships, privileges, integrity level, claims. The token is what the principal's processes run on.
  • Manage logon sessions. authd creates a session per authentication event, attaches the minted tokens to it, tracks lifecycle via the logon-session-destroyed event.
  • Distribute CAAP. authd reads central access policies from its source (registry or the domain's directory) and pushes them into the kernel's policy cache via kacs_set_caap.
  • Resolve identity-related queries. authd is the answer to "what privileges does this user have?", "what claims should be on this token?", "is this user a member of this group?". These queries go to authd, which consults the directory.

authd is signed at TCB level, runs at pip_type = Protected, pip_trust = 8192, and is one of the PIP-protected processes. It is launched by peinit early in boot and runs continuously until shutdown.

authd's startup sequence #

When peinit forks-and-execs authd, the new process goes through several initialisation steps:

1. Verify own state #

Just like peinit at startup, authd verifies it is running in the expected state — PIP-protected, holding the expected token, with the expected mitigations applied. Anything else indicates the boot is broken; authd refuses to proceed.

2. Connect to the directory #

authd opens its connection to the directory. The connection mechanism depends on deployment:

  • Standalone systems use loregd (the registry daemon). authd communicates with loregd over a Unix socket and reads policy from registry keys.
  • Domain-joined systems use Samba 4. authd talks to a Samba 4 instance (locally or on another machine in the domain) for domain identity and policy resolution.

The directory is the source of truth for everything authd needs to know — principals, groups, privileges, CAAP, claims. authd's connection to the directory is established early because everything else depends on it.

If the directory connection fails (loregd not running, AD unreachable), authd cannot mint real tokens. The system continues on bootstrap tokens (everything as SYSTEM) until the directory becomes available. authd retries with exponential backoff; in the meantime peinit may proceed with whatever services don't need authd.

3. Populate the CAAP cache #

For each central access policy in the directory, authd parses it and pushes it via kacs_set_caap. The kernel's policy cache fills up with the deployment's CAAP.

This step matters for the recovery policy: until this step completes, every CAAP-referencing object resolves to the recovery policy (administrators and SYSTEM only). After this step, the object's actual policy applies.

CAAP population happens before authd advertises itself as ready. This is what makes the boot ordering safe — services that depend on CAAP being live can wait for authd to signal readiness; by then, CAAP is in place.

4. Begin serving authentication requests #

authd opens its socket — typically a Unix domain socket — and starts accepting connections from clients. The clients include:

  • peinit, asking for tokens to assign to new services.
  • Login frontends (console, SSH, terminal services) authenticating users.
  • Services that handle their own re-authentication (rare, but possible).

Each authentication request follows a similar pattern:

  1. Client connects to authd's socket and presents credentials.
  2. authd verifies the credentials against the directory.
  3. authd resolves the principal's full identity (groups, privileges, claims) from the directory.
  4. authd creates a logon session via kacs_create_session.
  5. authd calls kacs_create_token to mint the token, passing the wire-format specification with everything resolved.
  6. authd returns the token (or token fd) to the client.

The client now has a token reflecting the authenticated principal. The client can install the token on a child process (if it's peinit launching a service) or on itself (if it's a login frontend completing the user's sign-in).

5. Signal readiness #

authd writes a readiness signal to peinit — typically by closing a pre-existing pipe fd, by writing a sentinel value to a known location, or by reaching a state peinit can probe for. peinit, which has been blocked waiting for this, proceeds to launch the rest of the services.

The handoff in one paragraph #

The transition: before authd is ready, every userspace process is running on the SYSTEM token (or a SYSTEM-derived FilterToken variant). After authd is ready, peinit's subsequent service-launch operations assign each new service a token authd produces — derived from the directory, with the appropriate identity for the service. The system goes from "everything SYSTEM" to "services as themselves" gradually, over the course of however long peinit takes to launch the rest of userspace.

There is no single moment where the system "becomes secure" — it's a gradient. Even before authd, the kernel's access control is fully operational; SYSTEM just happens to be everywhere. As real tokens replace SYSTEM in process after process, the system's authorisation surface narrows. By the time login frontends are accepting users, every service is at its right identity, and users sign in to fresh sessions of their own.

authd in steady state #

After startup, authd's day-to-day work is:

  • Authenticate users when they sign in. Each login produces a session and a token (or a pair of tokens for UAC-style elevation).
  • Mint tokens for new service starts. When peinit needs a token for a service it's about to launch, authd produces one.
  • Track session lifecycle. authd subscribes to the kernel's logon-session-destroyed event and uses it to release session-scoped state (Kerberos tickets, cached directory data).
  • Distribute CAAP updates. When a policy in the directory changes, authd re-pushes it via kacs_set_caap. The kernel's cache is kept in sync with the directory.
  • Handle session revocation. When a user must be forcibly logged out, authd walks /proc/*/token to find tokens with the offending auth_id, identifies the holding processes, and signals them to exit. This is the userspace-coordinated revocation pattern documented in Session lifecycle.

authd is a long-running daemon. It is not periodically restarted. Its uptime equals the system's uptime (modulo any administrative restarts in response to configuration changes).

What authd does not do #

A few clarifications:

  • authd does not make access decisions. AccessCheck is in the kernel. authd produces inputs to it (tokens) but does not decide whether any specific access is granted.
  • authd does not enforce CAAP. AccessCheck enforces CAAP; authd just distributes the policies into the kernel cache.
  • authd does not authenticate every operation. Authentication is a sign-in event. Operations done after sign-in run against the token, not against authd. authd does not see most of the per-operation traffic.
  • authd does not store passwords. Credential verification goes against the directory; authd is an intermediary. The directory (loregd's registry, or the domain's directory) is where the credentials live.
  • authd does not implement the directory. authd consumes a directory; it doesn't be one. For standalone systems, loregd is the directory; for domain-joined, the domain's directory via Samba 4. authd talks to whichever is appropriate.

The cleanest mental model: authd is the converter between "directory state" and "kernel-resolvable identity". Everything else — access decisions, audit, lifecycle — happens around authd, not through it.

When authd fails #

authd is signed at TCB level, runs hardened, and is operationally critical. If authd crashes:

  • New authentication requests fail. Users cannot sign in, peinit cannot get new tokens for new services.
  • Existing tokens continue to be valid. Processes running on tokens authd already minted are unaffected.
  • The kernel's policy cache continues to hold whatever CAAP authd had pushed. Updates to CAAP in the directory will not be reflected until authd is restarted.

peinit detects authd's exit (it's a child of peinit) and typically restarts it per policy. The restart goes through the same fork-install-exec pattern as the initial launch; the new authd instance reads the directory fresh and re-establishes the CAAP cache.

Sessions that authd had been managing are still associated with the old authd's auth_id values; the new authd doesn't "inherit" them but doesn't need to — the existing sessions continue running on the tokens already minted; the kernel knows they exist.

A failure that exceeds the restart policy's retry budget is treated as a critical failure; peinit may transition the system to a recovery state where administrative intervention is required.

Boot ordering implications #

Knowing what authd does has implications for boot ordering:

  • Services that need CAAP must start after authd has populated the cache. peinit enforces this by waiting for authd's readiness signal before launching such services.
  • Services that need real tokens must start after authd is up. Similarly waited for.
  • Services that are happy with SYSTEM can start before authd. This is mostly registry, observability, and other infrastructure services.
  • Login frontends must start after authd is up. They can't authenticate users otherwise.

The exact graph of dependencies is part of peinit's configuration. peinit knows which services depend on what and starts them in the right order.

The handoff is one-way #

A subtle but important property: the handoff from kernel-direct tokens to authd-managed tokens is one-way. Once authd is running and has taken over identity creation, the system does not revert to "kernel-direct only" without a full restart.

If authd crashes and restarts, it picks up where it left off (re-reading the directory, re-populating CAAP). It doesn't reset the system to SYSTEM-everywhere; that initial transition happens only once per boot.

The reason: the running services have already been assigned real tokens. Going back to SYSTEM would require reassigning every service's token, which would mean restarting every service. That's a system-wide reboot, not a recovery.

So the trust establishment chain is a one-time event per boot. It happens early; it sets up the system's authority distribution; it stays in place until shutdown. Reboot is the only way to redo it.

Where to go next #

For the init system that launches authd and waits on its readiness, read peinit at PID 1.

For the SYSTEM-everywhere state the handoff replaces, read Bootstrap tokens.

For the recovery policy that applies until the CAAP cache is populated, read Distribution and recovery.

Kernel invariants

Peios / Peios Security Fundamentals / Boot and trust establishment

The boot chain — kernel-direct tokens, peinit at PID 1, the authd handoff — relies on a handful of kernel-level invariants to be safe. Things like "PKM is in the LSM stack and no incompatible LSM is", "the kernel rejects unsigned kernel modules", "physical memory is restricted to what is appropriate". If these invariants are violated, the security model the rest of these docs describes is no longer guaranteed.

This page covers the invariants, where each is enforced, and the consequences of failure. They are not user-facing settings in the sense of "things you might change" — they are kernel build-time and init-time configurations that the deployment requires. But knowing what they are helps with reasoning about the trust chain and diagnosing the rare case where something doesn't fit the expected model.

The LSM stack invariant #

The Linux Security Module framework lets the kernel host multiple security modules in a defined order. Peios's KACS is implemented as an LSM called PKM (Peios Kernel Module). For PKM to function correctly:

  • PKM must be in the LSM stack.
  • MAC (Mandatory Access Control) LSMs must not be in the stack — specifically SELinux, AppArmor, SMACK, and TOMOYO.
  • The BPF LSM must not be active.

The reasoning: MAC LSMs and PKM both want to make authoritative access decisions, and the LSM framework's composition rules don't merge their decisions cleanly. A system with SELinux and PKM would have both modules trying to decide each access; the result would be that either both must agree (over-restrictive) or one supersedes the other (whichever the LSM order puts first), neither of which is the right semantic. Peios's model is that PKM is the sole authoritative module for the operations it covers.

What the kernel verifies #

The kernel verifies at PKM initialisation that no MAC LSM is active and that BPF LSM is not in use. If any are present, PKM refuses to activate. The kernel boots, but the access control is now broken — PKM is supposed to be enforcing, and it isn't. This is detected and reported as an init-time error.

Specifically, PKM refuses to activate if any of selinux, apparmor, smack, tomoyo, or bpf LSMs are present in the active stack.

The required stack #

The LSM stack Peios expects is, in order:

landlock, lockdown, yama, integrity, pkm
  • commoncap is implicit (Linux always includes it; not listed in CONFIG_LSM explicitly).
  • landlock is permitted — it's a process-local sandboxing LSM that processes opt into; it doesn't conflict with PKM.
  • lockdown is permitted — it gates specific kernel-debugging interfaces; orthogonal to PKM.
  • yama is permitted — it adds ptrace restrictions; complements PKM's ptrace gating.
  • integrity is permitted — Linux's IMA/EVM mechanisms can run alongside PKM for the use cases they cover.
  • pkm is the Peios module.

Distributors building a Peios kernel must include this stack. Building a kernel with SELinux enabled and active would produce a system where PKM cannot activate — boot would fail with a clear error.

Module signing #

CONFIG_MODULE_SIG_FORCE=y is required. This flag tells the kernel to refuse to load any kernel module that is not signed by a key the kernel recognises.

The reasoning: a kernel module runs with full kernel privileges. An attacker who can load arbitrary kernel modules bypasses every layer of PIP and KACS — they can read memory directly, modify any data structure, disable any check. The defence against this is to refuse to load unsigned modules.

CONFIG_MODULE_SIG_FORCE=y is the kernel-level switch that enforces it. With the flag set, unsigned modules are rejected at load time; a malicious or buggy administrator with SeLoadDriverPrivilege cannot load a malicious module unless they have the signing key, and it is not on a running system.

The module-signing key is deliberately a different key from the TCB key that signs binaries, and is not chained to it. Both are compiled into the image, so neither can rotate independently and neither can be an offline root — the TCB key signs binaries at build time. Chaining them would mean that compromising the TCB key also yielded the ability to load kernel modules. Keeping them independent contains binary signing and module loading separately. Both are ML-DSA-65.

The flag is verified at kernel build time. A kernel built without it doesn't enforce the rule and is not a valid Peios kernel for security purposes; the PIP threat model relies on this enforcement.

User-visible effect: kernel modules from the Peios distribution work (they are signed); modules built ad-hoc on a running system do not load. Third-party kernel modules (a vendor driver, say) must be signed by a key the kernel trusts, which in practice means going through the Peios signing infrastructure to add them.

The module-loading helper #

CONFIG_MODPROBE_PATH="/bin/modprobe" is required, and the binary it names must be TCB-signed.

When the kernel needs a module it does not have — mounting a filesystem whose driver is not built in, opening a socket for an unknown protocol family, resolving a crypto algorithm by name — it does not load it itself. It spawns a userspace helper to do it, and runs that helper at its own authority.

Two things follow. The path must be right: the upstream default is /sbin/modprobe, which does not exist on Peios, and left at that default every kernel-initiated load fails to find the helper while reporting nothing that names the cause.

And the path is a writable sysctl, which makes it an escalation vector — whoever can write /proc/sys/kernel/modprobe gets code of their choosing executed with the kernel behind it. The defence is not to protect the sysctl but to make redirecting it useless: KACS refuses any exec the kernel initiates on its own behalf unless the binary carries at least PeiosTcb trust. Pointing the sysctl elsewhere gains nothing without a TCB-signed binary to point it at.

This is the one place a signature gates execution rather than just labelling it. Everywhere else an unsigned binary runs and simply carries no integrity tier, because the process that asked for it bounds what it can do; a kernel-initiated exec has no such process behind it.

User-visible effect: a module the kernel asks for on your behalf loads only if the distribution's modprobe is in place and properly signed. A refusal is recorded as a kacs_exec event with reason umh-not-tcb, rather than appearing as an unexplained module-load failure.

Memory access restrictions #

CONFIG_STRICT_DEVMEM=y is required. This flag restricts /dev/mem and /dev/kmem access — physical-memory mapping pseudo-devices — to specific I/O regions, denying read or write access to actual system RAM.

The reasoning: a process that can read physical memory bypasses PIP. The DACL might say "this process cannot read the memory of authd"; the kernel's PIP check might agree; but if the process can mmap /dev/mem and read the corresponding physical pages, neither check matters. The defence is to restrict the device.

With CONFIG_STRICT_DEVMEM=y:

  • /dev/mem is readable only for the kernel's defined I/O regions (memory-mapped I/O, the VGA framebuffer, etc.).
  • General RAM is not readable through /dev/mem.
  • /dev/kmem is unavailable.

This closes the physical-memory bypass for PIP. Combined with CONFIG_MODULE_SIG_FORCE, the two flags eliminate the kernel-level escapes from PIP that don't require an actual kernel compromise.

The flag is build-time; the kernel image either has it or doesn't. A kernel without it is not a valid Peios kernel for security purposes.

What the kernel will not have #

The Peios kernel is built with specific things excluded:

  • CONFIG_BPF_LSM=n — the BPF LSM is not built. BPF programs running as security modules would conflict with PKM and bypass its checks.
  • MAC LSMs disabled — SELinux, AppArmor, SMACK, TOMOYO are all disabled. Their config flags are off.

These exclusions are part of the kernel image. A distribution that includes these LSMs in its kernel image is not running Peios's intended security model.

What about user-space hardening #

A few things are explicit in the Peios environment that aren't strictly kernel invariants but are operational expectations:

  • peinit is signed at TCB level. If the binary peinit is exec'd from is not signed at TCB, the kernel's verification at exec sets pip_type = None. peinit then runs unprotected, and any other process can interfere with it. This is operationally untenable; a properly-built Peios image has peinit signed.
  • authd, loregd, eventd, lpsd are signed at TCB level. Same reasoning. These are the TCB daemons; they need PIP protection to function in their role.
  • No kernel modules are loaded other than signed ones. Per CONFIG_MODULE_SIG_FORCE.

These are not enforced at boot in the sense of "the boot will fail" if violated — they are operational expectations. A Peios system where peinit is unsigned will boot, run, and "work" in a superficial sense; it just won't have the security properties Peios documents.

The invariant verification #

The kernel's init code does several checks at start:

  1. Verify PKM is in the LSM stack and no incompatible LSM is present. PKM refuses to activate if violated.
  2. Verify CONFIG_MODULE_SIG_FORCE is set. (This is a build-time flag; at runtime the kernel just behaves consistently with the build.)
  3. Verify CONFIG_STRICT_DEVMEM is set. (Same; build-time.)
  4. Construct the SYSTEM and Anonymous tokens. As covered in Bootstrap tokens.
  5. Attach the SYSTEM token to init. Init becomes the first process holding it.
  6. Start userspace. init execs the configured first program — prelude, the initramfs PID 1, which runs the initramfs and then execs the real init (peinit, in a normal Peios image). See The initramfs stage.

By the time userspace starts, the invariants are either satisfied (boot proceeds normally) or violated (boot fails or the security model is broken). The verification is internal; from the operator's perspective, the system either comes up cleanly or doesn't.

Failure modes #

If an invariant fails:

  • MAC LSM detected at PKM init. PKM refuses to activate. The kernel boots but the access control is effectively absent. peinit's exec verification (looking for a TCB signature) returns "valid" but no PIP is set because PKM isn't running. The system is unsafe.
  • CONFIG_MODULE_SIG_FORCE=n. Modules can be loaded without signing. An attacker with SeLoadDriverPrivilege can load arbitrary kernel modules and bypass everything. PIP is no longer meaningful.
  • CONFIG_STRICT_DEVMEM=n. /dev/mem can read physical memory. Any process with READ_CONTROL on /dev/mem (or whose DACL grants it) can bypass PIP by reading memory directly.

Each failure mode is a security regression. The boot chain depends on the invariants holding. A deployment that needs to be sure its security model is operating correctly verifies these as part of the boot validation — typically via a post-boot tool that reads /proc/sys/kernel/lsm, checks build-time flags via the kernel config, etc.

For most Peios deployments built from the official kernel image, the invariants hold by construction. The verification is mostly relevant for custom kernels built from source — anyone modifying the kernel image needs to know which flags they cannot change.

Why these invariants #

Two themes underlie all of these:

The trust chain is rooted in kernel integrity. SYSTEM token, PIP, signed binaries — every layer of the model assumes the kernel is honest. If the kernel can be modified at runtime (unsigned modules), or its memory can be read or written from userspace (/dev/mem), the foundation crumbles.

PKM must be the sole authoritative LSM. Trying to compose PKM with MAC LSMs would give either over-restrictive or inconsistent semantics; the Peios access model is built on PKM's decisions, and that requires PKM to make the decisions alone.

The invariants are what make these themes operational: the kernel is built and initialised in ways that protect itself from runtime modification and that ensure PKM is in charge.

Where to go next #

For the tokens the verified kernel constructs at init, read Bootstrap tokens.

For the signing scheme module signing shares its keys with, read Binary signing.

For the protection model these invariants defend, read Process integrity protection.

mkirf

Peios / Peios Security Fundamentals / Boot and trust establishment

mkirf compiles an initramfs source tree into an initramfs image. The source tree is an ordinary directory — on a running Peios system that directory is /boot/initramfs/ — whose contents map 1:1 onto / inside the initramfs. The image is a single deterministic, gzip-compressed newc cpio archive that the bootloader loads into memory alongside the kernel.

mkirf [--watch] [--debounce SECS] [--exclude GLOB]... <src-dir> <out-file>

This page is the command reference. For what the initramfs stage is — prelude, the /boot/initramfs/ layout, and the handoff to the real root — see The initramfs stage; for how hooks declare their order, see Boot hooks. mkirf reads <src-dir> and writes <out-file>; it never writes back into the source tree, so the directory you inspect is always exactly what packages and you have put there.

What a build does #

Given a source tree, one build runs a fixed sequence:

  1. Validate the layout. <src-dir> must be a directory, must carry an executable init (the initramfs PID 1 — a symlink is fine as long as it resolves within the tree), and must not already contain a hook sequence — hooks.seq or system/prelude/hooks.seq.<n> — which mkirf generates. A missing or non-executable init, or a stray sequence file, stops the build.

  2. Walk the tree. Every object beneath <src-dir> becomes a cpio entry — regular files (with their executable bit preserved), directories, symlinks (target stored verbatim), character/block device nodes, and FIFOs. Sockets, and any other unsupported type, are rejected. Entries are sorted in LC_ALL=C byte order, which places every directory before its descendants — the order the kernel's unpacker requires.

  3. Resolve the hook order. Hooks are the regular files directly under <src-dir>/usr/libexec/prelude/hooks.d/ (packaged) or <src-dir>/lcl/libexec/prelude/hooks.d/ (operator-placed), with the operator's copy winning when both hold the same file name. The legacy <src-dir>/hooks/ is still read, and each hook found there is reported with the directory it should move to. mkirf parses each hook's # /// hook metadata block, topologically sorts the resulting capability DAG, and bakes the execution order into generated manifests injected into the image at /system/prelude/hooks.seq.<n>. A missing hooks/ directory simply means no hooks — the manifests are still written, empty, because prelude treats a missing sequence as a broken image rather than as "nothing to run".

    The version is in the file name, and mkirf writes every version it can express faithfully — currently hooks.seq.1 (the flat resolved order) and hooks.seq.2 (that order plus each hook's declarations). mkirf ships in peiosutils and prelude ships in its own package, so an image can pair a newer writer with an older reader; naming the versions separately lets one image satisfy both, which a single file carrying an in-band version cannot. A version is dropped only when a future format carries something it can no longer project honestly.

    They live under /system — the tier for derived content, alongside system/retc — rather than /usr, which is the vendor tier no package ships these into, or /var/state, which is for mutable state rather than immutable build output. See Validation and errors below.

  4. Write atomically. The archive is written to a sibling temp file and renamed into place, so a failed run never leaves a half-written image behind. On success mkirf prints the path, entry count, and byte size to stderr.

Early (pre-decompression) segments #

A reserved <src-dir>/++/ directory, if present, holds early initramfs segments — content the kernel consumes before it decompresses the main archive, namely CPU microcode and ACPI table overrides. Each immediate child of ++/ is one segment whose own contents map onto the cpio root (++/microcode/kernel/x86/microcode/GenuineIntel.bin becomes kernel/x86/microcode/GenuineIntel.bin); the segment directory name is a human label and never appears in the archive. Every ++/ entry must itself be a directory. The segments are merged into one uncompressed cpio prepended ahead of the gzip-compressed main archive. With no ++/ directory the output is exactly a single gzip member. mkirf stays format-agnostic here: it knows "early segments", never "microcode".

The deterministic-build guarantee #

The same source tree always compiles to the same image, byte for byte. This is what makes "did anything actually change?" a meaningful question and underpins later work such as signed boot artifacts. Determinism comes from normalising everything that would otherwise vary between builds:

  • Ownership, inode numbers, and timestamps are all emitted as zero.
  • Permission bits are normalised. The source tree is authoritative for file type and, for regular files, executability — nothing else. Read/write permission bits are not Peios's access mechanism, so they are flattened to a constant: directories 0755, symlinks 0777, FIFOs and device nodes 0644, regular files 0755 if executable and 0644 otherwise.
  • The gzip member carries mtime 0 and no embedded filename (the gzip -n equivalent), at compression level 9.
  • The early region is byte-stable, with file payloads padded to a 16-byte boundary by widening the preceding entry's name field with NUL bytes.

Validation and errors #

mkirf will not produce an image from a source tree that cannot boot. A misconfiguration is caught when the image is built — on a running system where the message is easy to read — rather than as a mystery failure at the next boot. The checks:

  • Layout. <src-dir> is not a directory; no init, or init is not a regular file, or is not executable; a pre-existing hook sequence; a ++ entry that is not a directory; two ++ segments defining the same file. (Exit 1.)
  • Hooks. A malformed # /// hook metadata block (unclosed, a non-comment line inside it, a duplicate or unknown key, a badly-formed array), a dependency cycle, a requires that nothing supplies, or a capability that is both provided and contributed to (a capability is either a set of alternatives or a set of contributors, never both). An after naming a capability nothing supplies is not an error — that is what distinguishes it from requires. (Exit 1.) A hook with no metadata block at all is not an error — it is the escape hatch: it is scheduled last, in name order, and mkirf prints a warning so a forgotten block is visible.
  • I/O. Any filesystem read, write, or compression failure. (Exit 1.)
  • Usage. A semantic invocation error clap cannot express — currently, --watch with <out-file> inside <src-dir> (see below). (Exit 2.)

Watch mode #

With --watch, mkirf stays resident: it builds once up front (so the watch never begins from a stale archive), then watches <src-dir> recursively and rebuilds after every change, debounced. This is what lets /boot/initramfs/ behave like an ordinary part of the filesystem — edit a hook and the initramfs is current again — rather than a build artifact an administrator has to remember to regenerate.

Watch mode is a foreground loop that runs until killed; supervising and restarting it is a service manager's job, not mkirf's. A rebuild that fails (say, a hook edited into a dependency cycle) is logged but does not stop the watch, so fixing the offending file recovers on the next change.

<out-file> must not live inside <src-dir>: each rebuild would write the image back into the watched tree and retrigger the watch endlessly. mkirf refuses this invocation up front (exit 2). The --debounce window (default 5 seconds) is the settle time — mkirf waits for the tree to be quiet for that long before rebuilding, so a burst of edits produces one rebuild rather than many.

Options #

OptionEffect
--watchStay resident and rebuild on every change to <src-dir>, after an initial build. Runs until killed.
--debounce SECSWith --watch, the settle time before a rebuild. Default 5. A non-numeric value is a usage error.
--exclude GLOBExclude paths (relative to <src-dir>) matching GLOB from the image. Repeatable. * and ? stay within a path segment; ** crosses separators. A matched directory is pruned with its whole subtree. A malformed glob is a usage error.

The two positionals are both required:

PositionalMeaning
<src-dir>The source tree; its contents map onto / in the initramfs.
<out-file>The output cpio.gz archive.

Exit status #

CodeMeaning
0The image was built (or --help/--version was printed).
1An operational failure — an invalid layout, an unresolvable hook set, or an I/O/compression error.
2A usage error — a bad option, a missing positional, or --watch with <out-file> inside <src-dir>.

See also #

  • The initramfs stage — what mkirf's output is for.
  • Boot hooks — the # /// hook metadata block and how order is resolved.
  • mkuki — wraps mkirf's image, together with a kernel and command line, into a bootable UEFI unified kernel image.

mkuki

Peios / Peios Security Fundamentals / Boot and trust establishment

mkuki builds a UEFI unified kernel image (UKI): it takes a PE/COFF EFI stub and appends the kernel, the initramfs, and the kernel command line to it as named PE sections, producing a single EFI binary that UEFI firmware boots directly. Where mkirf produces the initramfs image, mkuki is the step that wraps that image — together with a kernel and a command line — into one bootable artifact. The two are the two halves of Peios' Dynamic Boot system.

mkuki --kernel PATH --initramfs PATH (--cmdline TEXT | --cmdline-file PATH) --out PATH
      [--stub PATH] [--watch] [--debounce SECS]
mkuki --stub-info

The UKI section layout #

A UKI is an ordinary PE/COFF EFI executable — the stub — with three extra sections appended, which the stub reads at boot to find its payloads:

SectionContentSource
.cmdlineThe kernel command line, NUL-terminated.--cmdline or --cmdline-file
.linuxThe kernel image.--kernel
.initrdThe initramfs cpio image.--initramfs

mkuki appends them in exactly that order. Each new section is marked as initialised, read-only data. The tool works at the PE level directly: it parses the stub's DOS/PE headers and section table, computes correctly-aligned virtual addresses and file offsets for the new sections, appends their data, updates the section count and the SizeOfImage/SizeOfHeaders fields, and — if the stub's header has no spare room for three more section entries — grows the header region and relocates the existing sections' file pointers to make space. A stub that is not a valid MZ/PE image, or whose optional header is malformed, is rejected.

The command line #

The command line is taken either literally from --cmdline or read from the file named by --cmdline-file (exactly one is required). Either way, mkuki trims trailing newlines and carriage returns and appends a single NUL terminator. A command line containing an embedded NUL byte is rejected.

The stub #

--stub names the EFI stub to append to. With no --stub, mkuki uses a stub bundled into the binary (the systemd EFI stub). mkuki --stub-info prints that bundled stub's provenance and exits, so you can see exactly what the default is without building anything.

Output and atomicity #

mkuki creates any missing parent directories of --out, writes the assembled image to a sibling temp file, and renames it into place, so an interrupted or failed build never leaves a half-written UKI behind. On success it prints the output path and byte size to stderr. Each of the three payloads must be non-empty; an empty .cmdline, .linux, or .initrd fails the build.

Note

mkuki does not sign the image. Producing the unified binary and signing it for Secure Boot are separate steps; mkuki has no signing options. Because a UKI bundles the kernel, initramfs, and command line into one PE image, that image is what a later Secure Boot milestone signs and the firmware verifies as a whole — but the signing itself is out of mkuki's scope.

Watch mode #

With --watch, mkuki stays resident: it builds once up front, then rebuilds the UKI whenever an input changes — so the boot image tracks a new kernel, a freshly-repacked initramfs (mkirf's half of Dynamic Boot), or an edited command-line file with no manual step. Like mkirf's watch mode, it is a foreground loop that runs until killed; supervising it is a service manager's job, and a rebuild that fails (for example, a kernel caught mid-copy) is logged rather than fatal, so fixing the input recovers on the next change.

The watched inputs are --kernel, --initramfs, and — only if it is a --cmdline-file, since a literal --cmdline is static — the command-line file. mkuki watches each input's parent directory (non-recursively), not the file itself: this survives the atomic temp-and-rename writes that mkirf and mkuki both perform (which appear as a directory event a stale single-file watch would miss) and catches a versioned kernel being swapped in /boot. Because the watch is non-recursive, a write to an --out path nested deeper under a watched directory does not retrigger it — but an --out sitting directly in a watched input directory would, so mkuki refuses that invocation. The --debounce window (default 5 seconds) is the settle time before a rebuild.

Options #

OptionEffect
--kernel PATHThe kernel image; becomes the .linux section. Required (except with --stub-info).
--initramfs PATHThe initramfs cpio; becomes the .initrd section. Required (except with --stub-info).
--cmdline TEXTThe kernel command line, given literally; becomes the .cmdline section. Mutually exclusive with --cmdline-file.
--cmdline-file PATHRead the kernel command line from PATH instead. Mutually exclusive with --cmdline.
--out PATHThe output UKI path. Required (except with --stub-info).
--stub PATHThe PE/COFF EFI stub to append sections to. Defaults to the bundled systemd stub.
--stub-infoPrint the bundled stub's provenance and exit.
--watchStay resident and rebuild the UKI whenever --kernel, --initramfs, or --cmdline-file changes. Runs until killed.
--debounce SECSWith --watch, the settle time before a rebuild. Default 5.

Exactly one of --cmdline or --cmdline-file must be given: supplying both, or neither, is a usage error.

Exit status #

CodeMeaning
0The UKI was built (or --stub-info, --help, or --version was printed).
1A build or watch failure — a missing or unreadable input, an invalid stub, an empty payload, or an I/O error.
2A usage error — a bad or missing option, or an invalid --cmdline/--cmdline-file combination.

See also #

  • mkirf — builds the .initrd payload mkuki wraps.
  • The initramfs stage — what the bundled initramfs does once firmware boots the UKI.

Peios Learn — documentation for the Peios project.

Built with Trail.