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)
gates / gates (push) Successful in 5s
gates / gates (push) Successful in 5s
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,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).
|
||||
|
||||
+35
-6
@@ -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=<vmid> 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)
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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=
|
||||
|
||||
Reference in New Issue
Block a user