# Boot and trust establishment

---

# Boot and trust establishment

_Peios / Peios Security Fundamentals / Boot and trust establishment_

> How a freshly-started kernel becomes a running system with real identities — bootstrap tokens, the initramfs, peinit at PID 1, and the authd handoff.

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](/peios/security-fundamentals/boot-and-trust-establishment/bootstrap-tokens.md).

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](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md).

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](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md).

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](/peios/security-fundamentals/boot-and-trust-establishment/peinit-pid-1.md).

If you want the authd transition — what authd does at startup, the CAAP cache population, the SYSTEM-everywhere-handoff — read [authd handoff](/peios/security-fundamentals/boot-and-trust-establishment/authd-handoff.md).

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](/peios/security-fundamentals/boot-and-trust-establishment/kernel-invariants.md).

The boot *artifacts* themselves are built by two command-line tools, the two halves of Peios' Dynamic Boot system. [mkirf](/peios/security-fundamentals/boot-and-trust-establishment/mkirf.md) compiles the `/boot/initramfs/` source tree into the deterministic initramfs image; [mkuki](/peios/security-fundamentals/boot-and-trust-establishment/mkuki.md) 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_

> Before userspace exists the kernel constructs two tokens — authority-maximal SYSTEM and identity-minimal Anonymous — that bootstrap the identity model.

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:

| Field | Value |
|---|---|
| `user_sid` | `S-1-5-18` (the SYSTEM well-known SID) |
| `groups` | `BUILTIN\Administrators` (with `SE_GROUP_OWNER`), `Everyone`, `Authenticated Users`, `Local`, and the bootstrap logon SID `S-1-5-5-0-0` |
| `integrity_level` | `S-1-16-16384` (System integrity — the highest) |
| `mandatory_policy` | `NO_WRITE_UP` |
| `privileges` | Every privilege defined in the catalog, all present, all enabled |
| `token_type` | Primary |
| `impersonation_level` | Anonymous (conventional for primary tokens) |
| `auth_id` | 0 (the SYSTEM session) |
| `default_dacl` | Grants 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](/peios/security-fundamentals/impersonation/impersonation-levels.md)).

| Field | Value |
|---|---|
| `user_sid` | `S-1-5-7` (the Anonymous well-known SID) |
| `groups` | `Everyone` only |
| `integrity_level` | `S-1-16-0` (Untrusted — the lowest) |
| `mandatory_policy` | `NO_WRITE_UP` |
| `privileges` | None |
| `token_type` | Impersonation (it exists specifically for impersonation) |
| `impersonation_level` | Anonymous |
| `auth_id` | 998 (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](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md)). 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](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md).

For the moment real identities start replacing SYSTEM, read [authd handoff](/peios/security-fundamentals/boot-and-trust-establishment/authd-handoff.md).

For what a token is in general — types, fields, lifecycle — read [Tokens](/peios/security-fundamentals/tokens/overview.md).

---

# The initramfs stage

_Peios / Peios Security Fundamentals / Boot and trust establishment_

> Between kernel init and peinit, prelude — the initramfs PID 1 — prepares the environment, runs the boot hooks that mount the real root, and hands off.

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](/peios/security-fundamentals/boot-and-trust-establishment/bootstrap-tokens.md) covers what the kernel constructs before any userspace exists; [peinit at PID 1](/peios/security-fundamentals/boot-and-trust-establishment/peinit-pid-1.md) 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](/peios/security-fundamentals/boot-and-trust-establishment/bootstrap-tokens.md) describes the SYSTEM token being handed to. From the moment prelude starts, the "everything is SYSTEM" phase of boot (covered in the [overview](/peios/security-fundamentals/boot-and-trust-establishment/overview.md)) 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](/peios/security-fundamentals/boot-and-trust-establishment/peinit-pid-1.md): 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](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md).

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](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md) 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](/peios/security-fundamentals/boot-and-trust-establishment/mkuki.md) it sits inside an image the firmware verifies in one piece.

