§12d. Boot ordering and failure posture
planarian @ a924e09 · Spec v4.6 · snapshot main · all versions
Admit no principal before the monitor. planariad is ordered before every unit that admits a principal: the display manager (whichever unit NixOS uses for it — display-manager.service, or greetd.service where services.greetd is enabled without the display-manager alias), getty, sshd, the model service, any remote relay, user@.service, and every agent unit, declared or launched. Agents started by planarian agent run are created in a planarian-agents.slice with After=/Requires=planarian-ready.target and a start condition that fails while any planarian-failed@*.target is active; the posture’s disable agents response stops that slice as a whole, not a configured list, so an agent the launcher created cannot outlive a failure posture (I20). Each launch carries TasksMax=, MemoryMax= and a per-uid count from services.planarian.agents.limits. When it is ready it reaches planarian-ready.target. Whether principal-admitting units merely follow that target or require it is the failure posture. With this ordering, the window before the monitor is ready contains only firmware, bootloader, kernel, initrd, and the generation’s own early units, all already in the trusted base; and the change path fails closed while planariad is down, since Proposal is the only write path.
Enforced outside the daemon. The NixOS module generates planarian-ready.target and one planarian-failed@<class>.target per failure class, wired with ordering and OnFailure= in systemd and the initrd. planariad never implements its own failure response. The posture is declared in the generation, so it is immutable at boot.
| Class | Meaning | Detected by |
|---|---|---|
Absent | planariad did not reach ready within the boot-ready timeout, or was masked or stopped | ordering timeout on planarian-ready.target |
Crashed | reached ready, then died and exhausted its restart budget; or planarian-recovery exhausted its own start limit | OnFailure= on both units, each with a StartLimitBurst so a crash loop ends in the class rather than running forever |
Integrity | core hash ≠ signature (I2); journal chain broken (I18); boot report found undeclared early units or processes; invalid signature on the external desired-state source (§9); in E, a measured-boot mismatch | boot report, planarian-recovery’s independent check, initrd measurement |
Integrity is always treated at least as seriously as Absent.
Responses (composable): alarm (a non-dismissable banner on the login screen and in every session until an administrator acknowledges it: for Absent/Crashed, “Planarian, which protects this computer from unsafe changes, is not running. Assistants and system changes are turned off until it is fixed. [Administrator] has been notified.”; for Integrity, “This computer may have been tampered with. Do not enter passwords or open sensitive files. [Administrator] has been notified.”); notify administrators, local and remote, with the boot report; disable agents (the model service, any relay, every agent principal are not started); restrict remote access to administrators; restrict login to local administrators in a recovery session; restrict network (none, mesh-to-administrators, or full); halt to recovery (boot stops at a recovery target); in E, seal (the disk-encryption key is sealed to measurements that include the core, so an Integrity failure leaves the disk locked).
| Posture | For | Absent / Crashed | Integrity |
|---|---|---|---|
| Relaxed | Developing Planarian; throwaway VMs | Alarm (dismissable), disable agents | Alarm, notify, disable agents |
| Standard (default) | Personal machines | Alarm, notify, disable agents, restrict remote access; local login proceeds | Alarm, notify, disable agents, restrict remote access, network to mesh-to-administrators (in A, before any mesh exists: remote access off, network otherwise unrestricted); local login proceeds with the warning |
| Guarded | Work machines; agent hosts | Alarm, notify, disable agents, restrict remote access, restrict login | Halt to recovery, network to mesh-to-administrators |
| Sealed | Servers, fleet nodes | Halt to recovery, network to mesh-to-administrators | Halt to recovery, no network, seal (E) |
Every posture disables agents on any failure (I20). Restrict remote access to administrators means Planarian’s administrators (root and planarian-admin), not wheel. In A, alarms and notification are local (the login screen, every session, wall, the system journal): root can clear them, and a machine whose planariad is stopped can go unnoticed by anyone not at it. From C, the signed health report doubles as a dead-man signal: an off-machine receiver alerts administrators when reports stop arriving within twice the declared cadence. Custom postures combine the options per class, with two constraints: agents are always disabled, and the Integrity response is never weaker than the Absent response. The installer selects the posture (C; A ships Standard). The boot-ready timeout is configurable per machine, default 60 s, bounded 15–600 s. Raising the posture or shortening the timeout is a Tested change; lowering the posture or lengthening the timeout is Security-sensitive (AskFirst with administrator authority) and takes effect at the next boot, never in the current one: the posture units read the declared posture from the booted generation’s files, and a switch that would weaken either leaves the booted values in force until reboot (the module implements this by having planarian-ready.target’s configuration come from /run/booted-system, not /run/current-system). Recovery exits: an administrator acknowledges the alarm (journaled with identity), or planariad reaches ready and the boot report is clean, after which restrictions lift. Only an administrator clears an Integrity failure.
Network security fails closed (owner decision, Nov 2). Which units. A network tool is a unit able to change how traffic leaves the machine: one that holds CAP_NET_ADMIN or CAP_NET_RAW, or could gain either (its bounding set allows it and NoNewPrivileges= is unset, so a setuid or file-capability binary such as ping can grant it), can open /dev/net/tun or /dev/ppp, declares network interfaces, or runs as root without a capability bounding set (computed at build time from the unit’s systemd configuration, never from its name; fail closed). A network tool is protecting traffic while the kernel has attributed to it a change that currently steers traffic: an interface it created that carries a default or all-covering route, a default or all-covering route it added over a virtual interface it did not create (a persistent tun or tap made by a setup unit, or one moved in from another network namespace; the creator, where known, is protecting too), a policy rule catching all traffic, DNS servers on its interface, the system proxy, or firewall rules that drop or redirect traffic. Planarian attributes every routing, interface, rule and nftables change to the unit that made it from inside the kernel (eBPF, written with aya, on the netlink receive paths, the legacy netfilter setsockopt path, and network-device registration, so an interface created by ioctl such as a tun is attributed too; only changes in the host’s network namespace count; crates/backends/netwatch), and keeps each unit’s current footprint in /run. A network-security unit is a network tool that is protecting traffic. Its own interfaces and peer endpoints, read from its footprint, are what the hold passes; holdInterfaces and holdAllow may add to them. Fail closed: a change the kernel could not attribute, a footprint missing or stale when a network tool fails, or a watch that failed to attach, all count as protecting. Network tools start after the watch (owner decision, Nov 2). Every network tool is ordered after planarian-netwatch.service. That service reports ready only once its probes are attached and its first footprints are written, so no network tool can change how traffic leaves the machine before the watch sees it. The ordering is not a dependency: if the watch fails to start, network tools still start, and the watch counts as failed (fail closed, above). The watch in the initrd (owner decision, Nov 3). With the systemd initrd, the watch also runs in the initrd, ordered before every network tool there. Its probes and event queue stay in the kernel across the switch to the root. The stage-2 watch takes over the initrd’s footprints and the events queued meanwhile. So no network tool, in either stage, acts unwatched. If the stage-2 watch cannot take over (a version mismatch, or the probes are gone), it attaches afresh, and anything it cannot attribute counts as protecting. A unit the watch itself needs in order to start cannot wait for it. The module computes those units, lists them in the export (networkToolsUnordered), and rejects a configuration in which the ordering would form a cycle; for those units, a failure before the watch attaches holds traffic. With a scripted initrd, the initrd runs no network tools. When a network-security unit has failed (observed Failed, or not running while it is desired running; a stop through an attested override is not a failure), its traffic is held: an nftables table drops all traffic in and out except loopback, address assignment (DHCP, router advertisements), traffic to the endpoints the unit’s manifest entry declares in holdAllow, traffic over the unit’s own interfaces (holdInterfaces: while it is down they carry nothing, and once it is back its traffic goes only through it, as a VPN kill switch does; owner decision, Nov 2), and established connections of administrator sessions; new connections, new SSH logins among them, are refused. The hold is applied by systemd through OnFailure= on the unit (a crash or a kill) and by planariad for a stop outside Planarian that is not attributed (always a fault, §11 residue), so it does not depend on the daemon; the table is Sensitivity::Core, survives a restart of planariad, and is in place before the incident opens. The unit’s restart runs at once, with an alarm (“Running and its Readiness probes pass. If repair fails (the attempt budget is exhausted, or RecoveryFailed), the hold stays and administrators are notified: the machine never falls back to unprotected traffic on its own. An authorized principal may lift a hold with planarian hold release <unit> [--until], journaled as an attested Override on the hold; a hold protects a Security-sensitive unit, so releasing it needs administrator authority, and a non-administrator’s release is held as AskFirst with ApproverClass::Administrator (§11). Gym cases (VM): kill a WireGuard unit, then new outbound connections fail, loopback works, an administrator’s established SSH session survives and a new SSH login is refused, the restart restores the tunnel and the hold lifts, with one alarm; the same with the restart failing every attempt, then the hold stays, administrators are notified and nothing is reachable; planarian stop on the unit holds nothing; a non-administrator’s hold release asks for an administrator; at boot, every network tool becomes active only after the watch is ready, and nothing is unattributed; with the watch failing to start, the tools still start and a failing one is held; with the systemd initrd, every network tool the initrd runs is attributed (journald, udevd, module loading), and no footprint carries pre-attach after switch-root.
A network tool Planarian does not manage (owner decision, Nov 3). Such a unit is a network-security unit with no services.planarian.units entry. When it fails: it is held like any other; Planarian does not repair it, because it repairs only declared units; the hold lifts when the unit is Running again by other means, or on hold release; the alarm says it needs a person: “
Core hash in A–D. The reference hash for I2 covers every Planarian binary and script the generation installs (planariad, planarian-recovery, the CLI, the posture units’ scripts), recorded at build. Signatures replace it in E. A test build (the gym-hooks package, §15) installed outside the gym VM tests is an Integrity failure, and the module asserts against it.
Lock the boot chain where the machine allows it: bootloader editing disabled (boot.loader.systemd-boot.editor = false); Secure Boot with a signed unified kernel image (Lanzaboote) from C; TPM measured boot later. In C, pre-Planarian boot entries are garbage-collected after enrollment. Entries below the security floor (§12e) stay in the boot menu (I1) but are labeled, are never the automatic fallback, and, where the bootloader supports it, require administrator confirmation.