From 8d5b86d737028cf4ae60d40aba2e847f815018b0 Mon Sep 17 00:00:00 2001 From: Claude Fable 5 Date: Sat, 29 Aug 2026 18:19:16 +0200 Subject: [PATCH] fix(rpi-image): patch the host OS automatically, unfreeze the Docker engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-update covers only FTW's own containers; the flashed Pi never received an OS security update, and the image build deleted Docker's apt source, so even a manual `apt upgrade` could not patch the engine. - install unattended-upgrades: Debian security + the Raspberry Pi archive (the kernel/firmware source has no security pocket), with automatic reboots pinned off — a reboot stops dispatch - park docker.list as docker.list.disabled during the image build (same export-image OOM workaround) and restore it in ftw-firstboot, so engine patching becomes a normal operator apt action - document the self-update/host boundary and a retrofit for devices flashed from older images Fixes srcfl/ftw#770 Co-authored-by: HuggeK <48095810+HuggeK@users.noreply.github.com> Claude-Session: https://claude.ai/code/session_012taMkhFbYYNyVQmCTU26sn --- .changeset/pi-host-security-updates.md | 5 ++++ .../stage-ftw/00-install-packages/00-packages | 1 + .../pi-gen/stage-ftw/01-ftw-setup/00-run.sh | 18 ++++++++++++--- .../01-ftw-setup/files/20auto-upgrades | 6 +++++ .../files/52ftw-unattended-upgrades | 18 +++++++++++++++ .../stage-ftw/01-ftw-setup/files/firstboot.sh | 9 ++++++++ docs/operations.md | 5 +++- docs/rpi-image.md | 23 +++++++++++++++++++ docs/self-update.md | 9 ++++++++ 9 files changed, 90 insertions(+), 4 deletions(-) create mode 100644 .changeset/pi-host-security-updates.md create mode 100644 deploy/pi-gen/stage-ftw/01-ftw-setup/files/20auto-upgrades create mode 100644 deploy/pi-gen/stage-ftw/01-ftw-setup/files/52ftw-unattended-upgrades diff --git a/.changeset/pi-host-security-updates.md b/.changeset/pi-host-security-updates.md new file mode 100644 index 000000000..bf064f6c6 --- /dev/null +++ b/.changeset/pi-host-security-updates.md @@ -0,0 +1,5 @@ +--- +"ftw": patch +--- + +Raspberry Pi image: apply host OS security updates automatically (Debian security + Raspberry Pi archives via unattended-upgrades, automatic reboot off) and restore Docker's apt source on first boot so the engine can be patched at all; document that self-update never covers the host. diff --git a/deploy/pi-gen/stage-ftw/00-install-packages/00-packages b/deploy/pi-gen/stage-ftw/00-install-packages/00-packages index 2e9e1a09e..5523d085d 100644 --- a/deploy/pi-gen/stage-ftw/00-install-packages/00-packages +++ b/deploy/pi-gen/stage-ftw/00-install-packages/00-packages @@ -4,3 +4,4 @@ curl jq network-manager raspberrypi-sys-mods +unattended-upgrades diff --git a/deploy/pi-gen/stage-ftw/01-ftw-setup/00-run.sh b/deploy/pi-gen/stage-ftw/01-ftw-setup/00-run.sh index 9e8aaa235..9d6fc11b9 100755 --- a/deploy/pi-gen/stage-ftw/01-ftw-setup/00-run.sh +++ b/deploy/pi-gen/stage-ftw/01-ftw-setup/00-run.sh @@ -33,9 +33,12 @@ systemctl enable avahi-daemon.service systemctl enable NetworkManager.service # pi-gen's export-image stage runs another apt update under qemu. Leaving # Docker's third-party apt source enabled has repeatedly OOMed that step on -# GitHub hosted runners after Docker is already installed. App updates pull -# containers from GHCR, so the image build does not need this repo afterward. -rm -f /etc/apt/sources.list.d/docker.list +# GitHub hosted runners after Docker is already installed. Park the source +# under a name apt ignores (it reads only *.list/*.sources) instead of +# deleting it; ftw-firstboot restores it on the flashed device, so the +# engine can be patched with a plain `apt upgrade` for the appliance's +# whole service life (srcfl/ftw#770). +mv /etc/apt/sources.list.d/docker.list /etc/apt/sources.list.d/docker.list.disabled # /etc/hosts entry prevents sudo's "unable to resolve host ftw" # warning on first boot. pi-gen writes /etc/hostname from # TARGET_HOSTNAME but leaves /etc/hosts at the stock Raspberry Pi @@ -85,6 +88,15 @@ install -m 0644 files/mosquitto.conf "${ROOTFS_DIR}/opt/ftw/mosquitto/con install -m 0755 files/firstboot.sh "${ROOTFS_DIR}/usr/local/sbin/ftw-firstboot" install -m 0644 files/firstboot.service "${ROOTFS_DIR}/etc/systemd/system/ftw-firstboot.service" +# Host OS security updates (srcfl/ftw#770). Self-update covers only FTW's +# own containers; the host relies on unattended-upgrades (installed via +# 00-install-packages). 20auto-upgrades switches the apt-daily timers' +# work on; 52ftw-unattended-upgrades adds the Raspberry Pi archive — the +# kernel/firmware source, which has no separate security pocket — and +# pins automatic reboots off, because a reboot stops dispatch. +install -m 0644 files/20auto-upgrades "${ROOTFS_DIR}/etc/apt/apt.conf.d/20auto-upgrades" +install -m 0644 files/52ftw-unattended-upgrades "${ROOTFS_DIR}/etc/apt/apt.conf.d/52ftw-unattended-upgrades" + # Sudoers fragment for ftw. Stage2 installs an `010_-nopasswd` for # the build-time FIRST_USER (ftw); we drop this belt-and-suspenders copy # so the default ftw recovery account keeps passwordless sudo even if a diff --git a/deploy/pi-gen/stage-ftw/01-ftw-setup/files/20auto-upgrades b/deploy/pi-gen/stage-ftw/01-ftw-setup/files/20auto-upgrades new file mode 100644 index 000000000..51a2665ec --- /dev/null +++ b/deploy/pi-gen/stage-ftw/01-ftw-setup/files/20auto-upgrades @@ -0,0 +1,6 @@ +// Switch the apt-daily / apt-daily-upgrade timers' work on: refresh +// package lists and run unattended-upgrade once a day. Normally written +// by debconf on `dpkg-reconfigure unattended-upgrades`; the image ships +// it directly. +APT::Periodic::Update-Package-Lists "1"; +APT::Periodic::Unattended-Upgrade "1"; diff --git a/deploy/pi-gen/stage-ftw/01-ftw-setup/files/52ftw-unattended-upgrades b/deploy/pi-gen/stage-ftw/01-ftw-setup/files/52ftw-unattended-upgrades new file mode 100644 index 000000000..573a96872 --- /dev/null +++ b/deploy/pi-gen/stage-ftw/01-ftw-setup/files/52ftw-unattended-upgrades @@ -0,0 +1,18 @@ +// FTW appliance policy for unattended-upgrades (srcfl/ftw#770). APT +// config lists append across files, so these patterns extend the Debian +// defaults in 50unattended-upgrades rather than replacing them. +// +// The Raspberry Pi archive is where the kernel, firmware and bootloader +// come from, and it has no separate security pocket — omitting it would +// leave exactly the packages this policy exists for unpatched. Docker's +// repository is deliberately NOT listed: an engine upgrade restarts every +// container, so it stays a manual, operator-timed `apt upgrade`. +Unattended-Upgrade::Origins-Pattern { + "origin=Debian,codename=${distro_codename}-security,label=Debian-Security"; + "origin=Raspberry Pi Foundation,codename=${distro_codename}"; +}; + +// This is an energy controller: a reboot stops dispatch. A staged kernel +// waits for an operator-chosen reboot; everything else in the origins +// above takes effect without one. +Unattended-Upgrade::Automatic-Reboot "false"; diff --git a/deploy/pi-gen/stage-ftw/01-ftw-setup/files/firstboot.sh b/deploy/pi-gen/stage-ftw/01-ftw-setup/files/firstboot.sh index 8ebef52f2..1761f299d 100755 --- a/deploy/pi-gen/stage-ftw/01-ftw-setup/files/firstboot.sh +++ b/deploy/pi-gen/stage-ftw/01-ftw-setup/files/firstboot.sh @@ -21,6 +21,15 @@ echo "[$(date -Is)] ftw-firstboot starting" cd /opt/ftw +# The image build parks Docker's apt source so pi-gen's export-image apt +# step cannot OOM on it (see 01-ftw-setup/00-run.sh). Restore it on the +# real device: without it the engine is frozen at the image's build +# version for the appliance's whole service life (srcfl/ftw#770). The +# signing key at /etc/apt/keyrings/docker.asc was never removed. +if [ -f /etc/apt/sources.list.d/docker.list.disabled ]; then + mv /etc/apt/sources.list.d/docker.list.disabled /etc/apt/sources.list.d/docker.list +fi + # Retry loop: GHCR and general LAN DHCP can be flaky for the first # couple of minutes after boot, and slow connections may need many # minutes per attempt. Retry indefinitely — the sentinel is only diff --git a/docs/operations.md b/docs/operations.md index 8e95e98a5..814805673 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -31,7 +31,10 @@ docker compose up -d ``` The UI updater performs an immutable pull and recreate through the updater -sidecar. See [self-update.md](self-update.md). +sidecar; it never patches the host OS or Docker engine — that is the +operator's job on a self-managed host, and automatic on the +[Raspberry Pi image](rpi-image.md#host-os-security-updates). See +[self-update.md](self-update.md). ## Persistent state diff --git a/docs/rpi-image.md b/docs/rpi-image.md index 00285a845..d10412e52 100644 --- a/docs/rpi-image.md +++ b/docs/rpi-image.md @@ -108,6 +108,29 @@ The installer image is independent of application releases. New images pull the current stable containers on first boot; installed systems use the normal beta/stable updater. +### Host OS security updates + +The in-app updater covers FTW's own components — Core, Optimizer, drivers — +never the host underneath them. The image keeps the host patched with +`unattended-upgrades`: Debian security updates and the Raspberry Pi archive +(kernel, firmware, bootloader) apply automatically once a day. The Pi never +reboots on its own — a reboot stops dispatch — so an installed kernel takes +effect at the next reboot you choose. + +The Docker engine comes from Docker's own apt repository, restored on first +boot, and updates only with a manual `sudo apt update && sudo apt upgrade`, +because an engine upgrade restarts the whole container stack. + +Devices flashed from an image built before this policy can adopt it: + +```bash +sudo apt-get update && sudo apt-get install -y unattended-upgrades +base=https://raw.githubusercontent.com/srcfl/ftw/master/deploy/pi-gen/stage-ftw/01-ftw-setup/files +sudo curl -fsSL "${base}/20auto-upgrades" -o /etc/apt/apt.conf.d/20auto-upgrades +sudo curl -fsSL "${base}/52ftw-unattended-upgrades" -o /etc/apt/apt.conf.d/52ftw-unattended-upgrades +echo "deb [arch=arm64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list +``` + ## Build the image Image provisioning lives under [`deploy/pi-gen`](../deploy/pi-gen): diff --git a/docs/self-update.md b/docs/self-update.md index 2a10ff670..21871f474 100644 --- a/docs/self-update.md +++ b/docs/self-update.md @@ -90,6 +90,15 @@ persistent component history after Core recreation. The updater accepts only known components and `vX.Y.Z` or `vX.Y.Z-beta.N` targets. +## Scope: the host is not updated here + +Self-update covers Core, the updater sidecar, the Optimizer and signed +drivers — never the host operating system, kernel or Docker engine. The +Raspberry Pi appliance image keeps its own host patched with +`unattended-upgrades` ([rpi-image.md](rpi-image.md#host-os-security-updates)); +on every other deployment the host and engine belong to the operator's own +package and service management. + ## Operator use The version badge selects `stable` or `beta`, checks availability and starts