The set is **closed under its own dependencies**: the build indexes it with `depmod -e` against the kernel's symbol table and fails if any module in it imports a symbol that neither the kernel nor another module in the set provides. That matters because a subset is exactly where an ordinary index goes quiet — depmod cannot record a dependency it cannot resolve, so a module whose provider was left out indexes cleanly and fails at boot with an unknown symbol. Every module the initramfs carries is therefore one it can load.

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](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md)).
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](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md), 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](/peios/security-fundamentals/boot-and-trust-establishment/mkirf.md) 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](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md) 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 `exec`s 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](/peios/security-fundamentals/boot-and-trust-establishment/bootstrap-tokens.md): 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](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md).
- **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](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md).

For the init system prelude hands the machine to, read [peinit at PID 1](/peios/security-fundamentals/boot-and-trust-establishment/peinit-pid-1.md).

For the tool that compiles `/boot/initramfs/` into the image, read [mkirf](/peios/security-fundamentals/boot-and-trust-establishment/mkirf.md).

---

# Boot hooks

_Peios / Peios Security Fundamentals / Boot and trust establishment_

> Boot hooks do the deployment-specific work of early boot. Where they live, the metadata block, the four ordering keys, and the exit-code protocol.

A **boot hook** is a shell script that runs inside the initramfs, before the real root is mounted — during the [initramfs stage](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md). 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:

| Directory | For |
|---|---|
| `/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](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md) 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.

```sh
#!/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:

| Capability | Meaning |
|---|---|
| `initramfs-ready` | The 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-ready` | The 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:

| Exit | Meaning | What prelude does |
|---|---|---|
| `0` | **Satisfied** — I did my part. | Its `provides` are achieved; its `contributes` are one step closer to complete. |
| `69` | **Declined** — not applicable on this machine. | It is not counted toward the capabilities it declared. |
| `75` | **Deferred** — I cannot run yet, and I have changed nothing. | Re-queued and tried again once something else has made progress. |
| anything else | **Failed.** | 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](/peios/security-fundamentals/boot-and-trust-establishment/bootstrap-tokens.md)), 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:

```sh
#!/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:

