clawdie-iso/tests/firstboot-rootpw-test.sh
Sam & Claude 0cd59efa6d feat(firstboot): force root + operator password on first boot (console gate)
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>
2026-06-25 05:54:13 +02:00

64 lines
2.5 KiB
Bash
Executable file

#!/bin/sh
# Logic test for the first-boot password gate (clawdie-firstboot-rootpw).
#
# Exercises the testable security logic on any POSIX host: marker skip,
# password validation, the secured-marker write, and — critically — that the
# password is applied to `pw usermod <user> -h 0` via STDIN (never argv).
# The interactive countdown (_start) needs a real console and is verified by
# booting on osa, not here.
set -u
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
GATE="${SCRIPT_DIR}/../live/operator-session/clawdie-firstboot-rootpw"
[ -r "${GATE}" ] || { echo "FATAL: gate not found at ${GATE}" >&2; exit 2; }
WORK=$(mktemp -d "${TMPDIR:-/tmp}/firstboot-rootpw.XXXXXX") || exit 2
trap 'rm -rf "${WORK}"' EXIT INT TERM
# Fake `pw`: records argv and the password read from stdin, so we can prove the
# secret arrives on stdin and never on the command line.
FAKEPW="${WORK}/fake-pw"
cat >"${FAKEPW}" <<EOF
#!/bin/sh
printf '%s\n' "ARGV: \$*" >> "${WORK}/pw.calls"
IFS= read -r _stdin_pw
printf '%s\n' "STDIN: \${_stdin_pw}" >> "${WORK}/pw.calls"
EOF
chmod +x "${FAKEPW}"
export CLAWDIE_ROOTPW_TEST=1
export SECURED_MARKER="${WORK}/secured"
export PW_BIN="${FAKEPW}"
export ROOTPW_MIN_LEN=8
# shellcheck disable=SC1090
. "${GATE}"
PASS=0; FAIL=0
ok() { PASS=$((PASS+1)); printf ' ok %s\n' "$1"; }
bad() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; }
chk() { [ "$1" -eq 0 ] && ok "$2" || bad "$2"; }
echo "== marker / idempotency =="
_rootpw_secured; chk "$([ $? -ne 0 ] && echo 0 || echo 1)" "not secured when marker absent"
_rootpw_mark_secured
[ -f "${SECURED_MARKER}" ]; chk $? "mark_secured creates the marker"
_rootpw_secured; chk $? "secured when marker present"
echo "== password validation =="
_rootpw_valid "goodpassword" "goodpassword" >/dev/null; chk $? "accepts matching >=8 char password"
{ ! _rootpw_valid "" "" >/dev/null; }; chk $? "rejects empty"
{ ! _rootpw_valid "abc" "abc" >/dev/null; }; chk $? "rejects too short"
{ ! _rootpw_valid "password1" "password2" >/dev/null; }; chk $? "rejects mismatch"
echo "== apply via stdin (secret never in argv) =="
: > "${WORK}/pw.calls"
printf '%s\n' "s3cret-on-stdin" | _rootpw_apply root
grep -q 'ARGV: usermod root -h 0' "${WORK}/pw.calls"; chk $? "pw called: usermod root -h 0"
grep -q 'STDIN: s3cret-on-stdin' "${WORK}/pw.calls"; chk $? "password delivered on STDIN"
{ ! grep -q 'ARGV:.*s3cret-on-stdin' "${WORK}/pw.calls"; }; chk $? "password NEVER appears in argv"
echo
echo "RESULT: ${PASS} passed, ${FAIL} failed"
[ "${FAIL}" -eq 0 ] || exit 1
exit 0