diff --git a/CHANGELOG.md b/CHANGELOG.md index bec4b02..17e4f8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +## v0.143.0 — the config bundle: a signed route for a box's root-owned files (R-840, decision 96) (2026-10-04) + +Released by `scripts/release-agent.sh`: binary sha256 `41c0d3060013dfda795262147454248149bee0888c0935170fdb85de6e7a35da`, +config bundle `felhom-config-bundle.json` sha256 `8d7273cf5313ef62b867cb6f831c631923a436452d6f90b8ff7f0771170396ba`. + +- **The bundle.** Every root-owned file the installer's step 5 writes (sudoers ×2, the five wrappers, the crash guard + and its units, the agent and rollback units, the start-limit drop-in, the mgmt watchdog, the OOB belt's files) as ONE + reproducible JSON file, built by `scripts/build-config-bundle.py` from `BUNDLE_FILES` in `configs/felhom-os-apply` + (one table) and published beside the binary. The installer (1.31.0) installs the same file. +- **The route.** A signed `agent_config_update` {agent_version, bundle_sha256} (`felhom-opsign -op agent_config_update + -bundle-sha256 …`). The agent is the courier (downloads, checks the sha, hands over); `felhom-os-apply` mode `bundle` + verifies it ITSELF: the operator signature against the root-owned `/etc/felhom/operator-signers` (or, when that file + is missing, ONLY the installer's pinned key, after which it creates the file with exactly that key), the host binding, + the window, its own nonce; the bundle sha; every path in `BUNDLE_FILES` (R16) and never a trust file (R17); every + content check before the first write (visudo, sh/bash -n, python, unit sections, the RuntimeDirectory guard, User=, + nft -c, and that the route itself survives). Atomic per file, previous copies kept under + `/var/lib/felhom-os-apply/bundle-prev/`; a self-check after (visudo -c, `sudo -l` lists the route, the new wrapper's + `--self-check`, the self-update wrapper's usage, the crash guard's status = kernel.panic); any failure puts every + previous copy back. A newly installed crash guard is started (`enable --now`: kernel.panic for this boot, no reboot). + Record `/etc/felhom/config-bundle.json`; the agent reports it as `system.config_bundle`, the facts mode adds drift. +- **Bootstrap.** A box whose `felhom-os-apply` predates 0.143.0 cannot take the first bundle by the route (nothing on it + can write a root file from a signed job): `felhom.eu/scripts/felhom-bundle-bootstrap.sh` is the one by-hand step. +- **`build-golden.sh` 3.2.0** (not part of the binary): `GOLDEN_GUEST_PKGS` brings the template to exactly the + approved guest release (only installed packages, never newer, never a removal or a new package) and prints the + first-night count. +- Tests: `configs/test_felhom_config_bundle.py` (43; 22 of 22 mutants red), `internal/osupdate/bundle_test.go`, + `internal/hub/bundle_record_test.go`. Live: `felhom.eu/documentation/audits/r840-config-bundle-2026-10-04/partB/`. + ## v0.142.1 — a Docker step no longer leaves the controller and traefik blind (R-858, `09` decision 95) > **RELEASED 2026-10-04** by `scripts/release-agent.sh` — tag `v0.142.1` (`4950030`), sha256 diff --git a/configs/build-golden.sh b/configs/build-golden.sh index 1128186..6afd5c0 100644 --- a/configs/build-golden.sh +++ b/configs/build-golden.sh @@ -61,7 +61,7 @@ set -euo pipefail # Script provenance — logged into every bake transcript next to the baked controller tag, so an # archive can always be traced to the script that produced it. Bump on any behavior change. -GOLDEN_SCRIPT_VERSION="3.1.0" +GOLDEN_SCRIPT_VERSION="3.2.0" VMID="${1:-9100}" TEMPLATE="${2:-local:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst}" @@ -137,6 +137,48 @@ pct exec "$VMID" -- env GOLDEN_DOCKER_PKGS="$GOLDEN_DOCKER_PKGS" bash -c ' fi dpkg-query -W containerd.io docker-buildx-plugin docker-ce docker-ce-cli docker-ce-rootless-extras docker-compose-plugin 2>/dev/null | sed "s/^/ installed: /" ' +# v3.2.0 (Part F of the R-840 brief, `11` §5.3): GOLDEN_GUEST_PKGS = the newest APPROVED guest release, as +# "name=version …" (the hub's os_releases row for layer guest, IN FORCE — never a cancelled test approval). The bake +# brings every package the template HAS to exactly that version, under felhom-os-apply's rules: never a package the +# template lacks (--only-upgrade), never newer than approved, never a removal or a new package (a simulation is checked +# first and the bake FAILS on either). Empty = no approved guest release in force: the template's versions stay, and +# the box's first night installs whatever release is approved then. Either way the bake PRINTS the first-night count: +# how many installed packages are older than the approved version (target 0). +GOLDEN_GUEST_PKGS="${GOLDEN_GUEST_PKGS:-}" +pct exec "$VMID" -- env GOLDEN_GUEST_PKGS="$GOLDEN_GUEST_PKGS" bash -c ' + set -e + export DEBIAN_FRONTEND=noninteractive + if [ -z "$GOLDEN_GUEST_PKGS" ]; then + echo "[golden] no approved guest release given - the template versions stay; first-night count vs an approved release: n/a" + echo "[golden] pending Debian upgrades in the baked guest (what a FUTURE approval may bring): $(apt list --upgradable 2>/dev/null | grep -c /)" + exit 0 + fi + want="" + for nv in $GOLDEN_GUEST_PKGS; do + n=${nv%%=*}; v=${nv#*=} + cur=$(dpkg-query -W -f="\${Version}" "$n" 2>/dev/null) || continue # not in the template: never added + dpkg --compare-versions "$cur" lt "$v" && want="$want $n=$v" + done + if [ -n "$want" ]; then + sim=$(apt-get -s install --only-upgrade -o Dpkg::Options::=--force-confold $want) + if echo "$sim" | grep -q "^Remv "; then echo "[golden] FATAL: the approved guest set would REMOVE a package"; echo "$sim" | grep "^Remv "; exit 1; fi + for p in $(echo "$sim" | awk "/^Inst /{print \$2}"); do + dpkg-query -W "$p" >/dev/null 2>&1 || { echo "[golden] FATAL: the approved guest set would ADD $p - not in the template"; exit 1; } + done + apt-get install -y -qq --only-upgrade -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef $want >/dev/null + echo "[golden] approved guest release installed: $(echo $want | wc -w) package(s) brought to the approved version" + else + echo "[golden] approved guest release: the template already runs every approved version" + fi + left=0 + for nv in $GOLDEN_GUEST_PKGS; do + n=${nv%%=*}; v=${nv#*=} + cur=$(dpkg-query -W -f="\${Version}" "$n" 2>/dev/null) || continue + dpkg --compare-versions "$cur" lt "$v" && { left=$((left+1)); echo " still older: $n $cur < $v"; } + done + echo "[golden] first-night count vs the approved guest release: $left (target 0)" + [ "$left" -eq 0 ] || { echo "[golden] FATAL: $left package(s) stayed older than the approved release"; exit 1; } +' echo "[golden] baking daemon.json: classic overlay2 driver (containerd-snapshotter OFF) + log rotation …" # containerd-snapshotter (Docker 28+/29 default) keeps the IMAGE content store under # /var/lib/containerd — which is NOT /var/lib/docker, so it would stay on the OS rootfs and the split