mirror of
https://github.com/openglow-org/forgefirm.git
synced 2026-09-27 16:51:12 -07:00
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:
+47
-5
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 --------------------------------------------------
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user