feat(firstboot): force root + operator password on first boot (console gate) #139

Merged
clawdie merged 6 commits from force-root-password-on-first-boot into main 2026-06-25 07:21:34 +02:00
Owner

What

A first-boot password gate so the operator USB no longer ships with an
unprotected root account. New rc.d service clawdie_firstboot_rootpw,
ordered REQUIRE: clawdie_live_gpu FILESYSTEMS / BEFORE: sddm colibri_daemon.

On the text console (the operator is physically present at first boot) it shows a
15s countdown to engage. If engaged, it forces a root and operator
(clawdie)
password — echo-off via stty, applied with pw usermod -h 0 which
reads the secret on stdin (never in argv/ps, never near the agent/LLM). It
prints "write both on paper — no recovery."

  • Idempotent: persistent success marker /var/db/colibri/.secured (/var
    persists on this image — varmfs="NO"). Present -> silent exit.
  • Non-bricking: skipping (no engage within 15s) leaves the node open and
    re-prompts next boot, so an unattended/headless boot never hangs while an
    attended boot is effectively forced.

Why this shape

Ordering the gate before colibri_daemon means the security decision is
always made before any agent can autospawn / node_register. That dissolves
the boot-vs-login race natively
— no cross-component interlock needed. The
.secured marker is also the signal a later colibri change can read to label an
unsecured node to mother (daemon-side; out of scope here).

