Rulings 61-62 built: calibre-web generated login name, the registry prune rule; R-750/R-752 closed, R-756/R-757 opened; runbook 4.1a; STATUS, report, evidence
gates / gates (push) Successful in 26s
gates / gates (push) Successful in 26s
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:
@@ -643,11 +643,15 @@ R-636's louder repeated alarm.
|
||||
password, so a stranger cannot aim the per-name lock at it — *operator ruling 2026-10-01 (R-752, option A).* The lock
|
||||
itself stays (the guessing guard is kept). **Why:** calibre-web locks a NAME for a day after 40 wrong tries
|
||||
(`cps/web.py:2218`, measured), has no knob for that length, and its default name `admin` is public.
|
||||
**Outcome (same day):** catalog `e9f50b5` — `ADMIN_USER` (`secret`, `hex:5`, no controller change); proven on 9202;
|
||||
demo-hp renamed by hand. An installed app is given a made-up name by the box's missing-field injection (R-757).
|
||||
62. **Registry retention: the prune keeps the newest 20 versions of each image, plus every version named by the vouched
|
||||
golden, the controller floor and the vouched agent** (`golden_version`, the floor, `agent_version`, `min_agent`) **and
|
||||
the controller image the golden names** — *operator ruling 2026-10-01 (R-750, option A).* It never runs on a schedule
|
||||
unless the operator says so; `--apply` is a person's act. **Why:** a manual `--keep 7` run (HM-024, August) kept about
|
||||
a day of controller releases and protected nothing in use.
|
||||
**Outcome (same day):** `admin/misc-scripts` `c9d5ed5`; live dry-run protected controller 0.285.0, golden 0.285.0,
|
||||
agent 0.138.0 + 0.131.0, hub 0.126.0 (the running hub, added: a version in use) and felhom-samba 1.1.0; nothing deleted.
|
||||
|
||||
### 2026-10-01 (afternoon) — decided by CC unattended, operator may reverse (R-752)
|
||||
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
11:01:30 the box's template copy names ADMIN_USER: 4 times
|
||||
13:01:33 [1] made the drive paths this app requires: ['/mnt/felhom-drives/scratch_hdd/userdata/calibre-web']
|
||||
13:01:33 [1] required fields filled beyond DOMAIN/SUBDOMAIN: ['HDD_PATH', 'ADMIN_PASSWORD']
|
||||
13:01:34 [1] deploy -> 202 {'ok': True, 'message': 'Telepítés elindítva – az állapot a kártyán követhető'}
|
||||
13:02:19 [1] deployed, controller state=running, pinned={'calibre-web': 'crocodilestick/calibre-web-automated:v4.0.8'}
|
||||
11:02:19 deploy: True
|
||||
11:02:34 stranger, default login admin/admin123 -> other:404
|
||||
11:02:34 stranger, default login admin/admin123 -> other:401
|
||||
11:02:34 stranger, default login admin/admin123 -> wrong
|
||||
11:02:34 stranger, default login admin/admin123 -> locked
|
||||
11:02:34 stranger got in with the default login: 0 of 31
|
||||
11:02:37 2026/10/01 11:01:34 install_hold.go:106: [INFO] [stacks] calibre-web: install HOLD before the first start — only the household reaches [books.enkisfelhom.hu] until the known first login is replaced
|
||||
2026/10/01 11:02:17 install_hold.go:135: [INFO] [stacks] calibre-web: install hold OPENED by after_install — the app is reached as without a hold
|
||||
11:02:40 app.yaml after_install record: after_install: at: "2026-10-01T11:02:17Z" ok: true install_hold:
|
||||
11:02:40 the household's name (read through the page's reveal): 10 characters, lowercase hex: True
|
||||
11:02:43 users in app.db (total, named admin): (2, 0)
|
||||
11:02:44 positive — form, name + password: ok | OPDS: ok
|
||||
11:02:44 admin + the RIGHT password (the old name is gone): locked
|
||||
11:02:44 ## a stranger: 40 wrong tries on `admin`, 3 a minute
|
||||
11:02:44 wrong try 1: locked
|
||||
11:02:44 wrong try 2: locked
|
||||
11:02:44 wrong try 3: locked
|
||||
11:05:48 wrong try 10: wrong
|
||||
11:08:51 wrong try 20: wrong
|
||||
11:11:55 wrong try 30: wrong
|
||||
11:16:00 wrong try 40: wrong
|
||||
11:17:01 one more stranger try on admin: locked
|
||||
11:17:02 HOUSEHOLD at once — form, own name + password: ok | OPDS: ok
|
||||
11:17:02 a wrong password on the real name: wrong | right again: ok
|
||||
13:17:07 [X] stop -> 200 {'ok': True, 'message': 'Stack calibre-web stop completed'}
|
||||
13:17:12 [X] remove (with drive data) -> 409 {'ok': False, 'error': 'A(z) /mnt/felhom-drives/scratch_hdd/userdata/calibre-web tárhely jelenleg nem elérhető — az alkalmazás nem távolítható el, amíg a meghaj
|
||||
13:17:12 [X] refused because the drive path cannot be resolved (R-442, fail-closed and right) — removing the app and KEEPING the drive data instead
|
||||
13:17:39 [X] remove (keeping drive data) -> 200 {'ok': True, 'data': {'removed': 'calibre-web', 'volumes_removed': ['calibre-web_calibre_web_config'], 'hdd_paths_removed': [], 'hdd_paths_preserved': [], 'back
|
||||
13:17:47 [X] after remove: deployed=False leftovers='/opt/docker/stacks/calibre-web'
|
||||
11:17:47 removed; deployed = False
|
||||
@@ -0,0 +1,14 @@
|
||||
# demo-hp 9201 calibre-web rename 2026-10-01T11:19:40Z (values in through stdin; never printed)
|
||||
admin row found: True
|
||||
cps.py: 0 (Standard/LDAP login)
|
||||
Password for user '<name>' changed
|
||||
hash takes the password: True | admin left: 0
|
||||
demo-hp login form — new name + password: ok | OPDS: ok
|
||||
demo-hp login form — admin + the same password: wrong
|
||||
# demo-hp: rename again, to the name the BOX recorded (InjectMissingFields 11:26:10) — 2026-10-01T11:27:25Z
|
||||
the current name found: True
|
||||
cps.py rc: 0
|
||||
hash takes the password: True | old name left: 0 | admin left: 0
|
||||
demo-hp — the box's recorded name + password: ok | OPDS: ok
|
||||
demo-hp — the previous name + password: wrong | admin + password: wrong
|
||||
credentials file: holds the box's recorded name
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
page carries the ADMIN_USER field: False
|
||||
label shown: False | reveal button: False
|
||||
reveal ADMIN_USER -> ok: True | value length: 10 | error:
|
||||
the password field's description now says 'the user name above': False
|
||||
@@ -0,0 +1,22 @@
|
||||
## GREEN
|
||||
PASS --keep defaults to 20
|
||||
PASS plan: delete 4, keep 20, protect 2 (latest + the floor)
|
||||
PASS the in-use 0.262.0 is not in the delete plan
|
||||
PASS the plan says why 0.262.0 is kept
|
||||
PASS dry-run
|
||||
PASS red: without the in-use list 0.262.0 WOULD be deleted
|
||||
PASS unreadable in-use list -> refused (exit 3)
|
||||
rc=0
|
||||
1
|
||||
## RED (is_protected without the in-use check)
|
||||
PASS --keep defaults to 20
|
||||
FAIL plan counts: Would delete 5 tag(s); keep 20; protect 1:
|
||||
FAIL the in-use 0.262.0 is in the delete plan
|
||||
FAIL no reason printed for 0.262.0
|
||||
PASS dry-run
|
||||
PASS red: without the in-use list 0.262.0 WOULD be deleted
|
||||
PASS unreadable in-use list -> refused (exit 3)
|
||||
rc=1
|
||||
sed: couldn't edit /dev/null: not a regular file
|
||||
## GREEN again
|
||||
rc=0
|
||||
@@ -0,0 +1,173 @@
|
||||
################ --type container --all prune (DRY-RUN)
|
||||
|
||||
[INFO] ╔══════════════════════════════════════════╗
|
||||
[INFO] ║ Gitea package prune ║
|
||||
[INFO] ╚══════════════════════════════════════════╝
|
||||
[INFO] Server: https://gitea.dooplex.hu Owner: admin Type: container Log: /tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/b4d68b9b-a6cf-4d21-8220-ece956837fc3/scratchpad/prune-dry.log
|
||||
[INFO] Auth: git credential helper (user: kisfenyo)
|
||||
[INFO] Gitea version: 1.26.2
|
||||
[STEP] Fetching container packages for owner 'admin' from https://gitea.dooplex.hu ...
|
||||
[INFO] Loaded 448 version records across 9 package(s).
|
||||
[WARN] --all covers type 'container' ONLY. Other types are untouched by this run.
|
||||
[INFO] prune: --keep defaults to 20 (decision 62)
|
||||
[INFO] In-use list (decision 62): 6 version(s) from the hub's vouch, golden 0.285.0's bake.log and the hub manifest.
|
||||
in use: felhom-agent:0.131.0 — min_agent
|
||||
in use: felhom-agent:0.138.0 — the vouched agent
|
||||
in use: felhom-controller:0.285.0 — the controller floor; the vouched golden's controller; baked into golden 0.285.0
|
||||
in use: felhom-golden:0.285.0 — the vouched golden
|
||||
in use: felhom-hub:0.126.0 — the hub the GitOps manifest runs
|
||||
in use: felhom-samba:1.1.0 — baked into golden 0.285.0
|
||||
[INFO] Action: prune Type: container Targets: felhom-act-runner felhom-controller felhom-hub felhom-samba jarr recipe-importer revfulop-calendar sparkyfitness-export wan-probe
|
||||
|
||||
== prune felhom-act-runner == mode: keep-last 20
|
||||
[INFO] Nothing to prune (1 tags: 1 kept, 0 protected).
|
||||
|
||||
== prune felhom-controller == mode: keep-last 20
|
||||
PROTECTED latest — matches --protect ^latest$
|
||||
PROTECTED 0.285.0 — the controller floor; the vouched golden's controller; baked into golden 0.285.0
|
||||
Would delete 70 tag(s); keep 20; protect 2:
|
||||
0.271.0
|
||||
0.270.0
|
||||
0.269.1
|
||||
0.269.0
|
||||
0.268.0
|
||||
0.267.0
|
||||
0.266.0
|
||||
0.265.0
|
||||
0.264.0
|
||||
0.263.2
|
||||
0.263.1
|
||||
0.263.0
|
||||
0.262.1
|
||||
0.262.0
|
||||
0.261.0
|
||||
0.260.0
|
||||
0.259.0
|
||||
0.258.0
|
||||
0.257.0
|
||||
0.256.1
|
||||
0.256.0
|
||||
0.255.0
|
||||
0.254.0
|
||||
0.253.0
|
||||
0.252.0
|
||||
0.251.0
|
||||
0.250.0
|
||||
0.249.0
|
||||
0.248.0
|
||||
0.247.0
|
||||
0.246.0
|
||||
0.245.0
|
||||
0.244.0
|
||||
0.243.0
|
||||
0.242.0
|
||||
0.241.0
|
||||
0.240.0
|
||||
0.239.0
|
||||
0.238.1
|
||||
0.238.0
|
||||
0.237.0
|
||||
0.236.0
|
||||
0.235.0
|
||||
0.234.0
|
||||
0.233.0
|
||||
0.232.0
|
||||
0.231.0
|
||||
0.230.0
|
||||
0.229.0
|
||||
0.228.0
|
||||
0.227.1
|
||||
0.227.0
|
||||
0.226.1
|
||||
0.226.0
|
||||
0.225.0
|
||||
0.224.0
|
||||
0.223.0
|
||||
0.222.0
|
||||
0.221.1
|
||||
0.221.0
|
||||
0.220.2
|
||||
0.220.1
|
||||
0.220.0
|
||||
0.219.0
|
||||
0.218.0
|
||||
0.217.0
|
||||
0.216.0
|
||||
0.215.0
|
||||
0.214.0
|
||||
0.213.0
|
||||
[WARN] DRY-RUN — nothing deleted. --apply is a person's act (decision 62); nothing schedules it.
|
||||
|
||||
== prune felhom-hub == mode: keep-last 20
|
||||
PROTECTED latest — matches --protect ^latest$
|
||||
PROTECTED 0.126.0 — the hub the GitOps manifest runs
|
||||
Would delete 8 tag(s); keep 20; protect 2:
|
||||
0.107.0
|
||||
0.106.0
|
||||
0.105.0
|
||||
0.104.0
|
||||
0.103.0
|
||||
0.102.0
|
||||
0.101.0
|
||||
0.100.2
|
||||
[WARN] DRY-RUN — nothing deleted. --apply is a person's act (decision 62); nothing schedules it.
|
||||
|
||||
== prune felhom-samba == mode: keep-last 20
|
||||
PROTECTED 1.1.0 — baked into golden 0.285.0
|
||||
[INFO] Nothing to prune (2 tags: 1 kept, 1 protected).
|
||||
|
||||
== prune jarr == mode: keep-last 20
|
||||
PROTECTED latest — matches --protect ^latest$
|
||||
[INFO] Nothing to prune (2 tags: 1 kept, 1 protected).
|
||||
|
||||
== prune recipe-importer == mode: keep-last 20
|
||||
PROTECTED latest — matches --protect ^latest$
|
||||
[INFO] Nothing to prune (8 tags: 7 kept, 1 protected).
|
||||
|
||||
== prune revfulop-calendar == mode: keep-last 20
|
||||
[INFO] Nothing to prune (7 tags: 7 kept, 0 protected).
|
||||
|
||||
== prune sparkyfitness-export == mode: keep-last 20
|
||||
[INFO] Nothing to prune (4 tags: 4 kept, 0 protected).
|
||||
|
||||
== prune wan-probe == mode: keep-last 20
|
||||
[INFO] Nothing to prune (1 tags: 1 kept, 0 protected).
|
||||
|
||||
[INFO] Prune summary: deleted=0 failed=0 (dry-run=yes)
|
||||
|
||||
[INFO] Done. Audit log: /tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/b4d68b9b-a6cf-4d21-8220-ece956837fc3/scratchpad/prune-dry.log
|
||||
rc=0
|
||||
################ --type generic --all prune (DRY-RUN)
|
||||
|
||||
[INFO] ╔══════════════════════════════════════════╗
|
||||
[INFO] ║ Gitea package prune ║
|
||||
[INFO] ╚══════════════════════════════════════════╝
|
||||
[INFO] Server: https://gitea.dooplex.hu Owner: admin Type: generic Log: /tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/b4d68b9b-a6cf-4d21-8220-ece956837fc3/scratchpad/prune-dry.log
|
||||
[INFO] Auth: git credential helper (user: kisfenyo)
|
||||
[INFO] Gitea version: 1.26.2
|
||||
[STEP] Fetching generic packages for owner 'admin' from https://gitea.dooplex.hu ...
|
||||
[INFO] Loaded 41 version records across 2 package(s).
|
||||
[WARN] --all covers type 'generic' ONLY. Other types are untouched by this run.
|
||||
[INFO] prune: --keep defaults to 20 (decision 62)
|
||||
[INFO] In-use list (decision 62): 6 version(s) from the hub's vouch, golden 0.285.0's bake.log and the hub manifest.
|
||||
in use: felhom-agent:0.131.0 — min_agent
|
||||
in use: felhom-agent:0.138.0 — the vouched agent
|
||||
in use: felhom-controller:0.285.0 — the controller floor; the vouched golden's controller; baked into golden 0.285.0
|
||||
in use: felhom-golden:0.285.0 — the vouched golden
|
||||
in use: felhom-hub:0.126.0 — the hub the GitOps manifest runs
|
||||
in use: felhom-samba:1.1.0 — baked into golden 0.285.0
|
||||
[INFO] Action: prune Type: generic Targets: felhom-agent felhom-golden
|
||||
|
||||
== prune felhom-agent == mode: keep-last 20
|
||||
PROTECTED 0.138.0 — the vouched agent
|
||||
PROTECTED 0.131.0 — min_agent
|
||||
[INFO] Nothing to prune (20 tags: 18 kept, 2 protected).
|
||||
|
||||
== prune felhom-golden == mode: keep-last 20
|
||||
PROTECTED 0.285.0 — the vouched golden
|
||||
[INFO] Nothing to prune (21 tags: 20 kept, 1 protected).
|
||||
|
||||
[INFO] Prune summary: deleted=0 failed=0 (dry-run=yes)
|
||||
|
||||
[INFO] Done. Audit log: /tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/b4d68b9b-a6cf-4d21-8220-ece956837fc3/scratchpad/prune-dry.log
|
||||
rc=0
|
||||
@@ -0,0 +1,12 @@
|
||||
# Rulings 61 (calibre-web's generated login name) and 62 (the registry prune rule) — 2026-10-01
|
||||
|
||||
Report: `felhom.eu/REPORT-calibre-name-and-prune-2026-10-01.md`. Tools: `../lockouts-2026-10-01/tools/`.
|
||||
|
||||
| file | what |
|
||||
|---|---|
|
||||
| `A/A1-9202-calibre-generated-name.txt` | 9202: install hold, after_install, the name, 40 wrong tries on `admin`, the household in at once |
|
||||
| `A/A2-demo-hp-rename.txt` | demo-hp: the rename by hand (twice — see A3), real logins over its traefik; no value printed |
|
||||
| `A/A3-demo-hp-page-after-sync.txt` | demo-hp after the sync: the box injected its own `ADMIN_USER` (R-757) |
|
||||
| `B/B1-test-and-red-proof.txt` | `misc-scripts/tests/test-prune-plan.sh` green, red (in-use check removed), green |
|
||||
| `B/B2-live-dry-run.txt` | the live dry-run, both package types — nothing deleted |
|
||||
| `T/` | 9202 back on live, the drill reset |
|
||||
@@ -0,0 +1,15 @@
|
||||
git:
|
||||
branch: main
|
||||
repo_url: https://gitea.dooplex.hu/admin/app-catalog-felhom.eu.git
|
||||
sync_interval: 15m
|
||||
token: <redacted>
|
||||
username: ""
|
||||
hub:
|
||||
0
|
||||
|
||||
felhom-controller gitea.dooplex.hu/admin/felhom-controller:0.285.0
|
||||
filebrowser gtstef/filebrowser:1.3.3-stable
|
||||
paperless-postgres postgres:18-alpine
|
||||
paperless-redis redis:7-alpine
|
||||
paperless-webserver ghcr.io/paperless-ngx/paperless-ngx:2.20.15
|
||||
traefik traefik:v3.6.7
|
||||
@@ -0,0 +1,3 @@
|
||||
# drill reset 2026-10-01T11:28:12Z
|
||||
4e18b3a DRILL calibre-web: generated admin login name (decision 61 proof on 9202)
|
||||
after: e9f50b5 = live e9f50b5
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Decision 61 (R-752) on 9202, DRILL catalog: calibre-web with a generated admin LOGIN NAME.
|
||||
1 the box's template copy carries ADMIN_USER; 2 a stranger polls the public default login from the deploy press until
|
||||
the install hold opens (R-741's window); 3 the household's name is read the way the app page reads it (the Settings
|
||||
page's reveal, POST /stacks/<n>/auto-field/reveal); 4 positive controls (name + password: the login form and OPDS),
|
||||
`admin` + the right password refused; 5 a stranger's 40 wrong tries on `admin` at the 3/minute pace (~14 min); 6 the
|
||||
household logs in AT ONCE with its own name (form and OPDS); a wrong password on the real name still refused. Removed."""
|
||||
import json, subprocess, threading, time
|
||||
import walk as w
|
||||
import lk
|
||||
|
||||
APP, SUB = "calibre-web", "books"
|
||||
|
||||
|
||||
def reveal(env_var):
|
||||
sess = open(f"{w.SC}/sess{__import__('os').getpid()}.txt").read().strip()
|
||||
csrf = open(f"{w.SC}/csrf{__import__('os').getpid()}.txt").read().strip()
|
||||
r = subprocess.run(["curl", "-sk", "-H", w.HOSTHDR, "-H", f"Cookie: {sess}", "-H", f"X-CSRF-Token: {csrf}",
|
||||
"-H", "Content-Type: application/x-www-form-urlencoded", "--data", f"env_var={env_var}",
|
||||
f"{w.BASE}/stacks/{APP}/auto-field/reveal"], capture_output=True, text=True)
|
||||
try:
|
||||
return json.loads(r.stdout)["data"]["value"]
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
w.login()
|
||||
for _ in range(12):
|
||||
w.sync_rescan()
|
||||
cp = w.guest(f"grep -c ADMIN_USER /opt/docker/stacks/{APP}/.felhom.yml").strip()
|
||||
if cp not in ("", "0"):
|
||||
break
|
||||
time.sleep(15)
|
||||
lk.p("the box's template copy names ADMIN_USER:", cp, "times")
|
||||
|
||||
res, stop = [], threading.Event()
|
||||
|
||||
|
||||
def poll():
|
||||
while not stop.is_set():
|
||||
res.append((lk.now(), lk.calibre("admin", "admin123")))
|
||||
time.sleep(2)
|
||||
|
||||
|
||||
t = threading.Thread(target=poll, daemon=True)
|
||||
t.start()
|
||||
lk.p("deploy:", w.deploy(APP, SUB))
|
||||
for _ in range(120):
|
||||
time.sleep(5)
|
||||
if "hold OPENED" in w.guest(f"docker logs --since 20m felhom-controller 2>&1 | grep '{APP}: install hold OPENED'"):
|
||||
break
|
||||
time.sleep(5)
|
||||
stop.set()
|
||||
t.join()
|
||||
prev = None
|
||||
for ts, c in res:
|
||||
if c != prev:
|
||||
lk.p(" stranger, default login admin/admin123 ->", c)
|
||||
prev = c
|
||||
lk.p("stranger got in with the default login:", sum(1 for _, c in res if c == "ok"), "of", len(res))
|
||||
lk.p(w.guest("docker logs --since 20m felhom-controller 2>&1 | grep -E 'calibre-web.*(install HOLD|hold OPENED|after_install)' | cut -c1-200").strip())
|
||||
lk.p("app.yaml after_install record:", w.guest(f"grep -A3 '^after_install:' /opt/docker/stacks/{APP}/app.yaml").strip().replace("\n", " "))
|
||||
|
||||
name = reveal("ADMIN_USER")
|
||||
pw = (w.GENERATED.get(APP) or {}).get("ADMIN_PASSWORD", "")
|
||||
lk.p(f"the household's name (read through the page's reveal): {len(name)} characters, lowercase hex: {name.isalnum() and name == name.lower()}")
|
||||
lk.p("users in app.db (total, named admin):", w.guest("""cat > /tmp/cwu.py <<'PY'
|
||||
import sqlite3
|
||||
c = sqlite3.connect('/config/app.db')
|
||||
print(c.execute("SELECT count(*), sum(lower(name) = 'admin') FROM user").fetchone())
|
||||
PY
|
||||
docker cp /tmp/cwu.py calibre-web:/tmp/cwu.py && docker exec calibre-web python3 /tmp/cwu.py""").strip())
|
||||
lk.p("positive — form, name + password:", lk.calibre(name, pw), "| OPDS:", lk.calibre_opds(name, pw))
|
||||
lk.p("admin + the RIGHT password (the old name is gone):", lk.calibre("admin", pw))
|
||||
lk.p("## a stranger: 40 wrong tries on `admin`, 3 a minute")
|
||||
n = 0
|
||||
while n < 40:
|
||||
for _ in range(3):
|
||||
if n < 40:
|
||||
n += 1
|
||||
r = lk.calibre("admin", f"daily-{n}")
|
||||
if n % 10 == 0 or r not in ("wrong",):
|
||||
lk.p(f" wrong try {n}: {r}")
|
||||
time.sleep(61)
|
||||
lk.p("one more stranger try on admin:", lk.calibre("admin", "one-more"))
|
||||
lk.p("HOUSEHOLD at once — form, own name + password:", lk.calibre(name, pw), "| OPDS:", lk.calibre_opds(name, pw))
|
||||
lk.p("a wrong password on the real name:", lk.calibre(name, "wrong-real"), "| right again:", lk.calibre(name, pw))
|
||||
w.remove(APP)
|
||||
lk.p("removed; deployed =", w.stack(APP).get("deployed"))
|
||||
@@ -861,12 +861,14 @@ class (an image `VOLUME` at an unmounted path) is still live — `immich-server`
|
||||
| **R-747** | **[P3-LOW] A stranger can lock the household out of mealie with five wrong logins.** MEASURED 2026-09-30 on 9202 (the R-741 proof): after the install hold opened, a stranger's default-login tries were refused (401) and after five of them mealie answered 423 (locked) to every login — the generated, correct password included. mealie's own brute-force guard, on an app published on the internet; the setup gate and the install hold do not cover an app after its first setup. Not measured: how long the lock lasts. **Needs:** measure the lock's length; decide whether the page tells the household what to do. `audits/night-rulings-2026-09-30/C/C3-mealie-poll.txt` **-- 2026-10-01:** Measured at v3.28.0 (source + 9202): 5 wrong logins lock the ACCOUNT (not the IP) for `SECURITY_USER_LOCKOUT_TIME` hours (default 24); the lock is lifted by an hourly job; an admin can unlock others via `POST /api/admin/users/unlock`, but the household's only admin is the locked account. Both login names are public (`admin`, `changeme@example.com`). **Fixed** (`09` §3 decision 57, decided by CC unattended — operator may reverse): `SECURITY_USER_LOCKOUT_TIME=1`, catalog `a4597cd`; on 9202 the right password answered 423 for 120 min, then 200; a wrong one still 401 (`audits/rulings-2026-10-01/C/`). **Left:** a stranger can renew the lock every hour (per account, public name — option (d) in decision 57 would end that); the page does not tell the household why it is locked; an INSTALLED mealie takes the new setting only when its compose is rendered again (not measured which act does that). | **NARROWED — the lock is 1–2 h; renewal and the page remain; owner: CC** |
|
||||
| **R-748** | **[P3-LOW] The register-shape gate skipped every row whose id has a letter suffix — so R-88a, R-88b and R-209a were never shape-checked, and its count read 3 short.** FOUND 2026-09-30 (late) while counting the register: `register_shape_gate.py` matched `R-\d+` only; the brief's „the reviewer's regex undercounted by 3” is the same three rows. Fixed the same session: `R-\d+[a-z]?`; decoy `suffix-row-eaten-state` (a suffixed row with its state cell eaten) seen passing with the old pattern and convicted with the new. The register is **382** rows by either count now. | **CLOSED 2026-09-30 — `scripts/register_shape_gate.py`** |
|
||||
| **R-749** | **[P3-LOW] `retest-floating.py` could never start on a fresh bench: it checked for `/opt/upg/upgrade-test.py` on the bench BEFORE the step that copies it there.** FOUND 2026-10-01 at the first full monthly run (decision 55): bench 9401 freshly created by the runbook, the run answered „CANNOT START — missing: the bench LXC 9401 on demo-hp with /opt/upg” in one minute. The runbook says the command syncs the bench itself — it does, but only after the check. On 2026-09-30 the bench had been synced by hand earlier, so nobody saw it. **Needs:** the check asks for what the bench must bring (docker, python3), the sync then provides `/opt/upg`. `audits/rulings-2026-10-01/A/` **-- 2026-10-01:** Fixed the same session (catalog `9e53205`): the check asks for docker + python3; after the sync `/opt/upg/upgrade-test.py` is required. The re-run started at once and finished both apps. | **CLOSED 2026-10-01 — catalog 9e53205** |
|
||||
| **R-750** | **[P3-LOW] The registry no longer holds controller releases older than 0.213.0 (2026-08-12) — something removed them, and nothing records what.** MEASURED 2026-10-01 (anonymous registry API, `audits/rulings-2026-10-01/B/`): `felhom-controller` has 91 tags, the oldest release 0.213.0; `0.201.0` answers 404; Gitea's package list starts 2026-08-12. No runbook, row or memory names a clean-up. Today nothing needs those versions: a box runs a newer one, a whole-guest restore brings the guest's own Docker store back (mp0 `backup=1`), and decision 56 deletes only on the box. **But** a box or a backup that names a removed version cannot pull it again (R-698's shape, for the controller). **Needs:** find what removed them (a Gitea clean-up rule?), and record the rule — or say it was a one-time act. Read-only on DooPlex. **-- 2026-10-01 (afternoon):** **Answered (read only, `audits/lockouts-2026-10-01/C/C1-registry-read.txt`).** Not a Gitea rule: `package_cleanup_rule` is EMPTY (READ ONLY query on the shared CNPG); `[cron.cleanup_packages]` only runs rules and Gitea's own expired-data clean-up. The cause is a MANUAL run of DooPlex's `~/git/misc-scripts/gitea-image-prune.sh --all --keep 7 --apply --reclaim` on the night of 2026-08-22/23, recorded in homelab-manifests HM-024 (the Gitea volume was full: `felhom-golden` 22 → 3 versions, /data 14.8 → 4.4 G); `--keep 7` per container package explains controller 0.213.0 (2026-08-12) as the oldest left. Nothing schedules it (crontabs, timers, cluster CronJobs read). If run again as its usage text says, it keeps 7 controller releases (about a day) and `--type generic --keep 3` would cut the agent to 3; it protects no vouched version. **Needs:** the operator's word on a written rule (STATUS). | **ANSWERED — WAITING-ON-OPERATOR: keep the manual prune or write a rule; owner: operator** |
|
||||
| **R-750** | **[P3-LOW] The registry no longer holds controller releases older than 0.213.0 (2026-08-12) — something removed them, and nothing records what.** MEASURED 2026-10-01 (anonymous registry API, `audits/rulings-2026-10-01/B/`): `felhom-controller` has 91 tags, the oldest release 0.213.0; `0.201.0` answers 404; Gitea's package list starts 2026-08-12. No runbook, row or memory names a clean-up. Today nothing needs those versions: a box runs a newer one, a whole-guest restore brings the guest's own Docker store back (mp0 `backup=1`), and decision 56 deletes only on the box. **But** a box or a backup that names a removed version cannot pull it again (R-698's shape, for the controller). **Needs:** find what removed them (a Gitea clean-up rule?), and record the rule — or say it was a one-time act. Read-only on DooPlex. **-- 2026-10-01 (afternoon):** **Answered (read only, `audits/lockouts-2026-10-01/C/C1-registry-read.txt`).** Not a Gitea rule: `package_cleanup_rule` is EMPTY (READ ONLY query on the shared CNPG); `[cron.cleanup_packages]` only runs rules and Gitea's own expired-data clean-up. The cause is a MANUAL run of DooPlex's `~/git/misc-scripts/gitea-image-prune.sh --all --keep 7 --apply --reclaim` on the night of 2026-08-22/23, recorded in homelab-manifests HM-024 (the Gitea volume was full: `felhom-golden` 22 → 3 versions, /data 14.8 → 4.4 G); `--keep 7` per container package explains controller 0.213.0 (2026-08-12) as the oldest left. Nothing schedules it (crontabs, timers, cluster CronJobs read). If run again as its usage text says, it keeps 7 controller releases (about a day) and `--type generic --keep 3` would cut the agent to 3; it protects no vouched version. **Needs:** the operator's word on a written rule (STATUS). **-- 2026-10-01 (late afternoon):** The rule built (`09` §3 decision 62, operator): `admin/misc-scripts` `c9d5ed5` — `gitea-image-prune.sh` keeps the newest 20 + every version in use (floor, vouched golden, vouched agent, `min_agent`, the golden's baked images, the running hub), refuses (exit 3) when that list is unreadable; `tests/test-prune-plan.sh` red-proofed. Live dry-run: protected controller 0.285.0, golden 0.285.0, agent 0.138.0 + 0.131.0, hub 0.126.0, felhom-samba 1.1.0; would delete 70 controller and 8 hub tags — nothing deleted (no `--apply`). `audits/calibre-name-and-prune-2026-10-01/B/` | **CLOSED 2026-10-01 — decision 62, misc-scripts c9d5ed5** |
|
||||
| **R-751** | **[P2] The image clean-up after an app update could crash the whole controller: it re-read the app after a rescan and dereferenced a nil stack when the app was gone.** FOUND 2026-10-01 by the full test suite (controller v0.284.2): `RetainImagesAfterUpdate` runs in a goroutine; `TestR705_TheManualLegRunsByDay` removed its temp dir under it → `panic: invalid memory address` at `image_retention.go:291`. In a box the same happens when an app is removed (or its compose vanishes) between an update's end and the clean-up — a panic in a goroutine ends the process (the agent's supervisor restarts it). Fixed in v0.285.0 the same session: it returns when the app is gone; `TestRetainImagesAfterUpdate_AppGoneDoesNotPanic` seen panicking on the old code; both retention seams are no-ops in the stacks tests (`TestMain`), so no test leaves the goroutine running. `audits/rulings-2026-10-01/B/B1-red-proofs.txt` **-- 2026-10-01:** Delivered: floor 0.285.0 reached both demo boxes in ~6 s (hub `managed floor SERVED … from declared`). | **CLOSED 2026-10-01 — controller v0.285.0, floor 0.285.0** |
|
||||
| **R-752** | **[P3-LOW] Four more catalog apps let a stranger lock the household out with wrong passwords for a known login name — like mealie (R-747).** READ 2026-10-01 in each app's source at its pinned tag (not measured live): **calibre-web-automated v4.0.8** — Flask-Limiter on the login keyed on the lowercased USERNAME, 3/minute and 40/day, checked before the password; the default login is `admin` → up to a day; no env switch (a database setting). **wger 2.7** — django-axes keyed on IP, 10 failures, 30 min, each failure restarts it; behind traefik every client has traefik's IP → everyone is locked out (`AXES_*` env vars exist; `AXES_IPWARE_PROXY_COUNT` 0). **Grafana 13.2.3** — per-account, 5 failures in a sliding 5 minutes; a slow trickle keeps it closed (`GF_SECURITY_*`). **BookStack 26.09.1** — key `email|ip`, 5 tries, 60 s, hard-coded; `APP_PROXIES` empty, so the key is the e-mail alone. gokapi (3 s delay, no lock) and claper (per-IP 10/min, no account lock) cannot. **Needs:** per app, the smallest fix that keeps a guessing guard (calibre-web-automated and wger first — longest and broadest), each proven on 9202 as R-747's was. **-- 2026-10-01 (afternoon):** **Measured on 9202, each through traefik as a stranger with the public name** (`audits/lockouts-2026-10-01/B/`): **wger** — control: 10 wrong on `admin` locked the second member too; FIXED (decision 58, catalog `82fff32`): username, 5 min, database handler — the second member unaffected, admin in again at 7.5 min (each try during a lock restarts it — measured: 8-minute retries kept a 15-minute lock closed 40+ min). **BookStack** — 1.0 min, kept (decision 59). **Grafana** — 5.0 min, kept (decision 60); a trickle did not hold the household out once the burst aged. **calibre-web-automated** — the form locks 3/min (1.2 min measured) and **40/day per name: after 40 wrong tries in 14 min the right password was refused 2 min later still; only an app restart cleared it** (in-memory store); OPDS has its own 3/min per name (`cps/main.py:75`), no daily limit. No knob for the daily length; both fixes have a household cost — operator decision in STATUS. Installed apps: a settings-only change reaches the stack file at the next sync (images equal, ≤15 min) and the running app at the next `compose up -d` — Restart/Start (measured: the env changed only at Restart), an Update, or a backup's restart (`backup.go:972`, read). | **NARROWED — wger fixed, BookStack/Grafana kept; calibre-web waits for the operator; owner: operator (calibre-web), CC** |
|
||||
| **R-752** | **[P3-LOW] Four more catalog apps let a stranger lock the household out with wrong passwords for a known login name — like mealie (R-747).** READ 2026-10-01 in each app's source at its pinned tag (not measured live): **calibre-web-automated v4.0.8** — Flask-Limiter on the login keyed on the lowercased USERNAME, 3/minute and 40/day, checked before the password; the default login is `admin` → up to a day; no env switch (a database setting). **wger 2.7** — django-axes keyed on IP, 10 failures, 30 min, each failure restarts it; behind traefik every client has traefik's IP → everyone is locked out (`AXES_*` env vars exist; `AXES_IPWARE_PROXY_COUNT` 0). **Grafana 13.2.3** — per-account, 5 failures in a sliding 5 minutes; a slow trickle keeps it closed (`GF_SECURITY_*`). **BookStack 26.09.1** — key `email|ip`, 5 tries, 60 s, hard-coded; `APP_PROXIES` empty, so the key is the e-mail alone. gokapi (3 s delay, no lock) and claper (per-IP 10/min, no account lock) cannot. **Needs:** per app, the smallest fix that keeps a guessing guard (calibre-web-automated and wger first — longest and broadest), each proven on 9202 as R-747's was. **-- 2026-10-01 (afternoon):** **Measured on 9202, each through traefik as a stranger with the public name** (`audits/lockouts-2026-10-01/B/`): **wger** — control: 10 wrong on `admin` locked the second member too; FIXED (decision 58, catalog `82fff32`): username, 5 min, database handler — the second member unaffected, admin in again at 7.5 min (each try during a lock restarts it — measured: 8-minute retries kept a 15-minute lock closed 40+ min). **BookStack** — 1.0 min, kept (decision 59). **Grafana** — 5.0 min, kept (decision 60); a trickle did not hold the household out once the burst aged. **calibre-web-automated** — the form locks 3/min (1.2 min measured) and **40/day per name: after 40 wrong tries in 14 min the right password was refused 2 min later still; only an app restart cleared it** (in-memory store); OPDS has its own 3/min per name (`cps/main.py:75`), no daily limit. No knob for the daily length; both fixes have a household cost — operator decision in STATUS. Installed apps: a settings-only change reaches the stack file at the next sync (images equal, ≤15 min) and the running app at the next `compose up -d` — Restart/Start (measured: the env changed only at Restart), an Update, or a backup's restart (`backup.go:972`, read). **-- 2026-10-01 (late afternoon):** calibre-web done by operator ruling `09` §3 decision 61 (catalog `e9f50b5`): a generated `ADMIN_USER` (`secret`, `hex:5`); `after_install` renames `admin` to it and proves it. 9202: 40 wrong tries on `admin`, the household in at once with its own name (form and OPDS). demo-hp renamed by hand; its name is in the operator's credentials file. All four apps answered (wger 58, BookStack 59, Grafana 60, calibre-web 61). An installed calibre-web is given a made-up name by the box — R-757. | **CLOSED 2026-10-01 — decisions 58–61** |
|
||||
| **R-753** | **[P3-LOW] Behind the tunnel every visitor reaches an app with the SAME address — the tunnel container's — so every per-address guard is an "everyone" guard and every app's log is blind.** MEASURED 2026-10-01 (`audits/lockouts-2026-10-01/A/A1-client-address.txt`): on demo-hp through its real tunnel, a request from DooPlex's public address reached traefik as `172.18.0.5` (cloudflared, in the guest on `traefik-public`) and BookStack as `172.18.0.3` (traefik); on 9202 an echo container showed `X-Forwarded-For`/`X-Real-Ip` = the sending container for the tunnel's hop (traefik DROPS the incoming chain — good: a client cannot forge it) and the real address from the LAN; `CF-Connecting-IP` passes untouched and is FORGEABLE from the LAN. **No box-wide fix taken:** trusting cloudflared in traefik passes Cloudflare's appended chain, whose LEFTMOST entry the client writes — every app reading the leftmost address would believe it; cloudflared's address is docker-assigned; a single-address rewrite needs a traefik plugin (a new dependency). Per-app fixes trust no header (R-752). **Needs (operator):** whether to build a safe version (cloudflared on a fixed-address network + traefik trusting only it + per-app proxy counts), or keep "one address" and fix per app. Only ONE outside address was available (DooPlex has no IPv6); a second was not measured. | **OPEN — rank P3-LOW; owner: operator (direction), CC measures** |
|
||||
| **R-754** | **[P3-LOW] `01-topology-and-trust.md` §7 says cloudflared runs on the Proxmox HOST as an agent-managed service; on every box it runs INSIDE the guest as a container the controller renders.** READ 2026-10-01: `felhom-controller` `internal/infra/templates/cloudflared-compose.yml.tmpl` (`container_name: cloudflared`, network `traefik-public`); demo-hp's guest 9201 runs `cloudflared` (ingress `*.enkisfelhom.hu -> https://traefik`); R-505 saw the same in VM 331. A design decision that the build does not follow — the document or the build is wrong, and only the operator decides which (R-370: a design decision is not a defect). | **OPEN — rank P3-LOW; owner: operator (which is right)** |
|
||||
| **R-755** | **[P3-LOW] wger runs Django's DEVELOPMENT server in production: `manage.py runserver`, because the template does not set `WGER_USE_GUNICORN=True`.** MEASURED 2026-10-01 on 9202 (`ps` in the wger container: `python3 manage.py runserver 0.0.0.0:8000`); wger 2.7's `extras/docker/production/entrypoint.sh:81-87` runs gunicorn only with that switch. Django's own documentation says runserver is not for production (one process, not hardened). Not changed this session (a different change from R-752's; needs its own bench + box proof, memory watch included). | **OPEN — rank P3-LOW; owner: CC (catalog)** |
|
||||
| **R-756** | **[P3-LOW] On 9202, "remove with drive data" refuses calibre-web with 409 „…/scratch_hdd/userdata/calibre-web tárhely jelenleg nem elérhető", while the controller container lists that folder.** MEASURED twice on 2026-10-01 (`audits/lockouts-2026-10-01/B/B1…`, `audits/calibre-name-and-prune-2026-10-01/A/A1…`): `POST /api/stacks/calibre-web/remove` with `remove_hdd_data` → 409; `docker exec felhom-controller ls -ld /mnt/felhom-drives/scratch_hdd/userdata/calibre-web` → the directory (dated 2026-09-22). The walk then removed the app keeping the data (R-442's fail-closed answer). Either the drive is not a registered drive on this scratch box (a test-venue artefact) or the resolver reads another path than the one it names. Not measured which. | **OPEN — rank P3-LOW; owner: CC** |
|
||||
| **R-757** | **[P3-LOW] A template that gains a generated `secret` field makes the box INVENT that value for apps already installed — for calibre-web a login name the app never got.** MEASURED 2026-10-01 on demo-hp: 9 minutes after catalog `e9f50b5` (decision 61) synced, the controller logged `InjectMissingFields … injected missing fields: ADMIN_USER` (deploy.go:1337) and the app page's reveal returned a 10-character name that was NOT calibre-web's login (the app had kept its own name; `after_install` runs only after a fresh install). The page did not list the field, but the reveal answers it, and the password field's text now says "the user name above". demo-hp was fixed by renaming the app's user to the box's recorded name (credentials file updated). Any other installed calibre-web gets the same made-up name at its next sync while its login stays `admin` (no other box has one today: N100 none, Tester-2 not registered). **Needs:** InjectMissingFields must not invent a value an app has to have been GIVEN (a field consumed only by `after_install`), or such a field needs an "installed apps: ask" path. `audits/calibre-name-and-prune-2026-10-01/A/A2…, A3…` | **OPEN — rank P3-LOW; owner: CC (controller)** |
|
||||
|
||||
<!-- DUE-CHECKS-BEGIN — machine-readable. Parsed by scripts/due_checks_gate.py.
|
||||
One row per dated check. The R-number must have a row above. Dates are UTC.
|
||||
|
||||
@@ -227,6 +227,15 @@ A golden is only needed when a publish train wants fresh installs current — de
|
||||
**Since 2026-09-13 the cadence is §4.2: weekly, and before any drill or fresh install.**
|
||||
The full 0.188.0 run, with the observables: `documentation/audits/tester-gate-golden-0.188.0-2026-07-31.md`.
|
||||
|
||||
### 4.1a Registry clean-up — a person's act (`09` §3 decision 62)
|
||||
|
||||
When the Gitea volume fills, `~/git/misc-scripts/gitea-image-prune.sh` (repo `admin/misc-scripts`) prunes old versions.
|
||||
**Dry-run first, always** (`./gitea-image-prune.sh --all prune`, then `--type generic --all prune`): it keeps the newest
|
||||
20 of each package plus every version in use — the floor, the vouched golden, the vouched agent and `min_agent` (read
|
||||
from the hub with `HUB_PW`), the images the vouched golden baked (its `bake.log` here), the hub `manifests/hub.yaml`
|
||||
runs — and prints each with its reason. It refuses when that list cannot be read. **`--apply` is a person's act**,
|
||||
after reading the dry-run; nothing schedules it. The August 2026 `--keep 7` run (HM-024) predates the rule.
|
||||
|
||||
### 4.2 Cadence — goldens are WEEKLY and before any install, not per release (operator ruling 2026-09-13)
|
||||
|
||||
**What was measured before the ruling:** 25 goldens in 26 days in August, almost one per release,
|
||||
|
||||
Reference in New Issue
Block a user