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
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:
@@ -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
|
||||
|
||||
@@ -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."
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user