ELEBAKE(8) System Manager’s Manual ELEBAKE(8)

elebake - compiler for a verified-boot trust chain: build, sign, attest, deploy, and the tamper-detection loader’s decisions

elebake command [args ...]

elebake help [command | group | topic]

elebake maintains the trust chain of a locally built FreeBSD boot loader: checked-out source, measured expectations, Authenticode signature, OpenPGP-attested manifest, deployment to removable boot media, and tamper witnesses read back at boot. Detection, not enforcement: the design assumes the boot medium can be tampered with and concentrates on noticing.

Every elebake command is a generator: it emits shell text describing an action. Whether that text is displayed, piped, or executed is decided by an interpreter the operator controls per function (see ENVIRONMENT). The shipped default displays (cat): inspect first, then pin an executing interpreter. Batch commands emit an unconditional sequence of further elebake commands; checking is itself a command in the sequence, and the batch machinery stops at the first failure.

The command reference below is generated from the tool’s own help corpus and is therefore always current; elebake help renders the same corpus in the terminal.

elebake is nothing without the loader it compiles for. The measurement, claim, gate and policy engine that reads the compiled decisions at boot is a FreeBSD patch series – platform trust gates – and the catalogs elebake offers (stage measure, stage action, stage when, stage phase show) are parsed from the headers of that series. Against a stock FreeBSD tree the catalogs are empty and no foundation can be built.

Get the tree that carries the series:

git clone -b elvboot-15.1 https://github.com/johannes-bruegmann/freebsd-src.git ~/git/freebsd-src
elebake setenv ELEBAKE_FREEBSD_SRC ~/git/freebsd-src
elebake freebsd prerequisites
    

The branch is based on releng/15.1 (15.1-RELEASE); the loader-side engine lives under stand/efi/loader/local/ and its README points back at this project. Parts of the series are on their way upstream. stage checkout adds a git worktree of that repository per stage, so the source stays one repository and every stage builds from a named ref.

Every command, grouped as elebake help groups it. elebake help <command> shows parameters, environment and examples.

The commands that run before a database exists: bootstrap creates one, help and the manual read the shipped corpus, complete serves the shell. Start here.

Create the database ~/.elebake/<name> (under ELEBAKE_ROOT) with the named profile and point the active-DB symlink db at it. <name> is a plain name, not a path: a slash is refused, db is reserved
Show the grouped overview, or detail for a command, group or topic
The concept topic: the environment system and the safety-first design (interpreters, pins, the three-layer cascade, profiles, logs) – template/manual/topic-environment.md
Emit the complete manual as Pandoc Markdown, part by part: the title block, the prose building blocks from template/manual/, COMMANDS from the help corpus, ENVIRONMENT from the variable templates – one truth per content kind (make man renders elebake.8 from it)
The Pandoc title block of the manual: name, section, author, today’s date
The COMMANDS section of the manual, generated from the help corpus and grouped as `elebake help' groups it
The ENVIRONMENT section of the manual: one entry per shipped variable template, its @summary as the body – the templates ARE the corpus (help env reads the same files)
Print completion candidates for a partially typed command line (bash-completion back-end): the words typed so far, the last one being the word under the cursor
Pipeline commands PRINT their shell: review, then append `| sh' to run (`| sudo sh' where the ESP or root-only keys are involved). Bookkeeping (add/bind/setenv/filter) is pinned to sh and acts directly. Experienced users may flip the safety default so EVERY terminal acts: elebake setenv ELEBAKE_TERMINAL_INTERPRETER sh (inspect any single command via setintp <family> cat, or per run under an explicit interpreter).

The database’s environment: the profile installed and re-synced (environment init), the cache, the freeze; variables and interpreter pins read and changed (setenv, getenv, unsetenv, setintp, getintp).

