Merge pull request 'feat(firstboot): force root + operator password on first boot (console gate)' (#139) from force-root-password-on-first-boot into main
Reviewed-on: #139
This commit is contained in:
commit
a29afa4b14
4 changed files with 231 additions and 0 deletions
8
build.sh
8
build.sh
|
|
@ -993,6 +993,11 @@ install_colibri_service() {
|
||||||
fi
|
fi
|
||||||
|
|
||||||
set_config_line "${MOUNT_POINT}/etc/rc.conf" "colibri_daemon_enable=\"${COLIBRI_DAEMON_ENABLE:-YES}\""
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" "colibri_daemon_enable=\"${COLIBRI_DAEMON_ENABLE:-YES}\""
|
||||||
|
# Operator-image only: require the first-boot password gate to have run
|
||||||
|
# (it writes /var/db/colibri/.secured) before the daemon autospawns an
|
||||||
|
# agent. Opt-in so deployed colibri hosts (no firstboot gate) are unaffected.
|
||||||
|
# Consumed by colibri_daemon.in prestart (colibri repo).
|
||||||
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_require_secured="YES"'
|
||||||
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_user="colibri"'
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_user="colibri"'
|
||||||
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_group="colibri"'
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_group="colibri"'
|
||||||
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_data_dir="/var/db/colibri"'
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'colibri_daemon_data_dir="/var/db/colibri"'
|
||||||
|
|
@ -1452,6 +1457,8 @@ EOF
|
||||||
"${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_wifi"
|
"${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_wifi"
|
||||||
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-seed" \
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-seed" \
|
||||||
"${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_seed"
|
"${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_seed"
|
||||||
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-firstboot-rootpw" \
|
||||||
|
"${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_firstboot_rootpw"
|
||||||
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-resolver" \
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-resolver" \
|
||||||
"${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_resolver"
|
"${MOUNT_POINT}/usr/local/etc/rc.d/clawdie_live_resolver"
|
||||||
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-audio" \
|
install -m 0755 "${LIVE_SESSION_DIR}/clawdie-live-audio" \
|
||||||
|
|
@ -1878,6 +1885,7 @@ EOF
|
||||||
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_nvidia_branch=""'
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_gpu_nvidia_branch=""'
|
||||||
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_wifi_enable="YES"'
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_wifi_enable="YES"'
|
||||||
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_seed_enable="YES"'
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_seed_enable="YES"'
|
||||||
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_firstboot_rootpw_enable="YES"'
|
||||||
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_resolver_enable="YES"'
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_resolver_enable="YES"'
|
||||||
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_audio_enable="YES"'
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_audio_enable="YES"'
|
||||||
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_power_enable="YES"'
|
set_config_line "${MOUNT_POINT}/etc/rc.conf" 'clawdie_live_power_enable="YES"'
|
||||||
|
|
|
||||||
158
live/operator-session/clawdie-firstboot-rootpw
Normal file
158
live/operator-session/clawdie-firstboot-rootpw
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
#!/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
|
||||||
|
|
@ -105,6 +105,7 @@ colibri_daemon_logfile="/var/log/colibri/daemon.log"
|
||||||
colibri_daemon_provider_env="/usr/local/etc/colibri/provider.env"
|
colibri_daemon_provider_env="/usr/local/etc/colibri/provider.env"
|
||||||
colibri_daemon_host="\$(/bin/hostname)"
|
colibri_daemon_host="\$(/bin/hostname)"
|
||||||
colibri_daemon_cost_mode="${COLIBRI_COST_MODE}"
|
colibri_daemon_cost_mode="${COLIBRI_COST_MODE}"
|
||||||
|
colibri_daemon_require_secured="YES"
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cat > "${ETC_DIR}/provider.env" <<'EOF'
|
cat > "${ETC_DIR}/provider.env" <<'EOF'
|
||||||
|
|
|
||||||
64
tests/firstboot-rootpw-test.sh
Executable file
64
tests/firstboot-rootpw-test.sh
Executable file
|
|
@ -0,0 +1,64 @@
|
||||||
|
#!/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
|
||||||
Loading…
Add table
Reference in a new issue