This replaces the earlier GUI-dialog idea: no new package (yad/zenity), no
edit to the XFCE session path (no login-brick risk), FreeBSD-native
(rc.d +
pw + stty, mirroring existing clawdie_live_gpu ordering and
clawdie-join-hive.sh's stty -echo).

Verification

tests/firstboot-rootpw-test.sh — 10/10: marker skip, password validation
(empty/short/mismatch rejected), and that the password reaches pw usermod root -h 0 on stdin and never appears in argv.

⚠ Boot-test before merge

The interactive bits — read -t countdown and stty echo-off on
/dev/console in the rc phase — are FreeBSD/vt(4)-sensitive and must be
booted on osa/bhyve to confirm
before merge. Everything else is verified.

Follow-ups (not in this PR)

  • Remove the misleading --root-password build flag (writes the password
    plaintext into build.cfg and never applies it).
  • colibri: read /var/db/colibri/.secured at daemon start and label an
    unsecured node to mother + reference it in the AGENTS.md nag.

🤖 Generated with Claude Code

## What A first-boot password gate so the operator USB no longer ships with an unprotected root account. New rc.d service **`clawdie_firstboot_rootpw`**, ordered `REQUIRE: clawdie_live_gpu FILESYSTEMS` / `BEFORE: sddm colibri_daemon`. On the text console (the operator is physically present at first boot) it shows a **15s countdown to engage**. If engaged, it forces a **root** *and* **operator (clawdie)** password — echo-off via `stty`, applied with `pw usermod -h 0` which reads the secret on **stdin** (never in argv/ps, never near the agent/LLM). It prints "write both on paper — no recovery." - **Idempotent:** persistent success marker `/var/db/colibri/.secured` (`/var` persists on this image — `varmfs="NO"`). Present -> silent exit. - **Non-bricking:** skipping (no engage within 15s) leaves the node open and **re-prompts next boot**, so an unattended/headless boot never hangs while an attended boot is effectively forced. ## Why this shape Ordering the gate **before `colibri_daemon`** means the security decision is always made before any agent can autospawn / `node_register`. That **dissolves the boot-vs-login race natively** — no cross-component interlock needed. The `.secured` marker is also the signal a later colibri change can read to label an unsecured node to mother (daemon-side; out of scope here). This replaces the earlier GUI-dialog idea: **no new package (yad/zenity), no edit to the XFCE session path (no login-brick risk), FreeBSD-native** (rc.d + `pw` + `stty`, mirroring existing `clawdie_live_gpu` ordering and `clawdie-join-hive.sh`'s `stty -echo`). ## Verification `tests/firstboot-rootpw-test.sh` — 10/10: marker skip, password validation (empty/short/mismatch rejected), and that the password reaches `pw usermod root -h 0` on **stdin** and **never appears in argv**. ## ⚠ Boot-test before merge The interactive bits — `read -t` countdown and `stty` echo-off on `/dev/console` in the rc phase — are FreeBSD/vt(4)-sensitive and **must be booted on osa/bhyve to confirm** before merge. Everything else is verified. ## Follow-ups (not in this PR) - Remove the misleading `--root-password` build flag (writes the password plaintext into `build.cfg` and never applies it). - colibri: read `/var/db/colibri/.secured` at daemon start and label an unsecured node to mother + reference it in the AGENTS.md nag. 🤖 Generated with Claude Code
clawdie added 1 commit 2026-06-25 05:54:41 +02:00
Adds clawdie_firstboot_rootpw, an rc.d gate ordered BEFORE sddm and
colibri_daemon. On the text console (operator present at first boot) it runs a
15s countdown to engage; if engaged it forces a root AND operator (clawdie)
password, echo-off, applied via 'pw usermod -h 0' over stdin (secret never in
argv/ps, never near the agent). Idempotent via a persistent success marker
/var/db/colibri/.secured (/var persists: varmfs=NO). Skipping leaves the node
open and re-prompts next boot — never bricks an unattended/headless boot.

Running before the daemon means the security decision is always made before any
agent can autospawn/node_register, so no cross-component interlock is needed
(rc ordering replaces it). The .secured marker is also the signal a future
colibri change can read to label an unsecured node to mother.

Tests: tests/firstboot-rootpw-test.sh proves marker skip, password validation,
and that the secret is delivered on stdin and NEVER appears in argv (10/10).

Console interactivity (read -t countdown, stty echo-off on /dev/console) must be
verified by booting on osa/bhyve before merge.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
claude-domedog added 1 commit 2026-06-25 05:59:15 +02:00
Reorder the gate to REQUIRE: FILESYSTEMS devfs / BEFORE: clawdie_live_gpu LOGIN
so it runs on the plain early boot text console, before clawdie_live_gpu does its
KMS/framebuffer mode-switch. That removes the console-flush race entirely, so the
sleep 1 + screen-clear workaround is gone. Still before LOGIN, hence before sddm
and colibri_daemon (race-free property preserved).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
claude-domedog added 1 commit 2026-06-25 06:02:11 +02:00
Replace the silent trailing sleeps with a counting-down message so the operator
sees the result (secured / skipped) and a clear cue before clawdie_live_gpu
repaints the screen. Same ~3s pause, now visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
claude-domedog added 1 commit 2026-06-25 06:08:24 +02:00
The '.secured' marker is written but not yet consumed by colibri, so the gate
must not imply colibri/zot are blocked. Reword the skip message to state the
node is UNSECURED and the agent SHOULD NOT register/run while unsecured — true
as a policy statement, without claiming enforcement we haven't built. Upgrade to
'will not' once the colibri .secured interlock lands.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
claude-domedog added 1 commit 2026-06-25 06:16:43 +02:00
clawdie-iso half of the .secured interlock:
- build.sh writes colibri_daemon_require_secured="YES" to the operator image's
  rc.conf. Opt-in so DEPLOYED colibri hosts (shared colibri_daemon.in via the
  FreeBSD port, no firstboot gate) are unaffected — they never set this knob.
- gate skip message upgraded to 'agent will NOT start or register until secured'.

Depends on the colibri-side consumer (colibri_daemon.in prestart): when
colibri_daemon_require_secured is YES and /var/db/colibri/.secured is absent,
export COLIBRI_AUTOSPAWN=NO (after the provider.env source block). Tracked as the
colibri follow-up; both must ship in the same 0.12 image for the message to hold.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Author
Owner

Paired colibri-side work: colibri#183.

This PR's gate writes /var/db/colibri/.secured and sets colibri_daemon_require_secured="YES" on the operator image, and the skip message now promises the agent will not start/register until secured. That promise is only true once colibri#183 lands (knob-guarded autospawn gate in colibri_daemon.in, defaulting OFF so deployed hosts are unaffected). Boot-test the two together on osa before merge.

**Paired colibri-side work:** [colibri#183](https://code.smilepowered.org/clawdie/colibri/issues/183). This PR's gate writes `/var/db/colibri/.secured` and sets `colibri_daemon_require_secured="YES"` on the operator image, and the skip message now promises the agent **will not** start/register until secured. That promise is only true once colibri#183 lands (knob-guarded autospawn gate in `colibri_daemon.in`, defaulting OFF so deployed hosts are unaffected). **Boot-test the two together on osa before merge.**
codex-osa added 1 commit 2026-06-25 07:05:29 +02:00
The rc.conf.sample on the live USB now sets require_secured=YES.
Together with the paired colibri change, this ensures the daemon
disables autospawn until the console gate writes .secured.
clawdie merged commit a29afa4b14 into main 2026-06-25 07:21:34 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: clawdie/clawdie-iso#139
No description provided.