```sh
#!/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](/peios/security-fundamentals/boot-and-trust-establishment/peinit-pid-1.md)'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](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md).

For the build that validates hooks and resolves their order, read [mkirf](/peios/security-fundamentals/boot-and-trust-establishment/mkirf.md).

For what takes over once a hook has mounted the real root, read [peinit at PID 1](/peios/security-fundamentals/boot-and-trust-establishment/peinit-pid-1.md).

---

# peinit at PID 1

_Peios / Peios Security Fundamentals / Boot and trust establishment_

> Why peinit is PID 1 — TCB-signed, holding the SYSTEM token — and the fork-install-exec pattern that launches every service with the right identity.

**peinit** is the init system of a running Peios system — the process that takes over once the [initramfs stage](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md) 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](/peios/using-peios/services-and-jobs/overview.md).

## 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](/peios/security-fundamentals/process-integrity-protection/pip-in-practice.md). 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](/peios/using-peios/services-and-jobs/boot-and-boot-modes.md); 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](/peios/using-peios/services-and-jobs/identity-and-privileges.md)).
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](/peios/using-peios/services-and-jobs/overview.md).

## 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_

> When authd comes online the system moves from SYSTEM-everywhere to real identities — token minting, session management, and CAAP distribution.

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](/peios/security-fundamentals/logon-sessions/lifecycle.md).

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](/peios/security-fundamentals/boot-and-trust-establishment/peinit-pid-1.md).

For the SYSTEM-everywhere state the handoff replaces, read [Bootstrap tokens](/peios/security-fundamentals/boot-and-trust-establishment/bootstrap-tokens.md).

For the recovery policy that applies until the CAAP cache is populated, read [Distribution and recovery](/peios/security-fundamentals/central-access-policies/distribution-and-recovery.md).

---

# Kernel invariants

_Peios / Peios Security Fundamentals / Boot and trust establishment_

> The kernel-level invariants the boot chain depends on — the LSM stack rule, forced module signing, strict /dev/mem — and what fails without them.

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](/peios/security-fundamentals/boot-and-trust-establishment/bootstrap-tokens.md).
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](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md).

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](/peios/security-fundamentals/boot-and-trust-establishment/bootstrap-tokens.md).

For the signing scheme module signing shares its keys with, read [Binary signing](/peios/security-fundamentals/binary-signing/overview.md).

For the protection model these invariants defend, read [Process integrity protection](/peios/security-fundamentals/process-integrity-protection/overview.md).

---

# mkirf

_Peios / Peios Security Fundamentals / Boot and trust establishment_

> Compile the /boot/initramfs/ source tree into a deterministic cpio initramfs image, resolving hook order and validating the layout. Has a watch mode.

`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/`](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md) — 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](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md); for how hooks declare their order, see [Boot hooks](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md). `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](#validation-and-errors) below.
4. **Write atomically.** The archive is written to a sibling temp file and `rename`d 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

| Option | Effect |
|---|---|
| `--watch` | Stay resident and rebuild on every change to `<src-dir>`, after an initial build. Runs until killed. |
| `--debounce SECS` | With `--watch`, the settle time before a rebuild. Default `5`. A non-numeric value is a usage error. |
| `--exclude GLOB` | Exclude 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:

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

## Exit status

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

## See also

- [The initramfs stage](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md) — what `mkirf`'s output is for.
- [Boot hooks](/peios/security-fundamentals/boot-and-trust-establishment/boot-hooks.md) — the `# /// hook` metadata block and how order is resolved.
- [mkuki](/peios/security-fundamentals/boot-and-trust-establishment/mkuki.md) — 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_

> Build a UEFI unified kernel image — kernel, initramfs, and command line appended as PE sections onto an EFI stub. Has a watch mode.

`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`](/peios/security-fundamentals/boot-and-trust-establishment/mkirf.md) 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:

| Section | Content | Source |
|---|---|---|
| `.cmdline` | The kernel command line, NUL-terminated. | `--cmdline` or `--cmdline-file` |
| `.linux` | The kernel image. | `--kernel` |
| `.initrd` | The 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 `rename`s 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](/peios/security-fundamentals/boot-and-trust-establishment/mkirf.md), 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

| Option | Effect |
|---|---|
| `--kernel PATH` | The kernel image; becomes the `.linux` section. Required (except with `--stub-info`). |
| `--initramfs PATH` | The initramfs cpio; becomes the `.initrd` section. Required (except with `--stub-info`). |
| `--cmdline TEXT` | The kernel command line, given literally; becomes the `.cmdline` section. Mutually exclusive with `--cmdline-file`. |
| `--cmdline-file PATH` | Read the kernel command line from `PATH` instead. Mutually exclusive with `--cmdline`. |
| `--out PATH` | The output UKI path. Required (except with `--stub-info`). |
| `--stub PATH` | The PE/COFF EFI stub to append sections to. Defaults to the bundled systemd stub. |
| `--stub-info` | Print the bundled stub's provenance and exit. |
| `--watch` | Stay resident and rebuild the UKI whenever `--kernel`, `--initramfs`, or `--cmdline-file` changes. Runs until killed. |
| `--debounce SECS` | With `--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

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

## See also

- [mkirf](/peios/security-fundamentals/boot-and-trust-establishment/mkirf.md) — builds the `.initrd` payload `mkuki` wraps.
- [The initramfs stage](/peios/security-fundamentals/boot-and-trust-establishment/initramfs-stage.md) — what the bundled initramfs does once firmware boots the UKI.
