From 1100a344d577a2b2ff3cfc1bc8475875afeb8333 Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Thu, 22 Oct 2026 17:11:07 +0200 Subject: [PATCH] =?UTF-8?q?night=202026-10-22:=20D4=20as=20two=20ready=20p?= =?UTF-8?q?atches=20(agent=20bundle=20+=20installer=20uninstall)=20?= =?UTF-8?q?=E2=80=94=20a=20push=20of=20the=20installer=20would=20publish?= =?UTF-8?q?=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../P2-D4/felhom-agent-bundle-D4.patch | 137 ++++++++++++++++++ .../P2-D4/felhom.eu-installer-D4.patch | 29 ++++ 2 files changed, 166 insertions(+) create mode 100644 documentation/audits/night-2026-10-22/P2-D4/felhom-agent-bundle-D4.patch create mode 100644 documentation/audits/night-2026-10-22/P2-D4/felhom.eu-installer-D4.patch diff --git a/documentation/audits/night-2026-10-22/P2-D4/felhom-agent-bundle-D4.patch b/documentation/audits/night-2026-10-22/P2-D4/felhom-agent-bundle-D4.patch new file mode 100644 index 00000000..204fc593 --- /dev/null +++ b/documentation/audits/night-2026-10-22/P2-D4/felhom-agent-bundle-D4.patch @@ -0,0 +1,137 @@ +commit a0c88e5183838ebad04d5aad4acb3e05e1c1c153 +Author: kisfenyo +Date: Thu Oct 22 16:58:45 2026 +0200 + + R-982 (D4): root SSH from the LAN by key only — a bundle drop-in, checked and reloaded by felhom-os-apply; R-1204: the heartbeat reports a future-stamped tunnel handshake; unreleased + +diff --git a/configs/felhom-os-apply b/configs/felhom-os-apply +index 81565fb..3b6fc9a 100755 +--- a/configs/felhom-os-apply ++++ b/configs/felhom-os-apply +@@ -165,7 +165,8 @@ KERNEL_ACTIVE = ("staged", "oneshot", "judging", "reverting") + # dest: (source file under configs/, mode, check, policy) + # check: the content check BEFORE anything is written (R18): sudoers → visudo -cf; sh / bash → -n; python → compile; + # unit → a [Unit]/[Service]/[Timer] section; unit-nort → that, and never RuntimeDirectory= (the G1 incident); +-# agent-unit → User=felhom-agent; dropin → a [Unit] section (SF-3); nft → nft -c -f; plain → none. ++# agent-unit → User=felhom-agent; dropin → a [Unit] section (SF-3); nft → nft -c -f; plain → none; ++# sshd → `sshd -t -f` on the snippet alone (R-982). + # policy: replace → always written when it differs; if-absent → only when the box has none (a setting the operator may + # have tuned); oob → only on a box with the OOB belt (/etc/felhom-sshd exists — the installer's --no-oob leaves none). + # Order matters: the sudoers files come LAST, so a referenced wrapper is in place before the line that allows it. +@@ -221,6 +222,9 @@ BUNDLE_FILES = [ + ("/etc/systemd/system/felhom-sshd.service", "felhom-sshd.service", 0o644, "unit-nort", "oob"), + ("/etc/felhom-oob.nft", "felhom-oob.nft", 0o644, "nft", "oob"), + ("/etc/systemd/system/felhom-oob-nft.service", "felhom-oob-nft.service", 0o644, "unit", "oob"), ++ # R-982, operator ruling D4 (2026-10-22): the host's own sshd (port 22, the LAN): root by key only. The console ++ # password stays; felhom-sshd (the operator's door) reads nothing from sshd_config.d. Reloaded in after_install. ++ ("/etc/ssh/sshd_config.d/10-felhom-root-key-only.conf", "felhom-root-ssh-key-only.conf", 0o644, "sshd", "replace"), + ("/etc/sudoers.d/felhom-op", "felhom-op.sudoers", 0o440, "sudoers", "oob"), + ("/etc/sudoers.d/felhom-agent", "felhom-agent.sudoers", 0o440, "sudoers", "replace"), + ] +@@ -434,7 +438,8 @@ class Runner: + def check_content(self, kind, data): + """Run an external syntax check on a root-only temp copy. Returns (rc, message).""" + import tempfile +- cmd = {"sudoers": ["visudo", "-cf"], "sh": ["sh", "-n"], "bash": ["bash", "-n"], "nft": ["nft", "-c", "-f"]}[kind] ++ cmd = {"sudoers": ["visudo", "-cf"], "sh": ["sh", "-n"], "bash": ["bash", "-n"], "nft": ["nft", "-c", "-f"], ++ "sshd": ["/usr/sbin/sshd", "-t", "-f"]}[kind] + with tempfile.TemporaryDirectory(prefix="felhom-bundle-") as d: + p = os.path.join(d, "f") + with open(p, "wb") as f: +@@ -1932,10 +1937,16 @@ class Bundle: + text = content.decode("utf-8") + except UnicodeDecodeError: + raise Refused("R18", f"{dest} is not UTF-8 text") +- if check in ("sudoers", "sh", "bash", "nft"): ++ if check in ("sudoers", "sh", "bash", "nft", "sshd"): + rc, msg = self.r.check_content(check, content) + if rc != 0: + raise Refused("R18", f"{dest} fails its {check} check: {msg}") ++ if check == "sshd": ++ # R-982: the drop-in may only ever make root KEY-ONLY. `no` would also shut the root key out (Proxmox's own ++ # node-to-node SSH and the LAN key route), `yes` would undo the ruling — both refused. ++ vals = re.findall(r"^\s*PermitRootLogin\s+(\S+)\s*$", text, re.M | re.I) ++ if vals != ["prohibit-password"]: ++ raise Refused("R18", f"{dest} must set exactly `PermitRootLogin prohibit-password` (found {vals})") + elif check == "python": + try: + compile(text, dest, "exec") +@@ -2073,6 +2084,12 @@ class Bundle: + "enable the crash guard") + if "/etc/systemd/system/felhom-mgmt-watchdog.timer" in dests: + self.must(["systemctl", "enable", "--now", "felhom-mgmt-watchdog.timer"], "enable the mgmt watchdog timer") ++ # R-982: the host's sshd takes the root-key-only drop-in. The WHOLE config is tested first (a failure is a ++ # refusal, and the bundle undo puts the old files back); `reload` keeps every open session, including the ++ # operator's. felhom-sshd is not touched. ++ if "/etc/ssh/sshd_config.d/10-felhom-root-key-only.conf" in written: ++ self.must(["/usr/sbin/sshd", "-t"], "sshd -t (the host's whole sshd config)") ++ self.must(["systemctl", "reload", "ssh"], "systemctl reload ssh") + + def must(self, argv, what): + rc, out, err = self.r.host(argv, 120) +diff --git a/configs/felhom-root-ssh-key-only.conf b/configs/felhom-root-ssh-key-only.conf +new file mode 100644 +index 0000000..f2ff547 +--- /dev/null ++++ b/configs/felhom-root-ssh-key-only.conf +@@ -0,0 +1,7 @@ ++# MANAGED BY FELHOM (the agent's config bundle) — R-982, operator ruling D4 (2026-10-22). ++# The Proxmox host's OWN sshd — the listener the household network reaches on port 22: root logs in with a KEY only. ++# The root password still works on the console (the break-glass password, `03` §8a). The operator's door ++# (felhom-sshd, port 8822, /etc/felhom-sshd/sshd_config) is a separate daemon whose config includes nothing from ++# here, so it is not changed. Debian's sshd_config reads this folder BEFORE its own `PermitRootLogin yes`, and for ++# sshd the first value wins. ++PermitRootLogin prohibit-password +diff --git a/configs/test_felhom_config_bundle.py b/configs/test_felhom_config_bundle.py +index 1c5a6ab..74f983d 100644 +--- a/configs/test_felhom_config_bundle.py ++++ b/configs/test_felhom_config_bundle.py +@@ -429,6 +429,48 @@ class SelfCheckUndo(unittest.TestCase): + self.assert_restored(box, before, rep) + + ++ ++class RootKeyOnly(unittest.TestCase): ++ """R-982, operator ruling D4 (2026-10-22): the bundle gives the host's own sshd a root-key-only drop-in, tests the ++ whole config and reloads sshd — only when the file was written; a drop-in that says anything but ++ `prohibit-password` is refused before anything is written. ++ RED-PROOF: with the after_install block removed, test_fresh_box_takes_the_dropin_and_reloads fails (no reload); ++ with the content rule removed, test_a_dropin_that_says_yes_or_no_is_refused fails — seen failing 2026-10-22.""" ++ DEST = "/etc/ssh/sshd_config.d/10-felhom-root-key-only.conf" ++ ++ def test_fresh_box_takes_the_dropin_and_reloads(self): ++ data, sha = real_bundle() ++ box = Box(data, signed_job(sha)) ++ rc, rep = run(box) ++ self.assertEqual(rc, 0, rep) ++ self.assertIn(b"PermitRootLogin prohibit-password", box.files[self.DEST]) ++ self.assertEqual(box.modes[self.DEST], 0o644) ++ self.assertIn(["/usr/sbin/sshd", "-t"], box.calls) ++ self.assertIn(["systemctl", "reload", "ssh"], box.calls) ++ self.assertLess(box.calls.index(["/usr/sbin/sshd", "-t"]), box.calls.index(["systemctl", "reload", "ssh"])) ++ self.assertFalse(any("felhom-sshd" in " ".join(c) for c in box.calls), "the operator's door must not be touched") ++ ++ def test_an_unchanged_dropin_is_not_reloaded(self): ++ data, sha = real_bundle() ++ box = Box(data, signed_job(sha)) ++ box.put(self.DEST, (REPO / "configs" / "felhom-root-ssh-key-only.conf").read_bytes(), 0o644) ++ rc, rep = run(box) ++ self.assertEqual(rc, 0, rep) ++ self.assertNotIn(["systemctl", "reload", "ssh"], box.calls) ++ ++ def test_a_dropin_that_says_yes_or_no_is_refused(self): ++ for bad in (b"PermitRootLogin yes\n", b"PermitRootLogin no\n", b"# nothing\n", ++ b"PermitRootLogin prohibit-password\nPermitRootLogin yes\n"): ++ def edit(files, bad=bad): ++ for e in files: ++ if e["path"] == self.DEST: ++ e["content_b64"] = base64.b64encode(bad).decode() ++ data, sha = edited_bundle(edit) ++ box = Box(data, signed_job(sha)) ++ rc, rep = run(box) ++ self.assertNotEqual(rc, 0, bad) ++ self.assertNotIn(self.DEST, box.files, bad) ++ + class TrustBootstrap(unittest.TestCase): + def test_missing_signers_verifies_against_the_pinned_key_and_creates_it(self): + data, sha = real_bundle() diff --git a/documentation/audits/night-2026-10-22/P2-D4/felhom.eu-installer-D4.patch b/documentation/audits/night-2026-10-22/P2-D4/felhom.eu-installer-D4.patch new file mode 100644 index 00000000..1ab88704 --- /dev/null +++ b/documentation/audits/night-2026-10-22/P2-D4/felhom.eu-installer-D4.patch @@ -0,0 +1,29 @@ +diff --git a/scripts/felhom-host-install.sh b/scripts/felhom-host-install.sh +index 970109fd..349da0c5 100644 +--- a/scripts/felhom-host-install.sh ++++ b/scripts/felhom-host-install.sh +@@ -1417,6 +1417,12 @@ run_uninstall() { + if [[ -f /etc/default/grub.d/zz-felhom-kernel-default.cfg ]]; then + log_info " kept /etc/default/grub.d/zz-felhom-kernel-default.cfg (the kernel this box booted healthily stays the default)" + fi ++ # R-982 (operator ruling D4, 2026-10-22; agent bundle after v0.162.0): the host sshd's root-key-only drop-in. The ++ # uninstall gives the host back as Proxmox set it up, so the drop-in goes and sshd is reloaded (open sessions stay). ++ if [[ -f /etc/ssh/sshd_config.d/10-felhom-root-key-only.conf ]]; then ++ run rm -f /etc/ssh/sshd_config.d/10-felhom-root-key-only.conf ++ run systemctl reload ssh 2>/dev/null || true ++ 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 +diff --git a/scripts/test_hostinstall.py b/scripts/test_hostinstall.py +index 3f7088a6..e9cf9ad9 100644 +--- a/scripts/test_hostinstall.py ++++ b/scripts/test_hostinstall.py +@@ -60,6 +60,7 @@ BUNDLE_DESTS = [ + "/etc/systemd/system/felhom-sshd.service", + "/etc/felhom-oob.nft", + "/etc/systemd/system/felhom-oob-nft.service", ++ "/etc/ssh/sshd_config.d/10-felhom-root-key-only.conf", # R-982, operator ruling D4 (2026-10-22) + "/etc/sudoers.d/felhom-op", + "/etc/sudoers.d/felhom-agent", + ]