Install (or re-sync after an update) the profile’s template variables into .env/default: the profile is shipped, then `environment install'
Manage the cached-environment optimisation
Build the environment cache, the pattern of stage site mk: `environment cache render' redirected into .env/local/ELEBAKE_CACHE_ENV_ARGS.new (a fresh scan of .env/), then `environment cache place' moves it into place (0600) and says how many variables. The scan happens when the lines run, not when this command generates: a compiled script carries them, and they see the setenv acts before them. A database whose cache predates this version knows no pin for `place' and only displays it: re-sync first (environment init <profile>), it drops the cache
The environment as a new call finds it: at generation time ELEBAKE_CACHE_ENV_ARGS and ELEBAKE_CONTEXT_BOOTSTRAPPED are unset; the lines are `environment cache off' and `environment cache on' – the cache rebuilt from .env/ when they run, the calls after them look their pins up afresh. A dump places it after the setenv lines of its prologue and of its epilogue
Remove the environment cache; later invocations read the layered store again
Is the environment cache on, how many variables does it hold, and is the environment frozen (environment freeze)?
Freeze the interpreter environment of this database, with no way back: the name IS the confirmation (as destroy), a warning says what goes, then every machine override that weakens a combinator or batch pin is removed (environment freeze pins), the terminal default becomes sh (acts run), the marker .env/frozen is written, the cache rebuilt. Frozen, setenv and unsetenv refuse those pins. The shipped defaults are the only interpreters that read ONE line reliably (head -n1 | xargs ... for a combinator, the batch runner for a batch): a pin of sh or cat there works until two lines stand on stdin. A pin that must deviate is a decision taken BEFORE the freeze, in the template that ships it, never a machine override afterwards. Assumes: the database is provisioned and about to be trusted with acts; there is no thaw
Initialize the database at ELEBAKE_BASE: the layout (database init), then the profile’s environment (environment init), then the environment cache (environment cache on) – every later invocation reads one file instead of the layered store
Set a machine override: the name is a variable name, a frozen database allows it (environment frozen allows: no combinator or batch pin after environment freeze), the value is written to .env/local/<VAR> (0640, owner of the database when possible), the environment cache refreshed when one is on. Effective with the next command
Show the effective value of a variable and the layer that answers (.env/local override, .env/default, the shipped template): the variable resolves: rewrite to `getenv read <VAR> <layer> <path>', else an error line
Remove the machine override .env/local/<VAR> when there is one (else say so) – a frozen database allows it first (environment frozen allows) – and refresh the environment cache when one is on
Show all effective environment variables, or just one
Text terminal: the documented variables (templates, and the copies under .env); show one with `help env <name>' – the name resolves literal, then ELEBAKE_<name>, then ELEBAKE_INTERPRETER_<name>
Pin a function’s interpreter: rewrite to `setenv <variable> <interpreter>' (the variable from the token)
Show a function’s pinned interpreter: rewrite to `getenv <variable>'
Show the documentation of an interpreter variable (class default or per-function): rewrite to `help env <variable>'

Back up, restore and batch-drive the database itself.

Sign a file with a registered openpgp key (armored detached signature -> <file>.asc): the file exists, the record exists and carries its keyid; then `attest sign'
Check a detached signature at generation time – signed by the PINNED key, key neither expired nor revoked: the file exists, the record exists and is complete; then `attest signer pinned' (a finding fails the command)
Emit the prologue of a dump: the SOURCE’s .env/local overrides that the restore itself depends on (the PROLOGUE profile’s list – deliberately WITHOUT function-specific interpreter pins, which could deactivate replayed commands), then `environment refresh' so the pins hold for the lines after it
Emit the epilogue of a dump: EVERY .env/local override of the source, including function-specific interpreter pins – restored at the very end, when the replay is done
Remove this database and everything it owns, IRRECOVERABLY, as three steps: the worktrees and their registration in the source repo, the records, the scaffolding (bundle handover area, active-DB symlink, empty root). The name IS the confirmation, checked before a line runs
Step 1 of destroy: the name is this database’s: rewrite to `destroy worktrees remove' (every stage worktree AND its registration in the source repo – a plain rm -rf would leave “missing but already registered worktree” behind; the ids are only knowable while the database exists), else an error line
Step 2 of destroy: the name is this database’s: rewrite to `destroy records remove' (the database directory itself, resolved through the active-DB symlink first – rm -rf on a symlink removes the LINK and orphans the database), else an error line
Step 3 of destroy: the name is this database’s: rewrite to `destroy scaffold remove' (the bundle handover area, the active-DB symlink if it points here, the empty scaffolding), else an error line
Print the version of this elebake: the dump format it writes and restores (ELEBAKE_FORMAT) and the commit of the checkout it runs from (git; `unknown' without one) – what a dump carries in its header and what a report names
Export the database as an executable shell script; the bare form is the COMPLETE description of every stage (strategy `complete', scope `all'). The strategy selects the vocabulary of the body: complete is everything the database holds, minimized is binary management only (loaders, kernel modules, loader.conf, backups, media – what a rescue system needs to roll back). The dump is an EXECUTABLE description: keep it in git and replaying a committed dump reproduces the database. Import lines reference base elements against “$ELEBAKE_ARCHIVE_BASE”; `export' pairs the dump with the bundle that carries them
The complete dump: the export serial read here (the header carries the next one), then `dump complete <stage> <serial>' – the prologue (header, environment, keys, provenance), the foundation arsenal as CLI replays, the full description of the stage (or every stage), the epilogue
The minimized dump, the rescue vocabulary: the export serial read here, then `dump minimized <stage> <serial>' – the prologue (header, environment, keys, provenance), per stage only what binary management needs (record, media, backups, the loader/kernel/loader.conf subset of boot/), the epilogue. No foundation, no worktree, no bindings, no rebuild: a rescue system swaps binaries, it does not build them
Replay a dump into the current database – only a dump signed by the PINNED attest key, at or above the lineage’s serial; <base> binds where its base elements come from (default: this database). The dump’s format (its `# Version:' header) selects the restore that speaks it: `restore v<format> <dump> <base>', which reads the serial, the pinned key, the signer and the signer’s floor once and hands them down – ONLY into an EMPTY database (see import): records already present collide with the replay
Execute a file of elebake commands line by line (ELEBAKE_BATCH_KEEP_GOING: 0 = stop at the first failing line, 1 = keep going): the file is readable: rewrite to the runner `batch <file> false' (no cleanup), else an error line
The complete list of files an archive must carry: every key class, the provenance, the foundation – and with a stage its records and boot artifacts. The bare form is `collect all'
Apply the default strategy to a collection (delegates: default -> redacted)
Drop what is a machine secret in spirit: marker values, site.mk baselines, backups – and always the operational directories. What a bug report needs, without the reporter’s fingerprints. The collection is readable: rewrite to `filter write redacted ...', else an error line
Keep everything except the operational directories. For one’s own disaster recovery, never for sending. The collection is readable: rewrite to `filter write full ...', else an error line
Keep binary management only: per stage the loader (built and signed), loader.conf, kernel and modules, the manifest pair, media and backups; keys and receipts. What a rescue system needs to roll back or repair, nothing to build with. The collection is readable: rewrite to `filter write minimized ...', else an error line
Pack the collected files with ELEBAKE_ARCHIVER: the collection is readable, the archiver set, the archive path absolute, the manifest pair (MANIFEST + MANIFEST.asc) lies beside the collection – an archive that carries no tamper detection is not an artifact this tool produces – and the collection lives inside the database (the pair is packed by its database-relative path); then `bundle pack'
Append the bundle’s sha256 and size to the dump as its seal line (`# Bundle: sha256=... bytes=...'): both files readable, the dump not yet sealed (a dump names ONE bundle) and not yet attested (sealing after signing would invalidate the signature); then `seal write'. Attest the dump AFTER sealing so one signature covers both
Check that the dump’s seal line names THIS bundle (sha256 and size): both files readable, the dump sealed, the seal matching – a mismatch fails the command before anything is extracted
Empty one scratch directory under $ELEBAKE_ROOT/incoming/ – import’s own unpacking area, cleared before each extract so a re-import never trips over the last one. The path lies below incoming/ and carries no `..': rewrite to `incoming remove', else an error line (only import’s own scratch is cleared here, never a database)
Unpack an archive into a scratch directory with ELEBAKE_EXTRACTOR: the archive readable, the extractor set, the destination absolute and empty (or not yet there); then `extract unpack'
Write the two artifacts that travel apart, bound and signed: the DUMP to a path of your choosing (commit it), the BUNDLE to its store. The strategy is the first word: redacted (no machine secrets, for sending) | full (own disaster recovery) | minimized (rescue: loaders, kernel, loader.conf, backups, media). redacted and full describe the database completely and govern the payload only; minimized describes and carries binary management only. The bare form covers every stage
Replay an exported pair into the current database – or, when there is none, into a fresh one (`import bootstrap': the profile from the dump, the signer from the keyring); checked end to end BEFORE anything lands, cheapest first, every detail read ONCE on the way in and handed down: the dump’s serial, the pinned signer, the dump’s signature (gpg once), the signer’s floor, this database’s serial; then `import <dump> <bundle> <serial> <key> <fpr> <floor> <cur>' – the seal (this bundle is the one the dump names), the bundle unpacked into import’s own scratch under incoming/, its MANIFEST (pinned signer, every hash), the receipt filed BEFORE the replay (the replay runs keep-going, so a redacted pair’s withheld elements cost no receipt), the replay. ONLY into an EMPTY database (a fresh bootstrap with the attest key pinned): on a database that already holds records the replay collides with them – an identical add is a no-op, a differing one is refused, an imported file replaces the one it names, the work link of the source displaces a worktree of your own
Import into a database that is not there (no .env layer under ELEBAKE_BASE): the dump is readable and signed, names its profile (`# Profile:' in its header) and carries the openpgp record of its signer (`openpgp add <name> <keyid> [<gnupghome>]'); the signature is verified against the keyring that record names – an absolute gnupghome that exists here, else YOUR default keyring (a gnupghome inside the database travels in the bundle and serves after the import, not before); then `import bootstrap <dump> <bundle> <profile> <record> <fingerprint> [<gnupghome>]'. The trust is the keyring’s: the first signer a fresh database meets is one whose public key you hold
Hash every entry of a collection into a manifest, same format as the boot manifest (LC_ALL=C-sorted `path sha256=hash', symlinks as `path symlink=target'): the collection is readable, the manifest is named MANIFEST (import looks for it by name, next to the collection), every entry is hashable; then `manifest write' – everything hashed at generation, the emission is one concrete heredoc write
Write the manifest for a collection and sign it, side by side with the collection: manifest + attest
Verify a manifest, both judgments at generation time: the signature (attest verify: signed by the PINNED key, key neither expired nor revoked) and the tree (manifest match: every listed entry present and identical) – a finding fails the batch before restore replays anything. The key is the RECEIVER’s openpgp record naming the signer it expects
Does the tree under <base> match the manifest? The manifest is readable and lists entries, the base exists; then `manifest tree matches' – every listed file hashes identically, every listed symlink points where recorded (MISSING, CHANGED, RETARGETED fail). Files present but unlisted are not findings: the base is an extraction directory
Advance the export serial by one: the number read here, then `provenance serial <current>' – export does this LAST, once the pair is attested: the dump header already carries current+1, so a failed export leaves the number to the next attempt (serials were once lost to a pinentry that could not open)
File the receipt of an admitted import. Every detail read ONCE on the way in and handed down – the dump’s serial, the pinned key, the signer (gpg once), the signer’s floor, this database’s serial – then `provenance add <dump> <bundle> <serial> <key> <fpr> <floor> <cur>': the bundle readable or `-', the serial not a downgrade, `provenance receipt' (files it, or says it is filed, or refuses a differing one) and the export serial raised to the imported one
Copy ONE file of a receipt record from another database (dump/restore base element; the record directory is created on first file): the id is a record name, the source exists; then `provenance import place'
Emit the provenance portion of a database dump: one `provenance import' line per receipt file (cat-pinned: dump TEXT, replayed by restore); none is a comment line
Text terminal: the receipt record files that belong into an archive, by their real path against “$ELEBAKE_ARCHIVE_BASE”
The export serial and every receipt: the serial read here, then `provenance list <serial>' renders

Key registries. Records hold REFERENCES (paths, URIs, key ids) – never private material; custody stays with the token, keyring, or root file.

Register a GnuPG attest key; gnupghome for a keyring living elsewhere: the name is a record name, the keyid is hex; then `openpgp record'
Emit the openpgp portion of a database dump: one `openpgp add' replay line per registered key, in the arity of the record (cat-pinned: dump TEXT, replayed by `restore'), plus one `openpgp import' per extra file; none is a comment line
Copy ONE extra file into the key record – dump/restore base element (the schema files travel as the `openpgp add' replay): rewrite to `key import openpgp <name> <absfile>'
List the files of the openpgp key records that belong into an archive – public material only; private key material stays a path promise into the world: one `openpgp collect <key>' per record, none is a comment line
Register a file-based signing key by its paths (for sites without a token): the name is a record name; then `pem record'
Emit the pem portion of a database dump: one `pem add' replay line per registered key (cat-pinned: dump TEXT, replayed by `restore'), plus one `pem import' per extra file; none is a comment line
Copy ONE extra file into the key record – dump/restore base element (the schema files travel as the `pem add' replay): rewrite to `key import pem <name> <absfile>'
List the files of the pem key records that belong into an archive – public material only; private key material stays a path promise into the world: one `pem collect <key>' per record, none is a comment line
Register a PKCS#11 token key, its URI and certificate path: the name is a record name; then `pkcs11 record'
Emit the pkcs11 portion of a database dump: one `pkcs11 add' replay line per registered key (cat-pinned: dump TEXT, replayed by `restore'), plus one `pkcs11 import' per extra file; none is a comment line
Copy ONE extra file into the key record – dump/restore base element (the schema files travel as the `pkcs11 add' replay): rewrite to `key import pkcs11 <name> <absfile>'
List the files of the pkcs11 key records that belong into an archive – public material only; private key material stays a path promise into the world: one `pkcs11 collect <key>' per record, none is a comment line

A stage is one named workspace for one boot tree: bind keys, check out a worktree, embed trust, build isolated, curate, sign, attest.

List all stages with their derived state (one line each): name, id, populated, signed (STALE = loader changed after signing), sign-key/attest-key
Create a named stage (workspace for one boot tree): the name is a record name; then `stage add new' – IDEMPOTENT, an existing stage is left untouched (a replayed dump must never mint a fresh id and bend the name symlink)
Edit a file of the stage’s boot tree (the single source of truth; publish with stage push): the stage exists, the file is in boot/, the editor is pinned; then `stage edit open'
Bind the loader-signing key slot (backend: pem | pkcs11): the stage exists, the key record is registered; then `stage sign key bind'
Bind the attest key slot (openpgp) – required BEFORE build, its anchor is embedded: the stage exists, the key record is registered; then `stage attest key bind'
Clear both key slots: the stage exists; then `stage unkey clear'
Add one absolute bootfs path to the stage’s EXIST or VERIFY prerequisites (idempotent append); `-' reads paths from stdin, one per line, each validated before anything is emitted. `-': rewrite to `stage prerequisites add stdin <stage> <kind>', else to `stage prerequisites add entry <stage> <kind> <path>'
Remove one path from the stage’s EXIST or VERIFY prerequisites: the stage exists, the kind is valid, the path is listed; then `stage prerequisites remove'
Show one of the stage’s prerequisites lists: the stage exists, the kind is valid; then `stage prerequisites table'
Add one absolute bootfs path to the stage’s EXIST prerequisites; `-' reads paths from stdin – rewrites to `stage prerequisites add <stage> exist <path|->'
Remove one path from the stage’s EXIST prerequisites – rewrites to `stage prerequisites drop <stage> exist <path>'
Show the stage’s EXIST prerequisites – rewrites to `stage prerequisites show <stage> exist'
Add one absolute bootfs path to the stage’s VERIFY prerequisites; `-' reads paths from stdin – rewrites to `stage prerequisites add <stage> verify <path|->'
Remove one path from the stage’s VERIFY prerequisites – rewrites to `stage prerequisites drop <stage> verify <path>'
Show the stage’s VERIFY prerequisites – rewrites to `stage prerequisites show <stage> verify'
Show the stage’s derived state (nothing cached – read from the artifacts): the stage exists; then the title and one line per aspect, each its own terminal
Authenticode-sign boot/loader.efi with the bound sign-key: the stage exists, the slot is bound; then `stage sign backend' lifts the record’s backend into the command (pem: uefisign, pkcs11: osslsigncode)
Authenticode-sign boot/loader.efi with the bound file-based key: the backend’s tools, the stage exists and is populated, the record carries key and cert; then `stage sign pem run' (uefisign)
Authenticode-sign boot/loader.efi with the bound pkcs11 key: the backend’s tools, the stage exists and is populated, the record carries uri and cert; then `stage sign pkcs11 run' (osslsigncode through the engine or provider bridge, PIN prompted hidden on the tty and handed over via a transient file – never argv, never env)
Detach-sign the stage manifest with the bound attest key: gpg is there, the stage exists, the attest-key is bound and complete, the manifest is there; then `stage detachsign' (armored detached signature boot/manifest.asc, exactly what the loader verifies against the anchor)
Add a DETACHED git worktree of ELEBAKE_FREEBSD_SRC at <ref> (a snapshot, never a branch to commit on) and point the stage at it: the toolchain is there, the stage exists, the source repo is set; then `stage worktree'
Replace the stage’s worktree by a fresh DETACHED one at <ref>: the toolchain is there, the stage exists and is checked out, the source repo is set; then `stage worktree remove' (git worktree remove –force and prune, the work link), then `stage worktree'. The old worktree holds generated files only – site.mk, secret.mk, foundation.c – which stage site mk and stage foundation make write again; the build outputs live in destdir/ and obj/ of the stage
cleandir the worktree’s stand/ build and reset the stage’s build outputs: the stage exists and is checked out; then `stage clean dir' and `stage reset'
The isolated stand/ build: the trust anchor in the worktree (stage trust) first – the cheapest check, local to the stage – then foundation.c rendered from the stage’s bindings (stage foundation make: what the owner bound is in the binary, a stage that binds nothing gets empty tables and a loader that boots like stock), then site.mk where the stage has baselines (stage site present: a loader compiled without it takes every expectation as none and measures nothing), then the toolchain, the build gate (keys bound), clean, then one `stage build stand <stage> <component>' per curated component
BUILD the stage: build (gate, clean, stand), install, include, sign – publishing is stage push. Needs executing interpreter pins on the terminals underneath, otherwise every step only displays (see the environment topic)
Unprivileged stand/ DESTDIR install into destdir/: one `stage install <stage> <component>' per component of ELEBAKE_STAND_INSTALL_SUBDIRS (curated policy); an unset list is an error line
Build the kernel from the stage’s checkout – the source delivers EVERYTHING, the filter selects: the stage exists and is checked out, ELEBAKE_KERNCONF is set (no implicit default: the guarding kernel is a decision) and exists in the checkout; then `stage build kernel run' (isolated per-stage obj)
Unprivileged installkernel into the stage’s destdir (destdir/boot/kernel becomes selectable by the filter): the stage exists and is checked out, ELEBAKE_KERNCONF is set, the obj tree exists; then `stage install kernel run'
The kernel of the stage’s boot tree onto the running root – the medium, the rescue (stage rescue kernel mirror) and the root then run one kernel with one set of modules, wherever the stage took it from (stage include /boot: the package; stage build kernel: the fork): the stage exists, boot/kernel/kernel is in it, the root is a boot environment (bectl); then `stage kernel backup' (a boot environment pre-kernel-<UTC> of the root as it is: the rollback) and `stage kernel copy' (boot/kernel of the stage over /boot/kernel, root:wheel). A reboot runs the new kernel; `stage kernel rollback' activates the saved environment
Back to a root before a kernel: the stage exists, the boot environment is listed (bectl); then `stage kernel activate' – the next boot runs that environment with its kernel. The stage’s boot tree and the medium stay as they are: the loader boots what the medium carries, the root’s modules are the environment’s
The kernels as notes: the version line of the stage’s boot/kernel/kernel, of the running root’s /boot/kernel/kernel, and the boot environments bectl lists (the pre-kernel-* ones are the rollbacks of stage kernel install): the stage exists; then `stage kernel lines'
Ingest an ALREADY-BUILT external loader as boot/loader.efi (sign it without building): the stage exists, the source file exists; then `stage loader ingest'
Generate the REAL veriexec manifest over boot/ (path sha256=hash, LC_ALL=C-sorted, every file except the manifest pair; loader.efi included on purpose – it is the disaster reserve, and an unverified reserve is not a reserve): the stage exists and is populated; then `stage manifest write' – the WHOLE manifest is computed at generation, the emission is one concrete heredoc write
Manifest consistency BOTH ways, checked at generation time: the stage exists, the manifest is there; then `stage verify listed' (every listed entry exists and hashes identically) and `stage verify unlisted' (every file in boot/ is listed)
Root of trust into the worktree, in one step: `stage trust anchor' exports the bound attest key (its public key and a self-test signature) into lib/libsecureboot, `stage trust mk' writes site.trust.mk – the veriexec anchor the loader is BUILT with, so this runs after `stage attest key' and before `stage build'. Assumes: the attest key is RSA (the loader’s BearSSL verifies RSA only) and its keyring is readable: the shipped profiles pin the anchor export to sudo sh (the keyring lives under /root), so the batch asks for the password by itself
Export the attest key + self-test signature into the worktree’s libsecureboot: the stage exists and is checked out, the attest key is bound and complete; then `stage trust anchor export'
Write site.trust.mk (elevated veriexec config) into the worktree: the stage exists and is checked out; then `stage trust mk write'
The curated list as it stands: the stage exists; then `stage filter table'
What the source directory holds at top level that the filter does not cover: the stage exists, the source is absolute; then `stage filter uncurated table'
What lies in boot/ that the filter does not cover and no step generates: the stage exists; then `stage filter orphaned table'
Emit the removal of every orphaned entry of boot/ (what `stage filter orphaned' lists: neither curated nor generated) – one rm per entry, displayed by default so the owner reads the list before piping it to sh. After `stage adopt' this is how a medium’s stale files (BIOS boot blocks, forth, old loader variants) leave the stage: the curation says what belongs, prune removes the rest. Nothing curated or generated is ever touched. The stage exists; then `stage filter prune rm'
Curate one destdir/boot-relative path into the boot/ list (idempotent append); `-' reads a FROZEN snapshot from stdin, one path per line – a directory entry is a LIVING curation (new files under it travel along), the snapshot a frozen one. `-': rewrite to `stage filter add stdin <stage>', else to `stage filter add entry <stage> <rel>'
Remove one entry from the boot/ curation list: the stage exists, the entry is listed; then `stage filter remove'
ONE view, the whole truth, as three tables: the curated list, what sits uncurated in the source (default: the stage’s own destdir/boot), what lies orphaned in boot/
Work the curated filter off from a chosen SOURCE directory (default: the stage’s own destdir/boot; e.g. /boot takes the same selection from the host’s binaries): the stage exists, the source is absolute and present, the filter is not empty, every entry is in the source or already in boot/; then `stage include copy'. Per entry: present in the source -> copied into boot/ (directories replaced whole); absent from the source but present in boot/ -> kept and noted (an adopted file the build does not deliver, e.g. loader.conf after stage adopt); absent from both -> refused before anything ran
Import ONE base element from another database: two args declare a record-tree DIRECTORY, three copy a file/symlink into it – an unconditional check-then-act sequence; the logic lives in the dump that emits these lines. Three args: the stage exists, the subdir is record-relative and declared, the source an absolute existing file or symlink; then `stage import file'
Import a whole subtree of the record from another database: the stage exists, the target is record-relative (never the record root), the source is an absolute directory; then `stage import tree copy' – the boot tree of a stage is one line, not one per file (the rescue probe: ~1100 lines, ~3000 processes)
Fan out to every stage: one `stage collect <stage>' per name symlink; none is a comment line
Publish the stage: the description checked (stage medium ensure), no freeze mark standing (stage freeze check: a finding of the narrow gates waits for elvboot freeze ack), manifest, attest, verify, the signed loader present; then the ESP mounted read-write and the card proven to be this medium by the letter it carries (stage medium mount: the wrong card is refused before the first write, a fresh card gets its letter with `stage medium stamp' first), the tree and the loader written (stage medium write), the medium released
The stage part of a dump: every stage in the complete vocabulary. The strategy selects the anchor that knows its blocks (dispatch): `stage dump complete <stage>' is the full description, `stage dump minimized <stage>' the rescue subset
Regenerate what a dump does not move – obj/ (stage build) and destdir/ (stage install) – where the worktree is; without one (a restored record: the work link dangles) a log line names the way: stage checkout <stage> <commit>, stage trust <stage>, then stage rebuild <stage>

The machine-bound trust expectations: the NVRAM boot marker and the compiled-in baselines (local/site.mk). What turns a generic loader into THIS machine’s tamper detector.

Store one build-provided value of the stage: a LOADER_TRUST_* macro the loader sources consume, rendered into site.mk by `stage site mk' (digest: 64 hex -> byte list; string: C string; int: bare). Immutable: an identical re-add is a no-op, a differing one is refused (drop first). This is where gate slots (gate add) and provider windows get their values; the dump replays every record
Remove one build-provided value of the stage: the stage and the record exist, then it is erased
Show the stage’s build-provided values (or one) as the -D lines site mk would render: one `stage baseline show <stage> <MACRO>' per record; none is a note
Take a digest baseline from THIS machine’s kenv when the batch runs – the value a trusted boot’s loader published (loader.trust.<gate>.<leaf>, 64 hex) – and record it as `stage baseline add <stage> <MACRO> digest <value>'. For the witnesses userland cannot compute (loaded EFI images, ACPI tables, EFI variables, PCI devices, PCR bank, soft PCR, guarded kenv). Assumes: the boot that published the value was the owner’s own, on the intended firmware – learning on a tampered platform bakes the tampering in
Record one internal GPT partition (nda0p1) whose GELI metadata sector and whose disk’s partition table the loader measures (measure_geli, measure_gpt); `stage site mk' hashes them when it runs. Order of the records = order of hashing. Assumes: the partitions are GELI providers on GPT disks the firmware exposes via Block I/O; a legitimate setkey/delkey or repartitioning moves the digest – re-run site mk afterwards
Remove one partition from the stage’s disks record: the stage exists and lists it, then the line is removed
List the stage’s recorded partitions as notes; none says so
Record one integrity witness of the stage: a GELI container the loader reads but never opens – the rescue card (da1p3 as zcard-b; a second card in the same slot is its own witness, zcard-a: the medium of the first receipt, the sha256 of the card’s GELI metadata sector, tells them apart even when the cards are dd clones with one GPT uuid; measure and check skip the card that is not in the slot), the quarantine (nda1p1 as zempty). Its four ZFS vdev labels and its GELI metadata sector, hashed in the ciphertext, say whether anything was written into it since the rollout (every transaction group writes a new uberblock into the labels). The stage exists, the name is a record name and the partition a GPT partition name; then written under witness/<name> (immutable: the same partition again is a note, another one is refused – drop first). `stage witness measure' takes the hash when it runs (root) and leaves the receipt `stage witness conf' puts into loader.trust.conf; `stage witness check' compares a container with its last receipt
Remove one witness of the stage and its receipts: the stage exists and records the witness; then `stage witness unlink'
The witnesses of the stage as notes – name, partition, the last receipt (when, GPT uuid, medium, digest) and the last whole-card receipt when there is one: the stage exists; none says so. The digest line is what goes on paper and into the public commit at a rollout: the one witness outside the machine
The anchor of the witness table in the TPM (the third witness): the stage exists and names the index (kenv loader.trust.tpm.witness.nv) and the PCR list (loader.trust.tpm.keyfile.pcrs), the RAM disk is mounted, the hierarchy password is on it (the write policy names the owner hierarchy, PolicySecret: the password from the seed, never on the running machine), every witness has its receipt and the conf records stand (loader.trust.inventory.witnesses); then `stage witness anchor write' defines the index once and writes the digest. After every rollout of a witness: measure, conf, anchor. The loader’s claim WitnessAnchor and elvbootd’s witness_anchor_act compare against this index; a conf rewritten on a medium by whoever holds the signing key carries a table the index does not
Measure every witness of the stage when the batch runs (raw device reads: the act is root) – the four vdev labels and the GELI metadata sector of each container in the ciphertext, one sha256 per witness – and append a receipt (UTC time, GPT uuid, digest) under witness-receipts/<name>: the stage exists and records at least one witness; then one `stage witness labels' per witness. Measure at the rollout, with the container closed; `stage witness conf' puts the last receipts into the kenv records the loader reads, `stage witness show' prints them for paper and commit
The whole-card receipt of every witness of the stage, when the batch runs (root; minutes per card: the entire disk is read): sha256 over the disk the witness partition lives on – GPT, every partition, the free space after the last one – against which a later `stage witness check full' tells any byte written anywhere on the card, inside or outside the containers. The stage exists and records witnesses; then one `stage witness whole' per witness. A witness whose medium is not in the slot is skipped like `stage witness measure'. The loader never reads this receipt; the report does, and so does the owner on suspicion
Compare the whole card of every witness with its last whole-card receipt when the batch runs (root; minutes per card): the stage exists and records witnesses; then one `stage witness compare whole' per witness – unchanged, or CHANGED with both digests (exit 1): a byte written anywhere on the card since the receipt, inside or outside the containers
Compare every witness of the stage with its last receipt when the batch runs (root): the stage exists and records at least one witness; then one `stage witness compare' per witness – unchanged, or CHANGED with both digests (exit 1). The probe after a rescue boot: a changed card means the boot imported the pool read-write; then the witness is worth nothing until the import is read-only
The witnesses into the stage’s kenv records, from the last receipts: loader.trust.inventory.witnesses (the names) and loader.trust.inventory.witness.<name>.guid, .medium and .digest – what the loader reads from loader.trust.conf (WitnessLabels): the stage exists, it records witnesses, every receipt carries a medium; then the records rewritten (erase, add). Arm them with `stage loaderconf mk', `stage include' and `stage push': the conf travels in the signed manifest, no rebuild – a rollout of a card is measure, conf, loaderconf mk, include, push
Store one loader.trust.* kenv value of the stage (dumb store, immutable: identical re-add is a no-op, a differing one is refused); `stage loaderconf mk' writes every record into boot/loader.trust.conf, `stage require' says which the bound actions demand. Values that are secrets (hashes) belong here only as HASHES; the medium carries this file in clear, covered by the manifest
Set a string baseline from a HIDDEN line: the word is read twice on the terminal when the batch runs, both must match, and PBKDF2-HMAC-SHA512 over it – salted with the first eight bytes of the stage’s LOADER_TRUST_RECORD_SALT, 100000 rounds, 32 bytes, hex: what the loader derives with GELI’s pkcs5v2 and compares – becomes the string baseline <MACRO> of the stage. The one use today: LOADER_TRUST_UNLOCK_SECRET, the passphrase unlock_act asks for the informed decision on a deviation; the hash opens nothing and tells no second role apart, so it may live in the signed loader (the three-factor design), and the rounds make a card thief’s guessing slow. The word reaches neither argv, history nor the batch text. The record salt must stand first (stage baseline add LOADER_TRUST_RECORD_SALT). Baselines are immutable: stage baseline drop <stage> <MACRO> first to change it
Remove one loader.trust.* value of the stage: the stage and the record exist, then it is erased
Show the stage’s loader.trust.* values (or one) as the lines loaderconf mk would write: one `stage kenv show <stage> <key>' per record; none is a note
Take a value from THIS machine’s kenv when the batch runs – what a trusted boot’s loader published – and record it as `stage kenv add <stage> <key> <value>': the way a key expectation (expectation add ... key ...) gets its value, e.g. loader.trust.kernellock.pcr.expected from loader.trust.kernellock.pcr.sha256. The loader reads it from loader.trust.conf, so learning changes the conf (stage loaderconf mk, include, sign, push), never the binary. Assumes: the boot that published the value was the owner’s own, on the intended firmware – learning on a tampered platform bakes the tampering in
Report what the stage’s BOUND loader policies demand: the loader.trust.<gate>.<leaf> keys the actions read at boot (parsed from the kenv(a, ...) calls in the checkout’s action.c, attributed per gate), each with its kenv record or MISSING; the gate-free leafs the boot itself reads (template/tbl/boot-leafs.tbl: the TPM key file, the GELI dialog); then the LOADER_TRUST_* baselines the measurements and actions consume without a code default, each ok or MISSING. With a container argument: the demands of that container’s bound measurements on the boot tree
Write boot/loader.trust.conf of the stage from its kenv records: the stage and its checkout exist, boot/loader.conf names the file in loader_conf_files (stage edit adds it), every leaf a bound action reads and every required boot leaf (boot-leafs.tbl) has its value (stage require), then the file is written. It rides on the medium in clear, the manifest covers it (stage manifest after this)
Regenerate loader.trust.conf from the kenv records and compare with the stage’s boot/loader.trust.conf: drift per key is reported, agreement is one line – the conf under tamper detection
Fetch the per-boot lists elvbootd filed under /var/db/elvboot/inventory (root’s, written by inventory_record_act in STARTUP) into the stage’s inventory/records/, owned by the database’s owner: the stage exists, then the copy (pinned sudo sh). Run it after every boot whose lists you want to compare
Lay the imported records side by side for one kind (acpi | efivars | images): per item its identity, size, for efivars the attributes in words, in how many boots it appeared, whether every boot saw the same digest (same) or not (MOVES), whether it is in the set (+), and the digest of each boot, oldest first. What moves is what the firmware rewrites: leave it out of the set
Take one item into the set of a kind: the stage exists, the kind is acpi, efivars or images, the entry has the identity form (<a>/<b>, letters, digits, _ . -), the newest imported record lists it (the loader saw it – stage inventory import first), then it is appended to inventory/<kind> unless already there. The set is what AcpiTables / EfiVariables measure; after a change: stage site mk, build, boot, then learn the digest
Take into the set every item that every imported record lists with the same digest – what held across all boots – one `stage inventory add' per item: the stage exists, the kind is valid, at least two records are imported (one boot proves nothing), then the stable entries (stage inventory stable). What moved stays out; the loader itself (file/BOOTX64.EFI) is stable too and is dropped by hand when the set must not include it
Take one item out of the set of a kind: the stage exists, the kind is valid, the entry is in the set, then its line is removed from inventory/<kind> – the set’s digest changes with it: after site mk, make and one boot, drop and learn LOADER_TRUST_<KIND>_DIGEST from loader.trust.inventory.<kind>.sha256 (stage baseline learn), or the kind’s claim keeps failing against the old baseline
The set of a kind as the stage has it, one entry per line: the stage exists, the kind is valid, then the lines of inventory/<kind> (a note when empty)
Render the sets of the stage as site.mk lines, one `stage inventory make set <stage> <kind>' per kind: LOADER_TRUST_ACPI_SET, LOADER_TRUST_EFIVARS_SET and LOADER_TRUST_IMAGES_SET, the entries sorted and comma-joined – the order the loader hashes the members in – and LOADER_TRUST_EFIVARS_KNOWN, every variable any record ever listed (the foreign count of EfiVarsForeign skips them). A part of stage site mk; an empty set renders nothing, the claim is then skipped
Forget the build root record of the stage (the dataset itself stays): the stage exists and has the record; then it is removed
Record the name of the container’s pool (zempty): the stage exists, the name is a ZFS pool name; then written (immutable, drop first to change)
Forget the pool name record of the stage: the stage exists and has the record; then it is removed
Record the container’s partition (nda1p1, an internal disk of the machine): the stage exists, the name is a GPT partition name; then written (immutable, drop first to change)
Forget the partition record of the stage: the stage exists and has the record; then it is removed
Record the name of the TPM’s file on the RAM disk (zempty.bin) that opens the container alone – the seed writes it there before init, open and push (workflow quarantine): the stage exists, the name is a file name without a slash; then written (immutable, drop first to change)
Forget the key file name record of the stage: the stage exists and has the record; then it is removed
Record the swap device of the quarantine’s fstab (/dev/gpt/illyria-swap.eli: the production’s swap, geli onetime, usable by a system that holds no key of its own): the stage exists, the device is an absolute path; then written (immutable, drop first to change)
Forget the swap device record of the stage: the stage exists and has the record; then it is removed
Record the show user of the quarantine (brj): the one account its login prompt offers – created by the build without a group, so neither wheel nor operator, with an everyday password read hidden; the stage exists, the name is a login name; then written (immutable, drop first to change)
Forget the show user record of the stage: the stage exists and has the record; then it is removed
Add a line of the quarantine’s /etc/rc.conf (name=“value”: hostname, zfs_enable, sshd_enable=“NO”, kern_securelevel=“2”, devmatch_enable=“NO”, dumpdev=“NO”, tmpmfs=“YES”, varmfs=“YES” – the whole configuration, the quarantine mirrors no rc.conf): the stage exists, the line has the form; then appended once
Remove a line from the quarantine’s rc list: the stage exists, the line is listed; then it is removed
The quarantine description of the stage as notes: dataset, pool, partition, key file name, show user, swap, the rc lines, the last snapshot and what the container received; the stage exists
Mount the build root for editing under $ELEBAKE_ROOT/mnt/<stage>-quarantine (mount -t zfs, so the mountpoint property stays /), devfs below it: the stage exists, the dataset is recorded, nothing is mounted there yet; then `stage quarantine dataset mount'
Unmount the build root and its devfs: the stage exists, the root is mounted; then `stage quarantine dataset umount'
Snapshot the quarantine root (<dataset>@<stage>-<stamp>, recursive) and record the name: the stage exists, the dataset is recorded, the root is not mounted (a quiescent tree), the baseline is signed; then `stage quarantine snapshot take' and the record
Forget the source dataset record of the stage (the dataset itself stays): the stage exists and has the record; then it is removed
Record the name of the pool every card carries (zcard): the stage exists, the name is a ZFS pool name; then written (immutable, drop first to change)
Forget the pool name record of the stage: the stage exists and has the record; then it is removed
Take one package into the rescue system’s tool list: the stage exists, the name is a package name (pkg(8)); an entry already listed is a note
Take one package out of the tool list: the stage exists and lists it; then the line is removed
Take one absolute path into the mirror list (a file or a directory of the production root, copied as it is by stage rescue config mirror): the stage exists, the path is absolute; an entry already listed is a note. Nothing here is decrypted, and nothing that cannot be revoked belongs here: the card carries keys with their own passphrases, never the seed (seed.gpg stays on the production root)
Take one absolute path into the transient list – a directory of the rescue root that gets a writable layer in memory at every boot (tmpfs over the read-only directory, unionfs): the stage exists, the path is absolute; an entry already listed is a note. The root itself is read-only on the card (stage rescue push), /tmp and /var are memory disks; what is written in a session is gone at the next boot
Take one path out of the transient list: the stage exists and lists it; then the line is removed
Take one path out of the mirror list: the stage exists and lists it; then the line is removed
The rescue description of the stage as notes: dataset, pool, package, key file, tools, config, local lines, sources, users, the cards, the patches, the last snapshot and what each medium received; the stage exists
Mount the rescue root for editing under $ELEBAKE_ROOT/mnt/<stage>-rescue (mount -t zfs, so the mountpoint property stays /), devfs below it: the stage exists, the dataset is recorded, nothing is mounted there yet; then `stage rescue dataset mount'
Unmount the rescue root and its devfs: the stage exists, the root is mounted; then `stage rescue dataset umount'
Copy every listed path from the production root into the mounted rescue root, as it is (owner, mode, an encrypted file stays encrypted): the stage exists, the root is mounted, the list is not empty; then `stage rescue config copy'
Snapshot the rescue root (<dataset>@<stage>-<stamp>, recursive for any child) and record the name as the stage’s rescue snapshot: the stage exists, the dataset is recorded, the root is not mounted (a quiescent tree), the baseline is signed (the build’s last step – a token that refused at the end of a long run leaves none, and an unsigned image must not reach a card); then `stage rescue snapshot take', then the record
What the machine holds against the description: the stage exists; then `stage rescue status show' (the dataset tree and its snapshots as zfs lists them, the mount point, the description as notes)
Record which load option carries the marker and where its value file lives (bookkeeping only – no NVRAM): the stage exists, the load option and the absolute path are well-formed; then `stage marker store'
Write the marker into the recorded load option: `restore' puts the value from the recorded file back (the file is read when the privileged act runs; empty or unreadable is an error there), `new' always generates. The marker is recorded: rewrite to `stage marker value <restore|new> <stage>', else an error line
Write the boot-entry marker into that load option: a batch of two – the RECORD (bootvar + value-file path, bookkeeping, acts) and the WRITE (NVRAM byte surgery, inspect-by-default – pipe the output to sudo sh). The value comes from the file, or is generated when the file is empty
Rotate the marker: a NEW value into the recorded load option and value file. The marker is recorded: rewrite to `stage marker write <stage> new' (inspect-by-default, pipe to sudo sh), else an error line
Write local/site.mk of the stage’s checkout: the stage and its checkout exist; the header, then the measured parts (board, keys, marker, origin, disks) and the baselines appended in file order to site.mk.new; the install (rendered, clean, placed) and the report. Run under sudo: the key store, the marker and the origin are EFI variables
Show the written site.mk of the stage (default: local/site.mk of its checkout) as comment lines
Make the storage key the loader’s sessions salt to persistent under loader.trust.tpm.key.handle: the stage exists, the leaf is set, the work directory is a mounted RAM disk; then `stage tpm key make' (createprimary from the owner seed – the same key every time on the same TPM – evictcontrol, readpublic: the name’s digest is what stage baseline learn takes from loader.trust.tpm.key.sha256 after the first boot)
The RAM disk of the database (ELEBAKE_TPM_WORKDIR, default .ram under the database) is mounted: a comment line; else rewrite to `ram mount'. What the tpm and rescue families keep there – secrets, auth hashes, session contexts, the rescue passphrase – never touches a disk
The RAM disk is mounted: rewrite to `ram release' (every file overwritten and removed, the tmpfs unmounted, the directory gone); else a comment line. The check compares the physical path: mount(8) prints the resolved path, a database behind a symlink is still found
Write the two policies the sealed objects are created under, on the RAM disk: owner.policy (PolicyPCR over loader.trust.tpm.keyfile.pcrs, PolicyAuthValue, PolicyNV duress.count.nv == duress.count.sealed, authorized by the index itself), duress.policy (PolicySecret against duress.nv, PolicyPCR). First the checks (stage tpm policy check), then the operands, then one trial session per role; the trial PolicySecret counts the PIN index, so pinCount is written back to 0 at the end
Set the owner and the lockout password of the TPM to the 32 bytes <workdir>/auth-hierarchy.hex spells, once, from empty: the work directory is a mounted RAM disk, the file is there; then `stage tpm hierarchy set'. Every later act that defines, evicts or writes as the owner passes the file; the boot path needs none of it (the loader and earlboot authorize through the indices themselves). Lost: the firmware’s TPM clear, then everything anew
Seal the secret into the persistent object of the role (refused while a freeze mark stands: stage freeze check): owner – the first handle of loader.trust.tpm.keyfile.handles, under owner.policy, with the owner passphrase’s sha256 as auth value (read hidden twice); duress – the second handle, under duress.policy, no auth value (the PIN index proves the passphrase). The stage exists, the role’s prerequisites hold (stage tpm seal check role), the passphrase is read where the role has one, the object is created, loaded, persisted. The owner’s secret is the production root’s key file; the duress one is the decoy root’s key file
Define the increment-only counters: loader.trust.tpm.counter.nv (the loader raises it when the duress object opened) and, when the stage names it, loader.trust.halt.nv (earlboot raises it before a shutdown it was bound to; the loader’s HaltQuiet claim reads it against the learned halt.expected): the stage exists, the counter leaf is set, the work directory ready; then `stage tpm counter make' (policy = PolicyCommandCode NV_Increment, attributes nt=counter|policywrite|authread|no_da: anyone reads the number, nobody lowers it – the trace of a duress boot, of a halt)
Define the duress index loader.trust.tpm.duress.nv, a TPM_NT_PIN_PASS index whose authValue is the duress passphrase’s sha256 (the same form the objects take): the stage exists, the leaf is set, the work directory ready; the passphrase is read hidden twice (stage tpm seal read <stage> duress, auth-duress.hex/.bin on the RAM disk – stage tpm policy needs it for the trial PolicySecret), then `stage tpm duress make'. The index reads pinCount || pinLimit; every duress unseal (PolicySecret) counts pinCount up, and pinLimit 0xffffffff never spends it: the decoy opens again and again, the count is the owner’s witness (stage tpm status). The seal’s death is the second counter’s business (duress.count.nv, raised by the loader on the first duress unseal). After a duress event: stage tpm duress reset (the count read, then back to 0), then stage tpm seal again
After a duress event (the owner is back with the recovery slot): write pinCount 0 pinLimit 0xffffffff into loader.trust.tpm.duress.nv as the owner, after the count was read (stage tpm status: how often the decoy opened); the second counter is not touched (a counter never goes back: stage tpm seal reads its new value). The stage exists, the leaf is set; then `stage tpm duress reset write'. Then, in this order: stage kenv drop <stage> loader.trust.tpm.duress.count.sealed (records are immutable, the old value would refuse the new one), stage tpm duress count (reads the counter’s new value), stage tpm duress count record, stage tpm seal read <stage> duress (the trial PolicySecret needs the passphrase), stage tpm policy, stage tpm seal for both roles, the probes; then loaderconf mk, include, push (the leaf rides on the medium)
Define the second counter loader.trust.tpm.duress.count.nv, the one earlboot raises for a boot answer of the coercion class (duress_count_act, PolicyCommandCode NV_Increment, authread so the loader’s PolicyNV reads it through the index itself), incremented once after the define (a fresh counter reads TPM_RC_NV_UNINITIALIZED, and its first value is the TPM-wide maximum + 1, never 0); its value written to <workdir>/count-sealed.hex for `stage tpm duress count record'. The stage exists, the leaf is set, the work directory ready; then `stage tpm duress count make'
Take the counter’s value stage tpm duress count left in <workdir>/count-sealed.hex (16 hex characters) and record it as loader.trust.tpm.duress.count.sealed: the stage exists; then `stage kenv add' with the value (stage tpm duress count sealed). The loader reads it from loader.trust.conf (stage loaderconf mk, include, sign, push after this); stage tpm policy and stage tpm seal owner take it from the record. Run again after every increment of the counter (a duress event of the fish path) – after `stage kenv drop <stage> loader.trust.tpm.duress.count.sealed': the record is immutable, a differing value is refused
Define the two time-anchor indices: loader.trust.tpm.anchor.nv, which the loader writes at record commit under PolicyPCR over the cap PCR in its BOOT state and then caps (one PCR extend: root at runtime cannot rewrite it), and loader.trust.tpm.shutdown.nv, which elvbootd writes at shutdown under PolicyPCR over the CAPPED state (so only the runtime after the loader’s cap can write it; a shutdown without the hook leaves an old index and SmartStep falls): the stage exists, the three leafs are set, the work directory ready; then `stage tpm anchor make'. Threat model assumed: wall time is the RTC, which anyone with the setup can set; the anchor makes a forged RTC agree with the TPM clock and the NVMe counters, which only grow. No secret is involved: the anchor carries an HMAC under the record material, the shutdown index a plain digest
Unseal the role’s object the way the loader does – a session salted to loader.trust.tpm.key.handle, the role’s policy replayed – and compare with the secret file: the stage exists, then the role’s probe (stage tpm probe role): its prerequisites (the same as the seal’s), the unseal, and for duress the pinCount written back to 0 (the PolicySecret counted)
Wipe what the family left on the work directory: the auth hashes, contexts, policies, key files (rm -P, the RAM disk is unmounted by the owner); the work directory ready; then `stage tpm wipe'
What the TPM holds against what the stage expects: the persistent handles, the counter’s public area, the two anchor indices; the stage exists; then `stage tpm status show' (no password needed; the PIN index’s pinCount shows only while auth-hierarchy.hex is on the RAM disk)

The recurring sequences of commands, as text to read and to type after: rebuild, pcr-learn, kenv-change, baseline-relearn, inventory-drop, policy-change, tpm-seal, halt-relearn, restore (template/workflow/*.md).

List the workflows: one line per file of template/workflow, name and title (the file’s first line); the same as `workflow list'
List the workflows: per file of template/workflow one `workflow line <name>'; none is a comment line
Print a workflow: the file exists (workflow exists), then its text as it is (workflow render) – commands and the comments between them, to read and to type after

The recurring sequences of commands, as text to read and to type after: rebuild, pcr-learn, kenv-change, baseline-relearn, inventory-drop, policy-change, tpm-seal, halt-relearn, restore (template/workflow/*.md).

List the workflows: one line per file of template/workflow, name and title (the file’s first line); the same as `workflow list'
List the workflows: per file of template/workflow one `workflow line <name>'; none is a comment line
Print a workflow: the file exists (workflow exists), then its text as it is (workflow render) – commands and the comments between them, to read and to type after

The recurring sequences of commands, as text to read and to type after: rebuild, pcr-learn, kenv-change, baseline-relearn, inventory-drop, policy-change, tpm-seal, halt-relearn, restore (template/workflow/*.md).

List the workflows: one line per file of template/workflow, name and title (the file’s first line); the same as `workflow list'
List the workflows: per file of template/workflow one `workflow line <name>'; none is a comment line
Print a workflow: the file exists (workflow exists), then its text as it is (workflow render) – commands and the comments between them, to read and to type after

The physical boot medium: bind it, back up its loader, swap, roll back.

Record the quarantine’s build root dataset (zroot/ROOT/zempty, a boot environment on the production pool whose basename names the root on the container’s pool): the stage exists, the name is a ZFS dataset name; then written (immutable: identical re-add is a no-op, a differing one is refused – drop first)
Create the build root on the production pool, empty: the stage exists, the dataset is recorded; then `stage quarantine dataset create' (mountpoint / and canmount noauto – a boot environment nothing mounts by itself; the base set fills it)
Build the opened quarantine root, in phases: the stage exists, the root is mounted; then `stage quarantine package install' (the base set, no kernel), `rc write' (/etc/rc.conf from the rc lines), `frame write' (fstab, localtime, the console insecure, the root password random), `user bound' (the show user when one is recorded: `user write' reads its password hidden), `guard write' (rc.d geomcheck from the template), `baseline' (the signed manifest of the tree). Then close and snapshot
Create the show user in the mounted quarantine root: the stage exists, the root is mounted, the user is recorded, the RAM disk is mounted; then `stage quarantine user read' (the password twice, hidden, hashed onto the RAM disk) and `stage quarantine user file' (useradd without a group, the hash set, the file removed)
Install the base set into the mounted quarantine root from this machine’s repositories, no kernel (the medium’s, verified, boots it – a disk without kernel and boot code boots nowhere by itself): the stage exists, the root is mounted; then `stage quarantine package pkg'
Write /etc/rc.conf of the mounted quarantine root from the rc lines: the stage exists, the root is mounted, the list is not empty; then `stage quarantine rc file'
Write the frame of the mounted quarantine root: the stage exists, the root is mounted; then `stage quarantine frame file' – fstab (the swap line when a swap device is recorded, nothing else), localtime copied from this machine, the console marked insecure in ttys (single user asks the root password, which nobody knows), the root password set to 48 random bytes nobody keeps (the show user comes with `stage quarantine user write')
Write the guard into the mounted quarantine root: the stage exists, the root is mounted; then `stage quarantine guard file' – /etc/rc.d/geomcheck from the template quarantine/geomcheck of this elebake (after mountcritlocal, before zpool, kld and zfs: every pool but the root pool exported, every GELI provider not in use detached, foreign mounts unmounted, tpm.ko unloaded), checked with sh -n and placed in rcorder
Take the baseline of the quarantine root – the manifest of its tree (every file and symlink, LC_ALL=C sorted, `path sha256=hash' / `path symlink=target') signed with the pinned attest key, into stage/<stage>/quarantine/baseline/MANIFEST(.asc): the stage exists, the root is mounted, the key is pinned; then `stage quarantine baseline write' and `attest'
Prepare the container, once: GELI on the partition (AES-XTS 256, HMAC/SHA256, BOOT, NODELETE, the TPM’s file alone, no passphrase, its own master key), every sector written with zeros (HMAC needs it – a grown provider without this writes integrity failures on every read of the new space), the pool created on it with ROOT below, exported: the stage exists, partition, pool and keyfile are recorded, the RAM disk is mounted and the file on it; then `stage quarantine container geli' and `container pool'
Attach the container with the TPM’s file and import its pool without mounting (altroot $ELEBAKE_ROOT/mnt/<stage>-quarantine-container): the stage exists, partition, pool and keyfile are recorded, the RAM disk is mounted and the file on it; then `stage quarantine container attach'
Export the container’s pool and detach the partition: the stage exists, partition and pool are recorded; then `stage quarantine container detach'
Send the recorded snapshot into the container: the stage exists, the snapshot is recorded, the container opens (stage quarantine container open), the stream goes (the whole tree the first time, the increment since the receipt after), bootfs is set, the root on the container is set read-only (a session leaves nothing), the GUID is proven, the receipt is recorded, the container closes. After it the witness of the container moves: stage witness measure, conf, anchor
Check the container against the baseline without sending, three questions: does it hold the recorded snapshot (GUID) with a read-only root, is the baseline manifest signed by the pinned key, and is the tree in the container exactly the baseline (every entry present and unchanged, nothing beyond it): the stage exists, the snapshot is recorded, the baseline is signed; then attest verify, container open, check, mount, tree check, umount, close
Record the rescue root dataset (zroot/ROOT/rescue, a boot environment on the production pool): the stage exists, the name is a ZFS dataset name; then written (immutable: identical re-add is a no-op, a differing one is refused – drop first)
Write the transient frame into the mounted rescue root: the stage exists, the root is mounted, the transient list is not empty; then `stage rescue transient file' – the rc.d script rescue_union (from the template), the mount point of its memory layers (/.rescue-union), /etc/rc.conf.d/rescue_union naming the listed paths, /etc/rc.conf.d/tmp (tmpmfs) and /etc/rc.conf.d/var (varmfs, unless /var is itself in the list)
Record the rescue partition of a medium (da1p3): the stage and the medium exist, the name is a GPT partition name; then written under rescue/cards/<medium> (immutable, drop first to change)
Forget the rescue partition record of a medium: the stage exists and has it; then it is removed
Create the rescue root on the production pool, empty: the stage exists, the dataset is recorded; then `stage rescue dataset create' (ONE dataset, mountpoint / and canmount noauto – a boot environment the loader can name and nothing mounts by itself; the meta package fills it)
Take the baseline of the rescue root – the manifest of its tree (every file and symlink, LC_ALL=C sorted, `path sha256=hash' / `path symlink=target', the format of the boot manifest) signed with the pinned attest key, into stage/<stage>/rescue/baseline/MANIFEST(.asc): the stage exists, the root is mounted, the key is pinned; then `stage rescue baseline write' as root (the tree holds root-only files) and `attest' as the owner. Close the root and take the snapshot right after: the snapshot and this manifest are one image, `stage rescue verify' checks a card against it
Read the rescue passphrase hidden, twice, into <workdir>/auth-rescue.txt on the RAM disk (geli init -J and geli attach -j take the file; it is what the owner types at the rescue boot): the stage exists, the work directory is a mounted RAM disk; then `stage rescue passphrase read'
Prepare a medium’s rescue partition, once: GELI (AES-XTS 256, HMAC/SHA256, BOOT, NODELETE, slot 0 = the rescue passphrase together with the key file, its own master key), every sector written (HMAC needs it), the pool created on it with ROOT below, exported again: the stage exists, the medium is inserted, the partition, the pool and the key file are recorded, the passphrase file is there; then `stage rescue card geli' and `stage rescue card pool'
Attach the medium’s rescue partition (passphrase and key file) and import its pool without mounting (altroot $ELEBAKE_ROOT/mnt/<stage>-rescue-<medium>): the stage exists, the medium is inserted, the partition, the pool and the key file are recorded, the passphrase file is there; then `stage rescue card attach'
Export the medium’s pool and detach its rescue partition: the stage exists, the partition and the pool are recorded; then `stage rescue card detach'
Send the recorded snapshot to the medium’s card: the stage exists, the snapshot is recorded, the card opens (stage rescue card open), the stream goes (the whole tree the first time, the increment since what the card holds after), bootfs is set, the root on the card is set read-only (the rescue is transient), the snapshot on the card is verified against the source, the receipt is recorded, the card closes
Check a card against the baseline without sending, three questions: does the card hold the recorded snapshot (GUID) with a read-only root, is the baseline manifest signed by the pinned key, and is the tree on the card exactly the baseline (every entry present and unchanged, nothing beyond it) – the stage exists, the snapshot is recorded, the baseline is there and verified, the card opens, the root is mounted read-only, the tree is checked, the root is unmounted, the card closes. Anything found is an error naming it
Record the name of the meta package that describes the rescue system (illyria-rescue): the stage exists, the name is a package name; then written (immutable, drop first to change)
Forget the meta package name of the stage: the stage exists and has the record; then it is removed
Record the GELI key file of the cards as the loader names it (/boot/keys/zroot.key, in the boot tree on the card; the host reads the stage copy, stage/<stage>/boot<path>): the stage exists, the path is absolute; then written (immutable, drop first to change). The slot opens with the rescue passphrase AND this file, as zroot does
Forget the key file record of the stage: the stage exists and has the record; then it is removed
Record the checkout a tool is packaged from (elvboot, vpn-switch): the stage exists, the tool is a package name, the path is absolute; then written under rescue/sources/<tool> (immutable, drop first to change). `stage rescue tool package' runs `make package' there
Forget the checkout record of a tool: the stage exists and has the record; then it is removed
Take one rc.conf line into /etc/rc.conf.local of the rescue system – what the rescue deliberately keeps different from the production, whose rc.conf travels 1:1 (sshd_enable=“NO”): the stage exists, the line is name=“value”; a line already listed is a note
Take one line out of the rc.conf.local list: the stage exists and lists it; then the line is removed
Write /etc/rc.conf.local of the mounted rescue root from the local lines: the stage exists, the root is mounted, the list is not empty; then `stage rescue local file'
Take a user of this machine into the rescue system: the stage exists, the user exists here (pw usershow); then recorded under rescue/users/<name> as read from the production – uid, gid, groups, home, shell, gecos (immutable, drop first to change). The password hash is not a record: `stage rescue user mirror' takes it from master.passwd when it runs as root. `root' takes its password only
Forget a user record of the stage: the stage exists and has the record; then it is removed
Create the recorded users in the mounted rescue root with the uid, gid, groups, home and shell they have here, and copy their password hashes (and root’s) from master.passwd: the stage exists, the root is mounted, users are recorded; then `stage rescue user write'
Build a tool’s package from its recorded checkout into the stage’s repository THROUGH ITS PORT: the stage exists, the checkout is recorded, the ports framework is installed (/usr/ports/Mk); then `stage rescue tool build' – the distfile as a git archive of the checkout (<tool>-0.0.<commit count>.tar.gz into the stage’s distfiles), `make makesum package' in <checkout>/port with the stage’s work directory and repository; the meta package names the tool by the port’s origin
Build the meta package into the stage’s repository: the stage exists, the package name is recorded, the tool list is not empty; then `stage rescue manifest render' into +MANIFEST, `stage rescue package create' (pkg create -M), `stage rescue repo index' (pkg repo, and the repository configuration the install reads)
Install the meta package into the mounted rescue root – and with it the base sets, every tool, elvboot and vpn-switch, from this machine’s repositories and the stage’s: the stage exists, the root is mounted, the package is built; then `stage rescue package pkg'
Record a patch of the built rescue root: the stage exists, the name has the form of a record name, the script is an absolute path IN THE IMAGE (the package’s own copy under its template tree, e.g. /usr/local/lib/elebake/template/rescue/elvboot-import-db-patch.sh), the user exists on this machine; then written under rescue/patches/<name> as `<user>:<script>' (immutable, drop first to change) and the resource directory $ELEBAKE_BASE/.resource/<name>/ made – the fixed place the owner fills with what the script takes as its second argument (the export pair of a database: dump.sh with its .asc, bundle.tar.gz, and signer.asc, the signer’s public key). `stage rescue patch apply' runs every patch as root INSIDE the mounted root (a one-shot jail): sh <script> <user> <resources>
Forget a patch record of the stage: the stage exists and has it; then it is removed (the resource directory stays: the owner’s files)
Apply every recorded patch inside the mounted rescue root, in name order: the stage exists, the root is mounted; then per patch `stage rescue patch complete' (its script in the root, its resource directory here) and `stage rescue patch run' (the resources hung into the root read-only, a one-shot jail on the root, the script as root in it) – fail-fast, the first failing patch stops the batch. No patch recorded is a note. A phase of `stage rescue build', right before the baseline
Take the kernel of the stage’s boot tree over into the mounted rescue root: the stage exists, the root is mounted, boot/kernel/kernel is in the stage; then `stage rescue kernel copy'. The root runs the kernel the medium boots – the one the pair carries, redacted or full – whether `stage include' took it from the package under /boot or `stage build kernel' baked it from the fork; so what rc loads at run time (kld_list, the Linuxolator’s fdescfs) was built with the kernel that runs. The package’s copy under /boot/kernel goes.
Build the opened rescue root, in phases: the stage exists, the root is mounted; then `stage rescue package install' (the meta package and everything it names), `kernel mirror' (the stage’s kernel over the package’s), `config mirror', `local write', `user mirror', `transient write', `patch apply' (the databases, built into the image by the recorded patches) and, last, `baseline' (the signed manifest of the finished tree). Before: dataset open and package build; after: dataset close and snapshot
After a rotation of the key file (zroot.key): set slot 0 of the medium’s rescue partition to the rescue passphrase and the NEW recorded key file, opening it with the passphrase and the OLD one: the stage exists, the medium is inserted, the partition and the key file are recorded, the passphrase file is there; then `stage rescue card setkey'
Import the medium’s pool writable, from its GPT label (confined: no device scan), rooted under $ELEBAKE_ROOT/mnt/<stage>-<medium>: the description checked, then `stage pool mount' – idempotent, an imported pool stays as it is
Import the medium’s pool read-only (adopt reads, nothing is written), rooted under $ELEBAKE_ROOT/mnt/<stage>-<medium>: the description checked, then `stage pool mount readonly' – idempotent
Unmount the boot dataset and export the medium’s pool (the reverse of stage pool import): the description checked, then `stage pool unmount' – idempotent, a pool that is not imported is nothing to do
Write the stage’s boot/ tree onto the medium’s dataset: the medium prepared (pool imported, dataset mounted), then snapshot + copy + verify (fail-fast), then close (rollback if the tree is not the manifest) and pool export – close and export ALWAYS run (the batch keeps going), so the medium is never left half-written or imported
Snapshot the medium’s boot dataset (elebake-<stamp>) before it is rewritten: the medium prepared, then `stage tree snap', then `stage tree prune' (the older elebake-* snapshots beyond ELEBAKE_SNAPSHOT_KEEP go)
Replace the boot/ tree on the medium with the stage’s boot/: the medium prepared, the manifest written and attested (both idempotent), then `stage tree write'
Inspect the boot tree on the medium against the stage’s manifest, both directions: the medium prepared, the manifest written, the findings taken (stage tree findings), then `stage tree matches' – a finding fails the command
Leave the medium consistent: the medium prepared, the findings taken (stage tree findings), then `stage tree judge' – a tree that IS the manifest stays (the snapshot kept as history), a tree that is not goes back to the newest elebake snapshot
Take the boot tree FROM the medium into the stage’s boot/ (the reverse direction: a card you trust becomes the description): the medium prepared read-only, then `stage adopt copy', then pool export
Register a NAMED deploy medium (its backups sort under backup/<medium>): the stage exists, the medium name and the device node are well-formed; then `stage medium record'
Register where the medium’s boot tree lives (used by stage push/tree sync, adopt, pool import): `stage boot tree ensure' (the description: stage, medium, a well-formed label and dataset name), then the guarded act – the World as it is, read here from gpart: the label names a freebsd-zfs partition of the medium’s disk, never its EFI system partition: `stage boot tree record', else an error line. A medium that is not inserted is registered unchecked (a dump replays this command; the batches that need the card check it when they run)
Save the loader currently ON that medium as a backup RECORD backup/<medium>/<label>/ (loader.efi, description, sha256, created, source, by): `stage backup ensure' (the description: stage, medium, a new record name, a description), then the acts – `stage medium mount' (the card verified when it runs), `stage backup take', `stage medium release'. Label defaults to the UTC stamp, description to medium/stage/user
Show the backup records of that medium, oldest first (label, created, sha256, description – the view for a rollback decision): the stage and the medium exist; then `stage backup table'
Put a backup back onto the medium – but FIRST save what is on the medium now as record suspect-<stamp> (quietly: the loader about to be overwritten may be the evidence). One line of preconditions, `stage rollback ensure' (the description: stage, medium, the record present and intact), then the acts on one mount: `stage medium mount' (the card verified when it runs), `stage backup take', `stage rollback write' (the hash verified on the medium), `stage medium release'
The loader swap onto the NAMED medium: the stage and the medium exist, the device is present, the signed loader is there and newer than the built one; then `stage deploy write' (mount, backup record of what is there, copy, hash check, umount)
Stamp the medium’s letter onto its ESP (, one byte): the loader reads it as the identity of the card it was loaded from, the anchor records it as “last booted”, MediumSwitch tells when it changes – two cloned cards differ in nothing else. The stage and the medium exist, the device is present; then `stage medium stamp write'. Threat model assumed: the letter is an identity, not a secret; a thief who copies a card copies the letter, and the anchor in the TPM still says which letter booted last
Which card is in? The stage exists, the medium is registered, its device node is present; then `stage medium stamp read' mounts the ESP read-only and says the letter the card carries (or that it carries none). Nothing is written. With cloned cards every registered letter names the same device node, so any registered medium serves as the way to the node

Toolchain readiness checks; each backend answers for itself.

List the measurements the containers’ catalogs offer in the stage’s checkout, with their descriptions: one `stage measure list <container> <stage>' per container of ELEBAKE_CONTAINERS, or the one container given
List the actions the containers’ catalogs offer in the stage’s checkout, with their descriptions: one `stage action list <container> <stage>' per container of ELEBAKE_CONTAINERS, or the one container given
List the firing predicates the containers’ catalogs offer in the stage’s checkout, with their descriptions: one `stage when list <container> <stage>' per container of ELEBAKE_CONTAINERS, or the one container given
Print the entry’s name and the description its source carries (template/awk/catalog-doc.awk over the catalog file); an entry without one says so
Bind a policy into a phase of the stage: ensure its prerequisites in the container that hosts the phase, then append it to the phase’s policy list (a position inserts instead)
Unroll the containers: one `stage phase policy ensure <container> <stage> <phase> <policy>' per container of ELEBAKE_CONTAINERS; the container that hosts the phase ensures the policy, the others say so in a comment line
The policy’s prerequisites in the container: every line `<kind> <name>' of the record (`gate <gate>', `trigger <trigger>') is one `stage <kind> ensure <stage> <container> <name>'
The stage exists; every bound policy has its prerequisites (stage phase policy ensure, per phase and policy); every baseline the bound policies demand is reported when missing (gate slots, macro expectations, and what the catalog names demand per template/tbl/baseline-demands.tbl); then the summary
List the phases the stage’s checkout offers per container (ELEBAKE_CONTAINERS, in that order), each with its bound policies: one `stage phase show <container> <stage>' per container; one phase in detail with the 2-arg form
Unbind a policy from a stage’s phase (the policy itself survives): the stage exists and the phase binds the policy, then the line is removed
Store a macro record for a compiled baseline: <MACRO> is the C macro stem (BOARD_DIGEST), <type> sha256 | byte, <label> the measurement label. The 4-arg form names the #else alternative (a verbatim C expression, `-' derives MEASUREMENT_NONE), the 5-arg form also the defined name (`-' derives <MACRO> minus _DIGEST plus _EXPECTED). No arsenal record is referenced: the chain is write only
Remove a macro record: it exists, no macro expectation names the macro it defines (a referenced record is never dropped), then it is erased
Show every macro record (or one) as the C that WOULD be emitted: one `macro show <macro>' per record; none is a note
Store a named, reusable expectation: <type> byte | sha256 | string | macro | key; a macro expectation names a macro an arsenal macro record defines (the reference never dangles; the type word dispatches the check); a key expectation names the leaf of a kenv record the loader reads at run time, loader.trust.<gate>.<key> – for a value that includes the loader itself (PcrBank, LoadedImages) and so cannot be compiled into it; stage kenv learn records it, no build follows. Only for gates of PHASE_KERNEL and later: the loader reads loader.trust.conf after PHASE_LOADER, a key bound earlier finds no value and the claim stays skipped. Then the record is written – an identical re-add is a no-op, a different one is refused (immutable; drop first)
Remove a expectation: it exists, no claim names it as its expectation (a referenced record is never dropped), then it is erased
Show every expectation record (or one) as the C that WOULD be emitted: one `expectation show <expectation>' per record; none is a note
Store a named, reusable claim: its expectation must exist in the arsenal (the reference never dangles), then the record is written – an identical re-add is a no-op, a different one is refused (immutable; drop first)
Remove a claim: it exists, no gate lists it (a referenced record is never dropped), then it is erased
Show every claim record (or one) as the C that WOULD be emitted: one `claim show <claim>' per record; none is a note
Store a named, reusable FIRE(<when>, <action>) pair. Each field is a catalog name or a composition without whitespace: the when may be and(a,b,...), or(a,b,...) or not(a), nested at will; the action may be compose(a,b,...) to run several in order. The loader sees AND/OR/NOT and COMPOSE, the sh containers { a && b; }, { a || b; }, ! a and a; b. The leaves are checked against the container’s catalog when a policy binds: no arsenal record is referenced, the chain is write only – an identical re-add is a no-op, a different one is refused (immutable; drop first)
Remove a trigger: it exists, no policy lists it (a referenced record is never dropped), then it is erased
Show every trigger record (or one) as the C that WOULD be emitted: one `trigger show <trigger>' per record; none is a note
Create a gate: <gate> is a C identifier (it lands in foundation.c). A gate carries no secret – the loader compares no password, the three factors open the encrypted providers or nothing does (the three-factor design). No arsenal record is referenced: the chain is write only, a re-add is a no-op and leaves the claim list untouched
Remove a gate: it exists, no policy names it (a referenced record is never dropped), then it is erased
Append a claim reference to a gate (order = evaluation order): the gate and the claim exist (the reference never dangles), then the claim is appended – a claim already listed is a no-op; the 3-arg form inserts at a 1-based position instead
Unlink a claim reference from a gate (the claim itself survives): the gate exists and lists the claim, then the line is removed
Show every gate record (or one) as the C that WOULD be emitted: one `gate show <gate>' per record; none is a note
Create a named policy for a gate (triggers are appended separately): the gate exists (the reference never dangles), then the record is written – an identical re-add is a no-op, a different gate is refused (immutable; drop first)
Remove a policy: it exists, no phase of any stage binds it (a referenced record is never dropped), then it is erased
Append a trigger (FIRE) reference to a policy: the policy and the trigger exist (the reference never dangles), then the trigger line is appended – a trigger already listed is a no-op; the 3-arg form inserts at a 1-based position among the trigger lines instead
Remove a trigger reference from a policy (the trigger itself survives): the policy exists and lists the trigger, then the line is removed
Show every policy record (or one) as the C that WOULD be emitted: one `policy show <policy>' per record; none is a note
Emit the foundation portion of a database dump: one block per family in dependency order (macros, expectations, claims, triggers, gates, policies); adds are idempotent-immutable, so replays are safe
List the arsenal record files (macros, expectations, claims, triggers, gates, policies) that belong into an archive
One sentinel answer class in one batch: the hash is 64 hex digits and the template policy exists; then expectation <name> (string, label <loader-gate>, value <hash> = sha256(salt+word) exactly as the loader publishes loader.trust.<loader-gate>.answer), claim <name> over measure_answer, gate <name> with `-' and `.' as ’_’, policy <name> with the triggers of the template, and the binding to <phase> of <stage>. Assumes the sentinel of <loader-gate> is bound in the loader (sentinel_act, leafs question/salt/display) and the container of <phase> has measure_answer. Names stay neutral (fish-3, not fish-halt): whoever reads the database must not learn the class from the name
Add one sentinel answer class with the word read HIDDEN from the terminal when the batch runs – twice, both must match – and hashed with the stage’s salt (stage kenv add <stage> loader.trust.<loader-gate>.salt <hex>). The word reaches neither argv, trace, history, the batch text nor the generator: the running script leaves only sha256(salt+word) in a file of the database, the next line lifts it into `answer hash add' and removes the file. Write the word down BEFORE typing it (inventory, paper): the database keeps only the hash
Remove one answer class in reverse order: the binding in <phase>, the policy, the gate, the claim, the expectation
Roll out every answer class of a table file, one line per class `<name> <policy-template> <word...>' (the word is the rest of the line, spaces kept; no word = the empty answer; `#' lines and blank lines skipped): the file is readable by its owner only and the salt exists; then per class line `answer line hash' (the word hashed when the batch runs, into .tmp/answer/<name>) and `answer hashed add' (the hash lifted into `answer hash add') – the words reach only the running script, never argv, trace, the batch text or the generator. The file IS the written place of the words: keep it on a tmpfs while typing, encrypt and print it, then remove it (rm -P)
Remove every answer class named in a table file (first field of each class line; the words are not read): one `answer drop' per line
The catch-all answer class: expectation <name> (byte, label <loader-gate>, 1), claim over measure_answer_matched, gate, policy with the triggers of the template – a when_fail template (react-miss: spool-fail, shutdown-fail) – and the binding. It fails whenever no word class matched: empty and wrong alike. Bind it AFTER the word classes (the phase runs in binding order; a class added later must be followed by `answer drop' + re-add of the catch-all), and give every class template the trigger for answer_matched_act
List the answer classes seen from the stage, in policy order: every policy whose gate’s first claim measures measure_answer (a word class) or measure_answer_matched (the catch-all) – name, the phase of <stage> that binds it or `unbound', its triggers. The words are not here and never were: the inventory has them
Generate the earlboot rc.d script of the stage into its hooks/earlboot: the stage and its checkout exist, the bound measurements’ demands on the boot tree are met (stage require <stage> earlboot), then header, constants, state, tools, the catalog functions, the prologue, the phases SYSINIT and MOUNTED in that order, the footer, and the install. Assumes: the record LOADER_TRUST_WORD_SECRET is present when a word claim or handover is bound; mac_bootlock keeps loader.trust.* immutable; the script is installed with `stage earlboot install' and covered by mac_veriexec
Replay a captured boot through the CURRENT earlboot of the stage, without root and without side effects: the same rendering as `stage earlboot mk' but with mocks for state and tools – kenv/sysctl/kldstat answer from <dump>, shutdown and logger become notes on stderr, the state lives under hooks/ and is removed after – then the run: exit code, every file the run left (appraisal, book, spool, marks). Never run the INSTALLED earlboot by hand: book_act appends to the book. Capture a dump after a boot with: { kenv; sysctl -e kern.osrelease kern.ident kern.securelevel security.mac.veriexec.state vfs.root.mountfrom | sed `s/^([^=])=(.)$/sysctl.\1=“\2”/'; kldstat -q -m mac_bootlock && echo `kldstat.mac_bootlock=1'; } > /tmp/ram/boot.kenv (the mocks answer only the name=“value” form)
Install the generated earlboot script as /etc/rc.d/earlboot (root:wheel 0500; the root dataset, so the earliest userland does not depend on a mounted /usr/local) and enable it in /etc/rc.conf.d/earlboot; refused while none was generated. Run as root
Generate the elvbootd hooks of the stage into its hooks/: the stage and its checkout exist, the bound measurements’ demands on the boot tree are met, at least one runtime phase is bound; then one self-contained hook.<phase>.sh per BOUND runtime phase (STARTUP rc.d start, PERIODIC periodic/security, RESUME rc.resume, MEDIA devd with $1 = the cdev, SHUTDOWN rc.d stop), each with header, constants, state, tools, the catalog functions (earlboot’s palette inherited), the prologue and its phase; plus the glue that plugs a hook into its mechanism: hooks/elvbootd (rc.d) when STARTUP or SHUTDOWN is bound, hooks/elvboot.devd.conf (devd) when MEDIA is bound
Replay one runtime phase (STARTUP, PERIODIC, RESUME, MEDIA, SHUTDOWN) through the CURRENT elvbootd catalog of the stage, without root and without side effects: the same rendering as `stage elvbootd mk' but with mocks for state and tools – kenv/sysctl/kldstat answer from <dump>, shutdown, logger and the TPM writers become notes on stderr, the state lives under hooks/ and is removed after – then the run: exit code, every file the run left (appraisals, spool, marks, the boots/ snapshot of kenv_act). Capture a dump after a boot as for `stage earlboot test'
Install the generated hooks under /usr/local/etc/elvboot/ (0500) and wire each into its mechanism with the generated glue: STARTUP as /usr/local/etc/rc.d/elvbootd (enabled), PERIODIC as /usr/local/etc/periodic/security/900.elvboot, RESUME as a line in /etc/rc.resume, MEDIA via /usr/local/etc/devd/elvboot.conf (devd restarted). Refused while nothing was generated. Run as root
Generate foundation.c from the bound policies into the worktree: the stage and its checkout exist, the foundation/ directory is there, then the sections rendered in file order into foundation.c.new (header, macros, secrets, prerequisites, gates, phases, dispatch) and installed as one complete file
The foundation batch: check, make, report
Summarize the foundation of a stage: the checkout, per enum phase its bound policies, the referenced gates and the emission target – notes for the caller
The one reading view of a stage: every container, every phase, the bound policies, each policy’s gate with its claims (measurement, diagnose, publish, expectation) and its triggers in firing order (when -> action). Rewrites to `stage phase show <stage>'; with a container to `stage phase show <container> <stage>'

All configuration lives in ELEBAKE_* variables, layered as .env/local (override, written by setenv), .env/default (the installed profile) and the shipped templates. Interpreter pins ELEBAKE_INTERPRETER_<function> decide per function whether emitted shell is displayed or executed: arity-specific before arity-agnostic before the class default (ELEBAKE_TERMINAL_INTERPRETER, default cat). Every variable answers elebake help env <VAR>.

The openpgp record that signs what this database exports – and the signer a receiver expects
The containers a stage binds policies into, in this order: loader (the EFI loader, phases PHASE_BOOT/LOADER/KERNEL, emitted as foundation.c), earlboot (rc.d at mountcritlocal, SYSINIT/MOUNTED, sh), elvbootd (runtime hooks, STARTUP/PERIODIC/RESUME/MEDIA, sh). Each name is a catalog directory under stand/efi/loader/local of the checkout (the loader in the headers there, the others as <name>/{policy,measure,action}.sh with a PHASES= line). A fourth name here is a fourth container: its own catalog directory and phases, no code change in elebake (extensible: an intermediate layer must be possible)
The editor stage edit opens on a file of the boot tree (stdin from /dev/tty, so it works under ... \| sh); edit = FreeBSD base ee(1). No fallback to the login environment: an unset pin is a refusal
Curated tool list freebsd prerequisites probes at generation time
Path to the FreeBSD source repository the stages build from
Modules preloaded at startup (engine.sh is always loaded first)
Interpreter for catalog doc – runs template/catalog-doc.awk over the catalog file and prints the entry with its description
Interpreter for catalog title – the heading of one catalog section (display)
Interpreter for the dump block claim dump – dump TEXT
Interpreter for dump env epilogue – dump TEXT
Interpreter for dump env prologue – dump TEXT
Interpreter for environment cache off – removes the cache file
Interpreter for environment cache status – one report line
Interpreter for the dump block expectation dump – dump TEXT
Interpreter of `freeze clear' (elvboot freeze ack): the act removes the mark in the hooks’ state directory, root’s – sudo sh runs it, cat displays it
Interpreter of `freeze lines' (elvboot): the act reads the hooks’ state directory, root’s – sudo sh runs it, cat displays it
Interpreter for the dump block gate dump – dump TEXT
Interpreter for help manual commands – one part of the manual as TEXT
Interpreter for help manual environment – one part of the manual as TEXT
Interpreter for help manual title – one part of the manual as TEXT
Interpreter for the dump block macro dump – dump TEXT
Interpreter for the dump block policy dump – dump TEXT
Interpreter for provenance collect – receipt files for an archive
Interpreter for provenance dump – dump TEXT
Interpreter for provenance file – files the receipt of an import
Interpreter of `report detachsign' (elvboot report sign): the attest key lives in root’s keyring – sudo sh signs, the signature is handed to the owner
Interpreter of `report forensic render' (elvboot): the act reads the database as the owner; cat displays it
Interpreter of `report render' (elvboot): the act reads the hooks’ state directory, root’s – sudo sh runs it, cat displays it
Interpreter for the batch stage adopt – KEEP-GOING wrapper
Interpreter for stage container prepare – the hooks/ directory
Interpreter for the dump block stage dump add – dump TEXT
Interpreter for the dump block stage dump baselines – dump TEXT
Interpreter for the dump block stage dump checkout – dump TEXT
Interpreter for the dump block stage dump disks – dump TEXT
Interpreter for the dump block stage dump filter – dump TEXT
Interpreter for the dump block stage dump hooks – dump TEXT (a note:
Interpreter for the dump block stage dump inventory dir – dump TEXT
Interpreter for the dump block stage dump inventory records – dump TEXT
Interpreter for the dump block stage dump inventory set – dump TEXT
Interpreter for the dump block stage dump kenv – dump TEXT
Interpreter for the dump block stage dump keys – dump TEXT
Interpreter for the dump block stage dump media – dump TEXT
Interpreter for the dump block stage dump phases – dump TEXT
Interpreter for the dump block stage dump prereqs – dump TEXT
Interpreter for the dump block stage dump quarantine – dump TEXT
Interpreter for the dump block stage dump rescue – dump TEXT
Interpreter for the dump block stage dump witness – dump TEXT
Interpreter for the dump block stage dump work – dump TEXT
Interpreter for stage elvbootd glue devd – the devd configuration as TEXT
Interpreter for stage elvbootd glue rcd – the rc.d script as TEXT
Interpreter of `stage freeze check' (stage push, stage tpm seal): the act tests the mark in the hooks’ state directory, root’s – sudo sh runs it, cat displays it
Interpreter for stage status attestkey – one derived-state line
Interpreter for stage status filter – one derived-state line
Interpreter for stage status marker – one derived-state line
Interpreter for stage status media – one derived-state line
Interpreter for stage status populated – one derived-state line
Interpreter for stage status signed – one derived-state line
Interpreter for stage status signkey – one derived-state line
Interpreter for stage status sitemk – one derived-state line
Interpreter for the batch stage tree sync – KEEP-GOING wrapper
Interpreter for the batch stage tree work – fail-fast core of tree sync
Interpreter of `stage witness anchor write': the TPM index is defined and written as root, the receipt chowned to the owner – sudo sh runs it, cat displays it
Interpreter of `stage witness compare tables': root reads the GPT of the witness’s disk and compares – sudo sh runs it, cat displays it
Interpreter of `stage witness tables': root reads the GPT of the witness’s disk and writes the receipt – sudo sh runs it, cat displays it
Interpreter for the dump block trigger dump – dump TEXT
Interpreter of `witness lines' (elvboot): the receipt lines of the stage’s witnesses are the emission itself – cat shows them
Extra make(1) arguments appended to every stand/ component build (stage
The pager last \<n\> \| sh opens on a trace (stdin from /dev/tty, so it works under ... \| sh). No fallback to the login environment: an unset pin is a refusal
The state directory of the generated earlboot/elvbootd scripts on the target (ELV_STATE in the scripts: appraisal, book, heartbeat, spool, marks, smart counters); created 0700 root by the install steps

~/.elebake/db
the active database (symlink)
~/.elebake/<name>/.env/{local,default}/
the layered variable store: overrides (setenv) over the installed profile over the shipped templates
~/.elebake/<name>/.log/YYYY-MM-DD/
per-invocation traces – the debugging ground truth (elebake last)
~/.elebake/<name>/stage/<stage>/
a stage record: boot/ (the built tree), backup/<medium>/<label>/ (backup records), media/, marker/
~/.elebake/<name>/provenance/
receipts of every admitted import (the lineage)
~/.elebake/worktree/
git worktrees of the FreeBSD source; build trees live outside the database
~/.elebake/bundle/, ~/.elebake/incoming/
where an exported bundle is handed to the user, and import’s scratch area

Create a database, connect the sources, register keys, build and publish a stage (every command that changes the database acts by itself; the pins decide what runs as root):

elebake bootstrap current minimal
elebake setenv ELEBAKE_FREEBSD_SRC ~/git/freebsd-src
elebake pkcs11 add db 'pkcs11:token=...;object=db' /root/sb/db.crt
elebake openpgp add manifest 4E1F0A2B7C9D8E6F5A4B3C2D1E0F9A8B7C6D5E4F /root/sb/.gnupg
elebake stage add smoke1
elebake stage sign key smoke1 pkcs11 db
elebake stage attest key smoke1 openpgp manifest
elebake stage checkout smoke1 elvboot-15.1^0
elebake stage make smoke1
elebake stage push smoke1 b
    

Take the database elsewhere – one signed pair, pinned on arrival:

elebake setenv ELEBAKE_ARCHIVE_ATTEST_KEY manifest
elebake export full ~/git/config/dump.sh ~/.elebake/bundle/a1b2c3d.tar.gz
# on the receiving machine
elebake openpgp add manifest 4E1F0A2B7C9D8E6F5A4B3C2D1E0F9A8B7C6D5E4F
elebake setenv ELEBAKE_ARCHIVE_ATTEST_KEY manifest
elebake import ~/git/config/dump.sh ~/.elebake/bundle/a1b2c3d.tar.gz
elebake provenance list
    

The rescue pair, and a rollback that keeps the evidence:

elebake export minimized ~/rescue/dump.sh ~/rescue/bundle.tar.gz
elebake stage backup list smoke1 a
elebake stage rollback smoke1 a known-good-p2
    

Inspect instead of act (any command): pin its act terminal to cat (elebake setintp stage_deploy_write cat), read the emitted shell, then pin it back. elebake is the wrapper make install places in $PREFIX/bin; from a checkout the same commands read ./elebake.sh ....

A trigger whose when is a composition, and one that runs two actions:

elebake trigger add report-measured 'and(when_fail,not(when_skipped))' report_act
elebake trigger add silence-tainted when_tainted 'compose(taint_act,silence_act)'
elebake trigger show report-measured
# report-measured: FIRE(AND(when_fail, NOT(when_skipped)), report_act)
    

A value the loader itself is part of – the PCR bank, the loaded images – is expected from the conf, not the binary; learned after a trusted boot, carried by loaderconf mk, include, sign, push, no build:

elebake expectation add pcr-expected key PcrBank pcr.expected
elebake claim add pcr measure_pcr - pcr.sha256 pcr-expected
elebake stage require daily-v1
# loader.trust.kernellock.pcr.expected  (claim pcr)  MISSING -- stage kenv learn ...
elebake stage kenv learn daily-v1 loader.trust.kernellock.pcr.expected loader.trust.kernellock.pcr.sha256
    

The platform sets, add semantics: import what elvbootd filed per boot, see what moves, take in what holds, render, build, learn:

elebake stage inventory import daily-v1
elebake stage inventory show daily-v1 efivars
elebake stage inventory add daily-v1 efivars 8be4df61/BootOrder
elebake stage inventory add daily-v1 acpi FACP/-
elebake stage inventory list daily-v1 acpi
elebake stage site mk daily-v1
    

uefisign(8), veriexec(8), gpg(1)

docs/QUICKSTART.md, docs/TUTORIAL.md, docs/ARCHITECTURE.md in the source tree; https://github.com/enk-ode/elvboot; the FreeBSD sources with the platform-trust-gates series: https://github.com/johannes-bruegmann/freebsd-src (branch elvboot-15.1).

Dr. Johannes Brügmann

Dr. Johannes Brügmann.

elebake October 15, 2026 ELEBAKE(8)