Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/pi-host-security-updates.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions deploy/pi-gen/stage-ftw/00-install-packages/00-packages
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ curl
jq
network-manager
raspberrypi-sys-mods
unattended-upgrades
18 changes: 15 additions & 3 deletions deploy/pi-gen/stage-ftw/01-ftw-setup/00-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -86,6 +89,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_<user>-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
Expand Down
6 changes: 6 additions & 0 deletions deploy/pi-gen/stage-ftw/01-ftw-setup/files/20auto-upgrades
Original file line number Diff line number Diff line change
@@ -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";
Original file line number Diff line number Diff line change
@@ -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";
9 changes: 9 additions & 0 deletions deploy/pi-gen/stage-ftw/01-ftw-setup/files/firstboot.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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

# Bring the stack up on whatever images are already present FIRST, so a box
# that already has them (a reboot mid-provision, a re-run, or a future
# pre-baked image) is never held hostage by GHCR: GHCR is GitHub-hosted, so a
Expand Down
5 changes: 4 additions & 1 deletion docs/operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
23 changes: 23 additions & 0 deletions docs/rpi-image.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,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):
Expand Down
9 changes: 9 additions & 0 deletions docs/self-update.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,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
Expand Down