Keep debug-tweaks out of release images; harden the installer

- Move the passwordless-root debug-tweaks image feature out of the
  shared kas config into forgefirm-image-dev.bb, so the release
  forgefirm-image built from the same config is not passwordless-root.
  release.sh gains a gate that reads the built rootfs /etc/shadow and
  fails on an empty root password, plus a config-level guard that
  debug-tweaks is not present in the resolved kas dump. (B-1)
- The installer copies ffboot out of the signature-verified new rootfs
  it already mounts, instead of fetching and executing it from a mutable
  GitHub raw ref. (B-2)
- Record audit remediation Phase 2 (GATE B) status in BRINGUP.md,
  including the bench pass still required to close the gate.
This commit is contained in:
ScottW514
2026-08-14 16:20:02 -04:00
parent cc927aca5f
commit e9443a60ef
5 changed files with 86 additions and 21 deletions
+47 -5
View File
@@ -1,10 +1,52 @@
# ForgeFIRM bring-up status & cold-start runbook # ForgeFIRM bring-up status & cold-start runbook
Last updated: **2026-08-14** — **audit remediation Phases 0 + 1 landed** Last updated: **2026-08-14** — **audit remediation Phases 0, 1, and 2
(from an independent whole-tree audit dated 2026-08-13; the remediation landed** (from an independent whole-tree audit dated 2026-08-13; the
is sequenced behind two gates — GATE A, uncommanded energy, before any remediation is sequenced behind two gates — GATE A, uncommanded energy,
further live-fire; GATE B, control surface + release, before any before any further live-fire; GATE B, control surface + release, before
published release). Phase 0: user-facing laser-safety and any published release).
**Phase 2 (GATE B, control surface + release) is code-complete and
host-verified.** forgectrl now has one auth layer applied to every
endpoint (`src/auth.c`): a first-boot bearer token in `/data`, embedded
in the panel and required on every state-changing call; a Host
address-literal check plus `Sec-Fetch-Site`/`Origin` validation that
refuses cross-site (CSRF) and DNS-rebinding requests; `/cool/state`
restricted to a loopback peer so a LAN client can no longer spoof a
thermal stand-down (F-1, F-2). The irrevocable fuse view and
unsigned-firmware installs additionally require the physical button held
(F-19, F-1). A native unit test of the real `auth.c` decision logic
passes all ten cases (authorized POST allowed; CSRF refused even with a
token; rebinding host refused; missing/wrong token refused; panel
bootstrap refused over a rebinding host; loopback report allowed, LAN
spoof refused). Also fixed: the `reply_settings` accumulator overflow
and its unbounded validators (F-4, F-18); cooling-tunable caps + a
resume-below-max cross-check + a loud flow-checks-disabled indicator
(F-5); the upload path is auth+idle+job gated (F-9); the liveness probe
refuses to move the gantry with a lid/interlock open (F-13);
`update_job_running()` cross-checks added to the diag and mode-switch
gates (F-14, partial — targeted checks, not yet a single-lock arbiter);
`machine_is_idle()` fails **closed** on a read error so a connection
flood can no longer read as idle mid-cut (X-2); the fd ceiling is raised
(F-15, partial — the MHD connection cap and moving the camera
`ensure_engine` `popen()`s out of the HTTP callback are deferred);
`esc()` and the panel attribute/innerHTML interpolations are escaped
(F-20); the restore `sh -c` double-shell is gone and the archive name is
charset-restricted (B-9). Release engineering: `debug-tweaks` moved out
of the shared kas config into `forgefirm-image-dev.bb` so the release
`forgefirm-image` is no longer passwordless-root, with a `release.sh`
gate that reads the built rootfs `/etc/shadow` and fails on an empty
root password (B-1); the installer copies `ffboot` out of the
signature-verified new rootfs instead of curl-ing it from a mutable ref
(B-2); `CONFIG_PANIC_ON_OOPS=y` + `panic=10` route a kernel oops into
the laser-safing panic handler (B-3, rides the image flash). **GATE B
requires a bench pass** (a CSRF probe from a second host rejected; a
spoofed `/cool/state` no longer drops the fans; a 13-max-length
`POST /settings` does not crash the daemon; a built release image shows
a non-empty root password), after which — combined with Phase 0's
safety/regulatory text — the first public `.fw` is allowed.
Phase 0: user-facing laser-safety and
regulatory text is in place (LIGHTBURN.md "Before you cut", README, regulatory text is in place (LIGHTBURN.md "Before you cut", README,
INSTALL.md "Regulatory and legal" + updater-first update path, a INSTALL.md "Regulatory and legal" + updater-first update path, a
persistent panel safety banner), the walkthrough no longer claims the persistent panel safety banner), the walkthrough no longer claims the
+5 -6
View File
@@ -115,12 +115,11 @@ local_conf_header:
PREFERRED_PROVIDER_virtual/kernel = "linux-fslc" PREFERRED_PROVIDER_virtual/kernel = "linux-fslc"
PREFERRED_VERSION_linux-fslc = "6.12%" PREFERRED_VERSION_linux-fslc = "6.12%"
# Development image features. debug-tweaks gives a passwordless root login for # NOTE: debug-tweaks (passwordless root) is deliberately NOT set here. It
# bench bring-up; kas generates its own local.conf (it does NOT inherit poky's # would apply to every target built from this config, including the release
# local.conf.sample), so this must be set explicitly. Drop it (and set a real # forgefirm-image. It lives in forgefirm-image-dev.bb's IMAGE_FEATURES so a
# root password via extrausers) before any production/field image. # single build yields a hardened release image and a debug dev image.
image-debug: | # release.sh gates the release rootfs against a passwordless root entry.
EXTRA_IMAGE_FEATURES = "debug-tweaks"
build-tweaks: | build-tweaks: |
# Parallelism for the 12-core / 16 GB WSL2 VM. (The earlier mid-build deaths # Parallelism for the 12-core / 16 GB WSL2 VM. (The earlier mid-build deaths
@@ -8,8 +8,12 @@ IMAGE_INSTALL += " \
forgectrl \ forgectrl \
" "
# debug-tweaks (passwordless root, root SSH login) belongs ONLY to the dev
# image - never the release image. It lives here, not in the shared kas
# local.conf, so the release forgefirm-image cannot inherit it.
IMAGE_FEATURES += " \ IMAGE_FEATURES += " \
tools-debug \ tools-debug \
debug-tweaks \
" "
# Dev images boot from SD, never from a 200 MiB eMMC slot: lift the slot # Dev images boot from SD, never from a 200 MiB eMMC slot: lift the slot
+11 -10
View File
@@ -17,7 +17,6 @@
# With no argument the latest release .fw is downloaded from GitHub. # With no argument the latest release .fw is downloaded from GitHub.
RELEASE_FW_URL="https://github.com/ScottW514/forgefirm/releases/latest/download/forgefirm.fw" RELEASE_FW_URL="https://github.com/ScottW514/forgefirm/releases/latest/download/forgefirm.fw"
FFBOOT_URL="https://raw.githubusercontent.com/ScottW514/forgefirm/master/scripts/ffboot"
ARCHIVE_DIR="/data/forgefirm/archive" ARCHIVE_DIR="/data/forgefirm/archive"
FW_FILE="/data/forgefirm/forgefirm.fw" FW_FILE="/data/forgefirm/forgefirm.fw"
MIN_DATA_FREE_KB=300000 MIN_DATA_FREE_KB=300000
@@ -281,20 +280,22 @@ mount -o ro -t ext4 "/dev/mmcblk2p$TARGET" "$MP" || die "new rootfs does not mou
NEWVER=$(cat "$MP/etc/forgefirm-version" 2>/dev/null) NEWVER=$(cat "$MP/etc/forgefirm-version" 2>/dev/null)
[ -n "$NEWVER" ] || { umount "$MP"; die "new rootfs has no ForgeFIRM version stamp"; } [ -n "$NEWVER" ] || { umount "$MP"; die "new rootfs has no ForgeFIRM version stamp"; }
[ -f "$MP/boot/zImage" ] || { umount "$MP"; die "new rootfs has no kernel"; } [ -f "$MP/boot/zImage" ] || { umount "$MP"; die "new rootfs has no kernel"; }
# Take ffboot (the factory-side boot-slot tool) from the rootfs we just
# signature-verified and mounted read-only - never fetch+exec it from a
# mutable network ref, which would be an unverified code path in an
# otherwise signature-gated install.
[ -s "$MP/usr/sbin/ffboot" ] \
|| { umount "$MP"; die "new rootfs does not contain /usr/sbin/ffboot"; }
cp "$MP/usr/sbin/ffboot" /data/ffboot.new \
|| { umount "$MP"; die "cannot copy ffboot out of the new rootfs"; }
umount "$MP" umount "$MP"
rmdir "$MP" 2>/dev/null rmdir "$MP" 2>/dev/null
echo -e "${ASTERISK}Slot $TARGET now holds ForgeFIRM $NEWVER" echo -e "${ASTERISK}Slot $TARGET now holds ForgeFIRM $NEWVER"
# --- ffboot for the factory side ---------------------------------------------- # --- ffboot for the factory side ----------------------------------------------
if curl -fL "$FFBOOT_URL" --output /data/ffboot.new 2>/dev/null \ mv /data/ffboot.new /data/ffboot
&& [ -s /data/ffboot.new ]; then
mv /data/ffboot.new /data/ffboot
else
rm -f /data/ffboot.new
[ -x /data/ffboot ] \
|| die "ffboot download failed and no /data/ffboot is present"
echo -e "${ASTERISK}ffboot download failed; keeping the existing /data/ffboot"
fi
chmod +x /data/ffboot chmod +x /data/ffboot
# --- flip the boot selection -------------------------------------------------- # --- flip the boot selection --------------------------------------------------
+19
View File
@@ -131,6 +131,25 @@ STAMP=$(debugfs -R "cat /etc/forgefirm-version" "$EXT4" 2>/dev/null)
[ "$STAMP" = "v$VERSION" ] \ [ "$STAMP" = "v$VERSION" ] \
|| die "rootfs stamp is '$STAMP', expected 'v$VERSION'" || die "rootfs stamp is '$STAMP', expected 'v$VERSION'"
# Back-door gate: the release image must not ship a passwordless root. A
# debug-tweaks image sets root's password field empty (root::...); a
# hardened image leaves it locked (root:*: / root:!:) or hashed. Read the
# actual built shadow file - this catches the flag however it slipped in
# (recipe, local.conf, an inherited class).
ROOT_PW=$(debugfs -R "cat /etc/shadow" "$EXT4" 2>/dev/null \
| awk -F: '$1=="root"{print $2; exit}')
[ -n "$ROOT_PW" ] \
|| die "release rootfs has a passwordless root (debug-tweaks leaked into forgefirm-image?)"
echo "root login gate OK (root password field is not empty)"
# Config-level guard: debug-tweaks must not sit in the shared kas config,
# where it would apply to every target including the release image.
if ( cd "$REPO" && kas dump kas/forgefirm-glowforge.yml 2>/dev/null ) \
| grep -q 'debug-tweaks'; then
die "debug-tweaks appears in the resolved kas config - it must live only in forgefirm-image-dev.bb"
fi
echo "kas config gate OK (no debug-tweaks in the shared config)"
echo "== pack + sign ==" echo "== pack + sign =="
STAGE="${RELEASE_STAGING_DIR:-$REPO/release-staging}/v$VERSION" STAGE="${RELEASE_STAGING_DIR:-$REPO/release-staging}/v$VERSION"
mkdir -p "$STAGE" mkdir -p "$STAGE"