hub v0.133.0 (R-859 test approvals end with the test; R-840 bundle on the System page, manifest, alarm); installer 1.31.0 (root files from the config bundle); bundle bootstrap script; golden 0.293.0 evidence; rulings 96–99; drill-r50 removed (evidence)
gates / gates (push) Successful in 29s

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-04 20:18:37 +02:00
parent 6b820143f8
commit ff1db11db4
45 changed files with 1707 additions and 42 deletions
+17
View File
@@ -1,3 +1,20 @@
## felhom-host-install.sh 1.31.0 — the root-owned files come from the agent's config bundle (R-840) (2026-10-04)
Needs a vouched agent ≥ 0.143.0 for the bundle (hub ≥ 0.133.0 serves its sha); an older vouched agent: the per-file
path of 1.30.0, unchanged, with a warning.
- **One source of truth.** Step 5 fetches `felhom-config-bundle.json` of the vouched agent from the package registry,
verifies its sha256 against the hub manifest (`bundle`), takes out the bundle's own `felhom-os-apply` (checked against
its entry, parsed), installs it and runs `felhom-os-apply --install-bundle` — the SAME code that installs a signed
`agent_config_update` on an installed box: every root-owned file (sudoers, 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), every check
before the first write, a self-check after, the previous copies kept, everything put back on a failure. The OOB
directory and user are created first so the bundle writes the belt's files; the host key, the belt loader and the
unit enables stay here.
- Uninstall also removes `/etc/felhom/config-bundle.json` and `/var/lib/felhom-os-apply`.
- New: `scripts/felhom-bundle-bootstrap.sh` — the ONE by-hand step an installed box from before agent 0.143.0 needs
(installs only the bundle-aware `felhom-os-apply`, checked against the vouched bundle); `11` §5.4.2.
## felhom-host-install.sh 1.30.0 — the crash guard and the slow-lane trust files (2026-10-04)
Needs agent ≥ 0.142.0 at the pinned tag for the crash guard (an older agent: skipped with a warning, the box keeps
+54
View File
@@ -0,0 +1,54 @@
#!/bin/bash
# felhom-bundle-bootstrap.sh — the ONE by-hand act that lets an installed box take config bundles (R-840, `11` §5.4.2).
#
# Why it exists: a signed agent_config_update is installed by the box's ROOT-OWNED felhom-os-apply. A box installed
# before agent v0.143.0 has an older felhom-os-apply that has no bundle mode, and no signed job can write a root file
# on such a box (that gap IS R-840). So the first bundle needs this one step, as root, once per box. After it, every
# later change to the box's root files arrives by the signed route.
#
# What it does: downloads the config bundle of AGENT_VERSION, checks its sha256 against the one you pass (the vouched
# one — the hub's Configuration page or the release output), takes out felhom-os-apply, checks it against the bundle's
# own entry and that it parses, installs it (0755 root:root, the old copy kept beside it), and runs its self-check.
# It changes NOTHING else: no sudoers, no unit, no restart, no app, no Docker.
#
# Usage (as root on the Proxmox host): bash felhom-bundle-bootstrap.sh <agent-version> <bundle-sha256>
set -euo pipefail
VER="${1:-}"; SHA="${2:-}"
[[ "$VER" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo "usage: $0 <agent-version> <bundle-sha256>" >&2; exit 2; }
[[ "$SHA" =~ ^[0-9a-f]{64}$ ]] || { echo "the bundle sha256 must be 64 lowercase hex characters" >&2; exit 2; }
[[ $EUID -eq 0 ]] || { echo "run as root" >&2; exit 2; }
URL="https://gitea.dooplex.hu/api/packages/admin/generic/felhom-agent/$VER/felhom-config-bundle.json"
DST=/usr/local/sbin/felhom-os-apply
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
echo "1/4 download $URL"
curl -fsS -o "$T/bundle.json" "$URL"
got=$(sha256sum "$T/bundle.json" | awk '{print $1}')
[[ "$got" == "$SHA" ]] || { echo "STOP: the bundle's sha256 is $got, not $SHA — nothing changed" >&2; exit 1; }
echo " sha256 OK ($SHA)"
echo "2/4 take felhom-os-apply out of the bundle and check it"
python3 - "$T/bundle.json" "$T/os-apply" "$VER" <<'PY'
import ast, base64, hashlib, json, sys
b = json.load(open(sys.argv[1]))
assert b.get("agent_version") == sys.argv[3], f"the bundle is for {b.get('agent_version')}, not {sys.argv[3]}"
e = [f for f in b["files"] if f["path"] == "/usr/local/sbin/felhom-os-apply"][0]
data = base64.b64decode(e["content_b64"])
assert hashlib.sha256(data).hexdigest() == e["sha256"], "felhom-os-apply does not match its own sha in the bundle"
text = data.decode()
ast.parse(text)
assert 'BUNDLE_OP = "agent_config_update"' in text, "this felhom-os-apply has no bundle mode"
open(sys.argv[2], "wb").write(data)
print(" felhom-os-apply sha256", e["sha256"])
PY
echo "3/4 install it (the previous copy is kept as $DST.pre-bundle)"
[[ -f "$DST" ]] && cp -p "$DST" "$DST.pre-bundle"
install -m 0755 -o root -g root "$T/os-apply" "$DST.new.$$"
mv "$DST.new.$$" "$DST"
echo "4/4 self-check"
out=$(python3 "$DST" --self-check)
echo " $out"
[[ "$out" == *"bundle-format=1"* ]] || { echo "STOP: the self-check failed — put the old copy back: mv $DST.pre-bundle $DST" >&2; exit 1; }
echo "DONE. This box can now take signed config bundles. Nothing else was changed."
+78 -8
View File
@@ -184,7 +184,7 @@
set -euo pipefail
SCRIPT_VERSION="1.30.0" # the SINGLE version source (F-1): -h and the run banners follow it.
SCRIPT_VERSION="1.31.0" # the SINGLE version source (F-1): -h and the run banners follow it.
# The hub used to carry a copy for its Setup tab; R-94 DELETED it
# (2026-08-02) because the hub cannot know which version a box runs —
# the Setup command fetches this script at run time. scripts/
@@ -444,6 +444,9 @@ resolve_artifacts() {
ART_AGENT_SHA=$(python3 -c "import json,sys;print(json.loads(sys.argv[1])['agent']['sha256'])" "$body" 2>/dev/null || echo "")
ART_GOLDEN_VER=$(python3 -c "import json,sys;print(json.loads(sys.argv[1])['golden']['version'])" "$body" 2>/dev/null || echo "")
ART_GOLDEN_SHA=$(python3 -c "import json,sys;print(json.loads(sys.argv[1])['golden']['sha256'])" "$body" 2>/dev/null || echo "")
# 1.31.0 (R-840): the vouched agent's CONFIG BUNDLE — every root-owned file below, as one checked unit. Absent when
# the vouched agent carries none (older than 0.143.0); step 5 then fetches the files one by one as before.
ART_BUNDLE_SHA=$(python3 -c "import json,sys;print((json.loads(sys.argv[1]).get('bundle') or {}).get('sha256',''))" "$body" 2>/dev/null || echo "")
}
# Resolve the Gitea fetch credential (git username + token) from the customer's controller.yaml —
@@ -1166,10 +1169,13 @@ run_uninstall() {
local cgf
for cgf in /usr/local/sbin/felhom-crash-guard /etc/systemd/system/felhom-crash-guard.service \
/etc/systemd/system/felhom-crash-guard-check.service /etc/systemd/system/felhom-crash-guard-check.timer \
/etc/felhom/crash-guard.conf /etc/felhom/os-trust.json /etc/felhom/operator-signers; do
/etc/felhom/crash-guard.conf /etc/felhom/os-trust.json /etc/felhom/operator-signers \
/etc/felhom/config-bundle.json; do
if [[ -e "$cgf" ]]; then run rm -f "$cgf"; fi
done
if [[ -d /var/lib/felhom-crash-guard ]]; then run rm -rf /var/lib/felhom-crash-guard; fi
# 1.31.0 (R-840): the bundle's previous copies and the wrapper's nonce record.
if [[ -d /var/lib/felhom-os-apply ]]; then run rm -rf /var/lib/felhom-os-apply; fi
if [[ -f /var/lib/vz/snippets/felhom-guest-hook.sh ]]; then run rm -f /var/lib/vz/snippets/felhom-guest-hook.sh; fi
local dconf _dnsmasq_touched=false
for dconf in /etc/dnsmasq.d/felhom-*.conf; do
@@ -2328,6 +2334,76 @@ step_agent_install() {
fi
fi
# 1.31.0 (R-840): the root-owned files come from the vouched agent's CONFIG BUNDLE — the SAME file a signed
# agent_config_update brings to an installed box, installed by the same code (the bundle's own felhom-os-apply), so
# a new box and an updated box cannot drift. A vouched agent older than 0.143.0 carries none: the per-file path.
if [[ -n "$ART_BUNDLE_SHA" ]]; then
install_root_files_bundle
else
log_warn " the vouched agent v$ART_AGENT_VER carries no config bundle (older than 0.143.0) — fetching the root files one by one"
install_root_files_legacy
install_mgmt_watchdog
# H1: dedicated felhom-sshd OOB instance + static belt (appliance default since v1.25.0; --no-oob opts out).
install_oob
fi
_state_mark agent_install
}
# install_root_files_bundle (1.31.0, R-840): fetch the vouched config bundle, verify it against the hub manifest, take
# out its own felhom-os-apply (checked against the bundle's entry), install that, and let it install every root-owned
# file with its checks (visudo, sh/bash -n, python, unit sections, the RuntimeDirectory guard, nft -c), its self-check
# and its undo. Then the parts that are not files: the OOB host key, the belt loader, enabling the units.
install_root_files_bundle() {
local url="$GITEA_BASE/api/packages/$GITEA_OWNER/generic/felhom-agent/$ART_AGENT_VER/felhom-config-bundle.json"
if $DRY_RUN; then
log_dry "fetch $url ; verify sha256=$ART_BUNDLE_SHA ; felhom-os-apply --install-bundle (every root-owned file, one checked unit)"
return 0
fi
# The bundle writes the OOB belt's files only on a box with the belt: create its directory and user FIRST.
if $ENABLE_OOB; then
install -d -o root -g root -m 0755 /etc/felhom-sshd /etc/felhom-sshd/authorized_keys
id felhom-op >/dev/null 2>&1 || useradd --create-home --shell /bin/bash felhom-op
fi
local btmp ostmp out rc=0
btmp=$(mktemp -t felhom-bundle.XXXXXX); ostmp=$(mktemp -t felhom-os.XXXXXX)
fetch_verify "$url" "$btmp" "$ART_BUNDLE_SHA"
python3 - "$btmp" "$ostmp" <<'PY' || { rm -f "$btmp" "$ostmp"; die "the config bundle carries no valid felhom-os-apply — refusing"; }
import ast, base64, hashlib, json, sys
b = json.load(open(sys.argv[1]))
e = [f for f in b["files"] if f["path"] == "/usr/local/sbin/felhom-os-apply"][0]
data = base64.b64decode(e["content_b64"])
assert hashlib.sha256(data).hexdigest() == e["sha256"], "felhom-os-apply does not match its sha in the bundle"
ast.parse(data.decode())
open(sys.argv[2], "wb").write(data)
PY
install -m 0755 -o root -g root "$ostmp" /usr/local/sbin/felhom-os-apply
rm -f "$ostmp"
# The wrapper refuses --install-bundle from a sudo caller (the agent's route is the signed job); this script is
# root already, so a `sudo bash` run must not look like one.
out=$(env -u SUDO_UID -u SUDO_GID -u SUDO_USER -u SUDO_COMMAND \
/usr/local/sbin/felhom-os-apply --install-bundle "$btmp" --sha256 "$ART_BUNDLE_SHA" 2>&1) || rc=$?
rm -f "$btmp"
grep '^os-apply: BUNDLE' <<<"$out" | sed 's/^/ /' || true
[[ $rc -eq 0 ]] || die "the config bundle did not install (rc=$rc) — nothing half-done stays (the wrapper put the previous files back): $(grep -E 'REFUSED|FAILED' <<<"$out" | head -2)"
systemctl daemon-reload
systemctl enable felhom-agent >/dev/null 2>&1 || true
log_success " installed every root-owned file from config bundle v$ART_AGENT_VER (sha ${ART_BUNDLE_SHA:0:16}…; checked, previous copies kept)"
if $ENABLE_OOB; then
if [[ ! -f /etc/felhom-sshd/ssh_host_ed25519_key ]]; then
ssh-keygen -t ed25519 -N "" -f /etc/felhom-sshd/ssh_host_ed25519_key -C felhom-sshd-hostkey -q
chmod 600 /etc/felhom-sshd/ssh_host_ed25519_key
fi
systemctl enable --now felhom-oob-nft.service >/dev/null 2>&1 || true # load the static belt now
systemctl enable felhom-sshd >/dev/null 2>&1 || true # NOT start — the agent renders the config first
log_success " OOB felhom-sshd instance + static belt from the bundle (agent renders config + fills sets once oob.enabled)"
else
log_skip " OOB (felhom-sshd) off (byo, or appliance --no-oob) — the bundle skipped its files"
fi
}
# install_root_files_legacy is the per-file path (before 1.31.0, and for a vouched agent older than 0.143.0).
install_root_files_legacy() {
# Guarded-mkfs wrapper (Impl-1 Part B) — the ONLY mkfs path the sudoers permits. Install it BEFORE
# the sudoers (which allowlists it), 0755 root:root under /usr/local/sbin. bash -n before install.
if $DRY_RUN; then
@@ -2503,12 +2579,6 @@ step_agent_install() {
# Non-fatal if the agent repo predates them (raw fetch 404s → break-glass just stays manual).
# HARD GUARD: refuse ANY fetched unit that declares RuntimeDirectory= — that directive is the very
# incident G1 closes (a second sshd's `RuntimeDirectory=sshd` removed the shared /run/sshd).
install_mgmt_watchdog
# H1: dedicated felhom-sshd OOB instance + static belt (appliance default since v1.25.0; --no-oob opts out).
install_oob
_state_mark agent_install
}
# install_mgmt_watchdog fetches + installs the G1 break-glass host artifacts (idempotent; enables the