From d63ea3591e7337f6005e9d6308a0ca15987d434b Mon Sep 17 00:00:00 2001 From: kisfenyo Date: Tue, 6 Oct 2026 19:04:57 +0200 Subject: [PATCH] box_walk: a target table (9202, 9201, tester-1 via a jump through the HP box); the Tester 1 guest is a test box for the admin seed (R-892, decision 158) Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS --- CHANGELOG.md | 9 ++++++ scripts/box_walk.py | 41 ++++++++++++++++++++++---- scripts/test_upgrade_bench.py | 51 +++++++++++++++++++++++++++++++++ scripts/upgrade_fixtures_box.py | 12 ++++---- 4 files changed, 101 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8cc144a..8de7599 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 2026-10-06 (night) — the box walk's target table; the Tester 1 box is a test box (R-892) + +**What runs on a box changed:** nothing. Test tools only. + +- `box_walk.py` `TARGETS`: one row per box (name for the guards, ssh host + optional jump, guest, dashboard base and domain): `9202` (default), `9201`, and `tester-1` — VM 341 on the HP box (node `felhom`, 192.168.0.154, reached with `-J demo-hp`), guest 9201 at 192.168.0.101 for `felhom.enkicsifelhom.hu`. `TARGET=` selects; the old `GUEST=` still works. Nothing edits DooPlex's `~/.ssh/config`. +- `BOX_ADMIN_SEED_GUESTS` adds `("tester-1", "9201")` (`09` §3 decisions 149, 158); demo-hp's 9201 stays refused. +- Tests: BoxWalkTargets (5, red-proved: without the jump the Tester 1 test fails), one more BoxAdminSeedGuard case. +- **Not yet usable on the Tester 1 box:** DooPlex's key is not authorized there (`Permission denied (publickey,password)`), and fetching its vaulted password was refused by the session's permission check. + ## 2026-10-06 (evening) — wishlist's own sign-up switch closes after the setup and reopens for the family window (R-717) **What runs on a box changed:** wishlist's `.felhom.yml` gains `after_setup` (`command` closes, `open_command` opens — Node's own sqlite module writes `system_config.enableSignup` in group `global`, the value read back before the marker prints) and `min_controller: "0.301.0"` (the controller that knows `open_command`). Pushed AFTER controller v0.301.0 reached demo-hp, demo-felhom and the Tester 1 box. A catalog change does not touch an installed wishlist (the 2026-09-29 Part 0 rule); no box reports wishlist today. Proven live on 9202 with the 0.301.0 test image: after the setup the switch read closed and a stranger straight at the app got 401 „invite only"; in the 15-minute window it read open and a family member signed up (users 1 → 2); at the window's end the close ran again 10 s later and a stranger got 401, users stayed 2 (`felhom.eu/documentation/audits/design-build-2026-10-06/E/live.txt`). Opengist cannot use it: its container has no sqlite tool and no script runtime, and its CLI has no settings command (R-717 stays open for opengist). diff --git a/scripts/box_walk.py b/scripts/box_walk.py index 0acdb16..3e6a272 100644 --- a/scripts/box_walk.py +++ b/scripts/box_walk.py @@ -28,12 +28,41 @@ from datetime import datetime, timezone SC = os.environ.get('SC', os.path.expanduser('~/.felhom-retest')) EV = os.environ.get('EV', os.path.join(SC, 'evidence')) DRILL = "/mnt/5_hdd/felhom.eu/drill/app-catalog-drill" -# GUEST=9201 selects demo-hp's hub-enabled guest (the mail proof); default 9202, the scratch guest. -GUEST = os.environ.get("GUEST", "9202") -BASE = os.environ.get("BASE") or {"9202": "https://192.168.0.114", "9201": "https://192.168.0.155"}[GUEST] -DOMAIN = os.environ.get("DOMAIN", "enkisfelhom.hu") +# THE TARGETS (R-892, 2026-10-06): one row per box the walk may drive. `hp` is the box's NAME for the guards +# (upgrade_fixtures_box.BOX_ADMIN_SEED_GUESTS keys on (hp, guest)); `ssh` is how its Proxmox host is reached, with an +# optional `jump` (never an edit of DooPlex's ~/.ssh/config); `guest` the customer LXC; `base` and `domain` its dashboard. +# TARGET=9202 (default) scratch guest 9202 on demo-hp +# TARGET=9201 demo-hp's hub-enabled guest (the mail proof) — household-shaped, never seeded through an admin +# TARGET=tester-1 the Tester 1 box: VM 341 on the HP box (`09` §3 decision 158), its node `felhom` at +# 192.168.0.154, guest 9201 at 192.168.0.101 for felhom.enkicsifelhom.hu (identity matched +# 2026-10-06: the agent's report `host.node=felhom` = the VM's certificate; the guest answers +# that domain, the other one 404 — `felhom.eu/documentation/audits/readback-2026-10-07/`) +# GUEST= still selects a demo-hp row (the old switch), unless TARGET is set. +TARGETS = { + "9202": {"hp": "demo-hp", "ssh": "demo-hp", "jump": None, "guest": "9202", + "base": "https://192.168.0.114", "domain": "enkisfelhom.hu"}, + "9201": {"hp": "demo-hp", "ssh": "demo-hp", "jump": None, "guest": "9201", + "base": "https://192.168.0.155", "domain": "enkisfelhom.hu"}, + "tester-1": {"hp": "tester-1", "ssh": "root@192.168.0.154", "jump": "demo-hp", "guest": "9201", + "base": "https://192.168.0.101", "domain": "enkicsifelhom.hu"}, +} +TARGET = os.environ.get("TARGET") or os.environ.get("GUEST", "9202") +if TARGET not in TARGETS: + raise SystemExit(f"box_walk: unknown TARGET {TARGET!r} — one of {sorted(TARGETS)}") +_T = TARGETS[TARGET] +GUEST = _T["guest"] +BASE = os.environ.get("BASE") or _T["base"] +DOMAIN = os.environ.get("DOMAIN") or _T["domain"] HOSTHDR = f"Host: felhom.{DOMAIN}" -HP = "demo-hp" +HP = _T["hp"] + + +def ssh_args(): + """The ssh argv prefix that reaches the target's Proxmox host (a jump when the row names one).""" + a = ["ssh", "-o", "ConnectTimeout=20", "-o", "StrictHostKeyChecking=accept-new"] + if _T["jump"]: + a += ["-J", _T["jump"]] + return a + [_T["ssh"]] LOG = [] @@ -58,7 +87,7 @@ def guest(script, timeout=600): # two concurrent walks (memory: guest-helper-shares-one-tmp-file). import secrets as _s t = f"/tmp/w{GUEST}-{os.getpid()}-{_s.token_hex(4)}.sh" - r = sh(["ssh", "-o", "ConnectTimeout=20", "-o", "StrictHostKeyChecking=accept-new", HP, + r = sh(ssh_args() + [ f"export LC_ALL=C; cat > {t}; pct push {GUEST} {t} {t} >/dev/null 2>&1; " f"pct exec {GUEST} -- bash {t}; pct exec {GUEST} -- rm -f {t}; rm -f {t}"], timeout=timeout, inp=script) diff --git a/scripts/test_upgrade_bench.py b/scripts/test_upgrade_bench.py index 374fb64..989a366 100644 --- a/scripts/test_upgrade_bench.py +++ b/scripts/test_upgrade_bench.py @@ -346,6 +346,12 @@ class BoxAdminSeedGuard(unittest.TestCase): self.assertIsNone(got) self.assertIn("test-box admin invite", fx_.tried) + def test_the_tester_1_box_is_a_test_box_and_demo_hp_9201_is_not(self): + self.assertEqual(self.allowed(FakeBoxWalk(hp="tester-1", guest="9201"), self.ENV), (True, "")) + ok, why = self.allowed(FakeBoxWalk(hp="demo-hp", guest="9201"), self.ENV) + self.assertFalse(ok) + self.assertIn("not a test box", why) + def test_only_a_drill_address_is_invited(self): with self.assertRaises(ValueError): fxbox.box_admin_invite(FakeBoxWalk(), 'x"}; rm -rf /; {"@gate.invalid') @@ -390,6 +396,51 @@ class DefinitionEdge(unittest.TestCase): ut.add_definition_edge("app1", "same") +class BoxWalkTargets(unittest.TestCase): + """R-892: box_walk's target table. Each test imports box_walk afresh under its own TARGET. RED-PROOF (REPORT): drop + the `-J` from ssh_args — test_the_tester_1_box_is_reached_through_the_hp_box fails.""" + + def load(self, env): + import importlib + with mock.patch.dict(os.environ, env, clear=False): + for k in ("TARGET", "GUEST", "BASE", "DOMAIN"): + if k not in env: + os.environ.pop(k, None) + sys.modules.pop("box_walk", None) + return importlib.import_module("box_walk") + + def test_every_row_is_complete(self): + bw = self.load({}) + for name, row in bw.TARGETS.items(): + self.assertEqual(set(row), {"hp", "ssh", "jump", "guest", "base", "domain"}, name) + self.assertTrue(row["base"].startswith("https://"), name) + + def test_the_default_stays_scratch_9202(self): + bw = self.load({}) + self.assertEqual((bw.HP, bw.GUEST, bw.BASE), ("demo-hp", "9202", "https://192.168.0.114")) + self.assertEqual(bw.ssh_args()[-1], "demo-hp") + self.assertNotIn("-J", bw.ssh_args()) + + def test_the_old_guest_switch_still_selects_demo_hp_9201(self): + bw = self.load({"GUEST": "9201"}) + self.assertEqual((bw.HP, bw.GUEST, bw.BASE), ("demo-hp", "9201", "https://192.168.0.155")) + + def test_the_tester_1_box_is_reached_through_the_hp_box(self): + bw = self.load({"TARGET": "tester-1"}) + self.assertEqual((bw.HP, bw.GUEST, bw.DOMAIN), ("tester-1", "9201", "enkicsifelhom.hu")) + a = bw.ssh_args() + self.assertEqual(a[a.index("-J") + 1], "demo-hp") + self.assertEqual(a[-1], "root@192.168.0.154") + self.assertIn(("tester-1", "9201"), fxbox.BOX_ADMIN_SEED_GUESTS) + + def test_an_unknown_target_is_refused(self): + with self.assertRaises(SystemExit): + self.load({"TARGET": "ep0"}) + + def tearDown(self): + sys.modules.pop("box_walk", None) + + class SecretHygiene(unittest.TestCase): """The run's secrets: .env 0600 and shredded, every evidence file redacted. RED-PROOF (REPORT): make redact_tree return [] without rewriting — test_evidence_files_are_redacted fails.""" diff --git a/scripts/upgrade_fixtures_box.py b/scripts/upgrade_fixtures_box.py index 114bb6b..f5238bd 100644 --- a/scripts/upgrade_fixtures_box.py +++ b/scripts/upgrade_fixtures_box.py @@ -306,11 +306,10 @@ def bench_admin_seed_allowed(w): # --- R-890 (`09` §3 decision 149): the TEST BOXES may seed vaultwarden through its admin route too --------------------- BOX_ADMIN_SEED_ENV = "FELHOM_BOX_ADMIN_SEED" # the run's explicit opt-in on a box walk -# The test boxes the walk can reach, as (Proxmox host, guest). Scratch 9202 only: box_walk.py drives guests on demo-hp -# alone (its HP), and 9201 there is demo-hp's household-shaped guest — a demo box's household apps are never seeded -# through their admin (decision 149). The Tester 1 box is allowed by the ruling but the walk has no route to it; add -# it here the day the walk can run there. -BOX_ADMIN_SEED_GUESTS = {("demo-hp", "9202")} +# The test boxes, as (box_walk target's `hp` name, guest): scratch 9202 on demo-hp and the Tester 1 box's guest 9201 +# (VM 341 on the HP box, `09` §3 decisions 149 and 158 — disposable). demo-hp's 9201 is household-shaped and is NEVER +# here: a demo box's household apps are never seeded through their admin. +BOX_ADMIN_SEED_GUESTS = {("demo-hp", "9202"), ("tester-1", "9201")} DRILL_CATALOG_MARK = "app-catalog-drill" # a test run points the box at the drill catalog (`09` §6.5) @@ -350,7 +349,8 @@ T=$(docker exec "$c" printenv ADMIN_TOKEN 2>/dev/null) H=$(mktemp); chmod 600 "$H" code=$(printf %s "$T" | curl -s -o /dev/null -D "$H" -w '%{http_code}' --max-time 30 --data-urlencode token@- "http://$ip:80/admin") T= -ck=$(grep -i '^set-cookie: *VW_ADMIN=' "$H" | head -1 | sed -e 's/^[Ss]et-[Cc]ookie: *//' -e 's/;.*//' | tr -d ' ') +ck=$(grep -i '^set-cookie: *VW_ADMIN=' "$H" | head -1 | sed -e 's/^[Ss]et-[Cc]ookie: *//' -e 's/;.*//' | tr -d ' +') if [ -z "$ck" ]; then shred -u "$H"; echo "RESULT signin=$code session=no"; exit 0; fi printf 'Cookie: %s ' "$ck" > "$H"; ck=