Mount the rootfs read-only on both images

The rootfs mounted read-write, so a slot ran with its own files open to
change, and the factory-slot mounts rode along on the release image.
Both images now carry the read-only-rootfs feature: the ro root line and
the rcS default, the volatile links made at rootfs time, a writable copy
of /var/lib at boot, a build failure for a post-install that needs the
machine, and the removal of shadow, base-passwd, update-rc.d and
update-alternatives.

What must last or change at run time is handled file by file:

- forgefirm-users renders the four account files from the record into
  /run/forgefirm/accounts and bind-mounts each copy over its /etc file
  (useradd and the rest are gone with shadow); a render writes through
  the mount, and the image's own files apply until the first render.
- forgefirm-banner bind-mounts a copy of /etc/issue and writes the
  address block through it.
- sshd keeps its host keys under /data/forgefirm/ssh, so the fingerprint
  survives updates; both sshd configs carry the same HostKey lines.
- forgefirm-logging passes logrotate a state file under /var/run
  (logrotate refuses to run without one).
- forgefirm-persist points the boot timestamp and the random seed at
  /data/forgefirm.

The dev image appends the /factory slot mounts, without nofail (busybox
mount hands it to the kernel, which rejects it). The rootfs command
entries lose their semicolons: on scarthgap the value is the task's
vardeps, split on whitespace, so "name;" left the function body out of
the signature and a changed body did not remake the rootfs; with the
bodies tracked, the dev image's DATETIME string needs a vardepsexclude.
release.sh gains the read-only gate (root ro, no /factory line,
ROOTFS_READ_ONLY=yes, host keys on /data). image.health checks the
mounts, the account binds, the banner bind, the host keys and the
dev-only /factory mounts.

Proven on the bench reference (dev image 20260909140901): / ro, /data
rw, /var/lib a tmpfs copy, the four account files and /etc/issue bound
from tmpfs, the host keys in /data/forgefirm/ssh, no "Read-only file
system" line in any log; forgectrl.auth and commission.account-login (a
temporary account rendered, logged in over HTTPS and removed again),
kernel.latch-locked-idle and motion.liveness-probe PASS; logrotate runs
with the volatile state. forgetest unit tests 335 OK; both images build
clean, and debugfs on the built rootfs shows every setting above.
This commit is contained in:
ScottW514
2026-09-09 11:02:10 -04:00
parent 2936890eaa
commit 8af8b197ee
14 changed files with 309 additions and 79 deletions
@@ -7,10 +7,15 @@
# is appended when absent. Called by the init script at boot and by the
# udhcpc hook on every lease event. Idempotent: the file is written only
# when the block changes.
#
# The rootfs is read-only: at the first change after boot the file is
# bind-mounted from a copy under /run/forgefirm (tmpfs) and the block is
# written through the mount. Before that the image's own file shows.
PATH=/sbin:/usr/sbin:/bin:/usr/bin
ISSUE=/etc/issue
STATE=/run/forgefirm/issue
MARK_BEGIN='# ForgeFIRM addresses'
MARK_END='# end'
PANEL='Control panel: https://forgefirm.local/'
@@ -51,13 +56,21 @@ old=$(awk -v b="$MARK_BEGIN" -v e="$MARK_END" \
'$0 == b { p = 1 } p { print } $0 == e { p = 0 }' "$ISSUE")
[ "$new" = "$old" ] && exit 0
tmp="$ISSUE.tmp.$$"
# A mount at the file, read from /proc/mounts (mountpoint(1) judges a
# file by its device numbers alone).
if ! awk -v t="$ISSUE" '$2 == t { f = 1 } END { exit !f }' /proc/mounts; then
mkdir -p "${STATE%/*}" \
&& cp -p "$ISSUE" "$STATE" \
&& mount --bind "$STATE" "$ISSUE" || exit 1
fi
tmp="$STATE.tmp.$$"
awk -v b="$MARK_BEGIN" -v e="$MARK_END" -v blk="$new" '
$0 == b { print blk; seen = 1; skip = 1; next }
$0 == e && skip { skip = 0; next }
!skip { print }
END { if (!seen) print blk }
' "$ISSUE" > "$tmp" || { rm -f "$tmp"; exit 1; }
chmod 0644 "$tmp"
mv -f "$tmp" "$ISSUE"
cat "$tmp" > "$ISSUE"
rm -f "$tmp"
exit 0