#!/bin/sh # Clawdie first-boot password gate. # # Forces the operator to set a root + operator (clawdie) password on the FIRST # boot, on the text console, BEFORE the GUI (sddm) and BEFORE the colibri daemon # autospawns an agent. Running before the daemon means the security decision is # always made before any agent can act — no cross-component interlock needed. # # Design: # - Idempotent via a persistent success marker (/var is persistent on this # image: varmfs="NO"). Marker present -> silent exit. # - A countdown to ENGAGE (read -t). If the operator does not engage, boot # continues with passwords UNSET and the gate re-prompts next boot (never # bricks an unattended/headless boot). Once set, never prompts again. # - Passwords are read with echo off (stty) and applied via `pw usermod -h 0`, # which reads the new password from STDIN — never in argv/ps, never near the # agent/LLM. # - On success, writes /var/db/colibri/.secured so the colibri daemon can # reflect security state to mother (label the node "unsecured" while absent). # The daemon-side consumption of this marker is a separate (colibri) change. # # Functions below are sourced and unit-tested on a non-FreeBSD host with # CLAWDIE_ROOTPW_TEST=1 (which skips the rc.subr handoff). The interactive # countdown lives only in _start and is not exercised by the logic test. # PROVIDE: clawdie_firstboot_rootpw # REQUIRE: FILESYSTEMS devfs # BEFORE: clawdie_live_gpu LOGIN # KEYWORD: nojail # # Ordering: runs on the plain early boot text console, BEFORE clawdie_live_gpu # does its KMS/framebuffer mode-switch (so there is no console-flush race) and # BEFORE LOGIN (so before sddm and before colibri_daemon, which REQUIRE LOGIN). # Needs only FILESYSTEMS + devfs (console, /etc/master.passwd, /var marker, pw). if [ -r /etc/rc.subr ]; then . /etc/rc.subr fi name="clawdie_firstboot_rootpw" rcvar="${name}_enable" start_cmd="${name}_start" stop_cmd=":" : "${clawdie_firstboot_rootpw_enable:=YES}" # Overridable for tests. SECURED_MARKER="${SECURED_MARKER:-/var/db/colibri/.secured}" PW_BIN="${PW_BIN:-/usr/sbin/pw}" ROOTPW_CONSOLE="${ROOTPW_CONSOLE:-/dev/console}" ROOTPW_COUNTDOWN="${ROOTPW_COUNTDOWN:-15}" ROOTPW_MIN_LEN="${ROOTPW_MIN_LEN:-8}" # --- pure logic (unit-tested) ----------------------------------------------- # Already secured? (marker present) _rootpw_secured() { [ -e "${SECURED_MARKER}" ] } # Validate a password pair. Echoes a reason and returns 1 on failure. _rootpw_valid() { _p1="$1"; _p2="$2" if [ -z "${_p1}" ]; then echo "empty password"; return 1; fi if [ "${_p1}" != "${_p2}" ]; then echo "passwords do not match"; return 1; fi if [ "${#_p1}" -lt "${ROOTPW_MIN_LEN}" ]; then echo "too short (minimum ${ROOTPW_MIN_LEN} characters)"; return 1 fi return 0 } # Apply a password to a user. Reads the password from STDIN (pw usermod -h 0). _rootpw_apply() { "${PW_BIN}" usermod "$1" -h 0 } # Record success so the daemon sees a secured node and the gate stops prompting. _rootpw_mark_secured() { mkdir -p "$(dirname "${SECURED_MARKER}")" 2>/dev/null || true # mirror the daemon's ownership intent; best-effort (no-op under test). chown colibri:colibri "$(dirname "${SECURED_MARKER}")" 2>/dev/null || true chmod 0750 "$(dirname "${SECURED_MARKER}")" 2>/dev/null || true : > "${SECURED_MARKER}" chmod 0644 "${SECURED_MARKER}" 2>/dev/null || true } # --- interactive (console only; not unit-tested) ---------------------------- # Prompt for one account, echo off, loop until a valid pair is applied. _rootpw_prompt_and_set() { _user="$1"; _label="$2" while :; do stty -echo 2>/dev/null printf ' %s password: ' "${_label}"; IFS= read -r _p1; printf '\n' printf ' confirm %s: ' "${_label}"; IFS= read -r _p2; printf '\n' stty echo 2>/dev/null if _why="$(_rootpw_valid "${_p1}" "${_p2}")"; then printf '%s\n' "${_p1}" | _rootpw_apply "${_user}" _p1=; _p2=; _why= printf ' -> %s password set.\n\n' "${_label}" return 0 fi _p1=; _p2= printf ' ! %s — try again.\n' "${_why}" done } # Visible "Continuing in 3s 2s 1s" countdown before boot resumes, so the # operator can read the result before clawdie_live_gpu repaints the screen. _rootpw_continue_countdown() { _n="${1:-3}" printf ' Continuing in ' while [ "${_n}" -gt 0 ]; do printf '%ss ' "${_n}" sleep 1 _n=$((_n - 1)) done printf '... proceeding.\n' } clawdie_firstboot_rootpw_start() { _rootpw_secured && return 0 # Talk to the operator on the system console. We run before the GPU/KMS # mode-switch, so this is the stable early text console — no settle/clear # workaround needed. exec < "${ROOTPW_CONSOLE}" > "${ROOTPW_CONSOLE}" 2>&1 printf '\n================ FIRST BOOT — SECURE THIS NODE ================\n\n' printf ' This stick boots with NO root password. Set one now.\n' printf ' WRITE BOTH PASSWORDS ON PAPER — there is no recovery.\n\n' printf ' Press ENTER within %ss to set passwords' "${ROOTPW_COUNTDOWN}" printf ' (otherwise skipped) ... ' if IFS= read -r -t "${ROOTPW_COUNTDOWN}" _ans; then printf '\n\n' _rootpw_prompt_and_set root "ROOT (admin)" _rootpw_prompt_and_set clawdie "OPERATOR (clawdie)" _rootpw_mark_secured printf ' Node secured.\n' _rootpw_continue_countdown 3 else printf '\n\n [skipped] root/operator passwords NOT set — node is UNSECURED.\n' printf ' The colibri agent will NOT start or register with mother until a\n' printf ' password is set (colibri_daemon_require_secured). Set one to activate\n' printf ' this node. You will be prompted again on the next boot.\n' _rootpw_continue_countdown 3 fi return 0 } # On FreeBSD, hand off to rc.subr. Under test, skip so functions can be sourced. if [ -n "${CLAWDIE_ROOTPW_TEST:-}" ]; then : elif command -v run_rc_command >/dev/null 2>&1; then load_rc_config "${name}" run_rc_command "$1" fi