From e9443a60eff251be73a62b74fb601622377ddb14 Mon Sep 17 00:00:00 2001 From: ScottW514 Date: Fri, 14 Aug 2026 16:20:02 -0400 Subject: [PATCH] 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. --- docs/BRINGUP.md | 52 +++++++++++++++++-- kas/forgefirm-glowforge.yml | 11 ++-- .../images/forgefirm-image-dev.bb | 4 ++ scripts/install-forgefirm.sh | 21 ++++---- scripts/release.sh | 19 +++++++ 5 files changed, 86 insertions(+), 21 deletions(-) diff --git a/docs/BRINGUP.md b/docs/BRINGUP.md index 9a04c83..a5768a8 100644 --- a/docs/BRINGUP.md +++ b/docs/BRINGUP.md @@ -1,10 +1,52 @@ # ForgeFIRM bring-up status & cold-start runbook -Last updated: **2026-08-14** — **audit remediation Phases 0 + 1 landed** -(from an independent whole-tree audit dated 2026-08-13; the remediation -is sequenced behind two gates — GATE A, uncommanded energy, before any -further live-fire; GATE B, control surface + release, before any -published release). Phase 0: user-facing laser-safety and +Last updated: **2026-08-14** — **audit remediation Phases 0, 1, and 2 +landed** (from an independent whole-tree audit dated 2026-08-13; the +remediation is sequenced behind two gates — GATE A, uncommanded energy, +before any further live-fire; GATE B, control surface + release, before +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, INSTALL.md "Regulatory and legal" + updater-first update path, a persistent panel safety banner), the walkthrough no longer claims the diff --git a/kas/forgefirm-glowforge.yml b/kas/forgefirm-glowforge.yml index 6c1823b..cfdf119 100644 --- a/kas/forgefirm-glowforge.yml +++ b/kas/forgefirm-glowforge.yml @@ -115,12 +115,11 @@ local_conf_header: PREFERRED_PROVIDER_virtual/kernel = "linux-fslc" PREFERRED_VERSION_linux-fslc = "6.12%" - # Development image features. debug-tweaks gives a passwordless root login for - # bench bring-up; kas generates its own local.conf (it does NOT inherit poky's - # local.conf.sample), so this must be set explicitly. Drop it (and set a real - # root password via extrausers) before any production/field image. - image-debug: | - EXTRA_IMAGE_FEATURES = "debug-tweaks" + # NOTE: debug-tweaks (passwordless root) is deliberately NOT set here. It + # would apply to every target built from this config, including the release + # forgefirm-image. It lives in forgefirm-image-dev.bb's IMAGE_FEATURES so a + # single build yields a hardened release image and a debug dev image. + # release.sh gates the release rootfs against a passwordless root entry. build-tweaks: | # Parallelism for the 12-core / 16 GB WSL2 VM. (The earlier mid-build deaths diff --git a/meta-forgefirm/recipes-forgefirm/images/forgefirm-image-dev.bb b/meta-forgefirm/recipes-forgefirm/images/forgefirm-image-dev.bb index 31b5852..365bcab 100644 --- a/meta-forgefirm/recipes-forgefirm/images/forgefirm-image-dev.bb +++ b/meta-forgefirm/recipes-forgefirm/images/forgefirm-image-dev.bb @@ -8,8 +8,12 @@ IMAGE_INSTALL += " \ 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 += " \ tools-debug \ + debug-tweaks \ " # Dev images boot from SD, never from a 200 MiB eMMC slot: lift the slot diff --git a/scripts/install-forgefirm.sh b/scripts/install-forgefirm.sh index 8c847d0..3fd9607 100644 --- a/scripts/install-forgefirm.sh +++ b/scripts/install-forgefirm.sh @@ -17,7 +17,6 @@ # With no argument the latest release .fw is downloaded from GitHub. 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" FW_FILE="/data/forgefirm/forgefirm.fw" 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) [ -n "$NEWVER" ] || { umount "$MP"; die "new rootfs has no ForgeFIRM version stamp"; } [ -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" rmdir "$MP" 2>/dev/null echo -e "${ASTERISK}Slot $TARGET now holds ForgeFIRM $NEWVER" # --- ffboot for the factory side ---------------------------------------------- -if curl -fL "$FFBOOT_URL" --output /data/ffboot.new 2>/dev/null \ - && [ -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 +mv /data/ffboot.new /data/ffboot chmod +x /data/ffboot # --- flip the boot selection -------------------------------------------------- diff --git a/scripts/release.sh b/scripts/release.sh index be0df9f..9231cac 100644 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -131,6 +131,25 @@ STAMP=$(debugfs -R "cat /etc/forgefirm-version" "$EXT4" 2>/dev/null) [ "$STAMP" = "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 ==" STAGE="${RELEASE_STAGING_DIR:-$REPO/release-staging}/v$VERSION" mkdir -p "$STAGE"