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

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:
2026-10-01 13:29:46 +02:00
parent 11a673d597
commit b63654a299
15 changed files with 481 additions and 36 deletions
+6
View File
@@ -16,6 +16,12 @@
> and holds nothing of its own; this file does hold its own content, namely the standing rulings below. > and holds nothing of its own; this file does hold its own content, namely the standing rulings below.
> **2026-10-01 (late afternoon) — 61 and 62 built.** calibre-web `ADMIN_USER` (catalog `e9f50b5`; after_install renames
> `admin` in app.db and proves it); demo-hp renamed by hand (name in the operator's credentials file). Registry prune rule in
> `admin/misc-scripts` `c9d5ed5` (keep 20 + in use; refuses when unreadable; dry-run only). R-750, R-752 closed; R-756
> (9202 remove-with-data 409), R-757 (InjectMissingFields invents a secret for installed apps) opened.
> Report: `REPORT-calibre-name-and-prune-2026-10-01.md`.
> **Rulings 2026-10-01 (late afternoon, operator) — `09` §3 decisions 61, 62, recorded before the work.** **61 (R-752 A):** > **Rulings 2026-10-01 (late afternoon, operator) — `09` §3 decisions 61, 62, recorded before the work.** **61 (R-752 A):**
> calibre-web's admin login name is generated at install and shown on the app page; the lock stays. **62 (R-750 A):** the > calibre-web's admin login name is generated at install and shown on the app page; the lock stays. **62 (R-750 A):** the
> registry prune keeps the newest 20 per image plus every version the vouched golden, the floor, `agent_version` and > registry prune keeps the newest 20 per image plus every version the vouched golden, the floor, `agent_version` and
@@ -0,0 +1,74 @@
# REPORT — rulings 61 (calibre-web's generated login name) and 62 (the registry prune rule) — 2026-10-01 late afternoon
Evidence: `documentation/audits/calibre-name-and-prune-2026-10-01/` (A, B, T); tools in `audits/lockouts-2026-10-01/tools/`
(`a_calibre_name.py`, `lk.py`, `walk.py`, `repoint.py`).
Read: `09` §3 decisions 45, 57–60; `FIRST-ADMIN.md`; rows R-752, R-750, R-753; `audits/lockouts-2026-10-01/` B1, C1;
homelab-manifests HM-024. Baselines (~12:55 CEST): controller `c1b123c64955`, felhom.eu `8dab40c7a786`, catalog
`ed6df4b46b93` — matched. Register 390; highest R-755; last decision 60 → the rulings are **61 and 62**.
## The Part table
| Part | done / not done / changed | why |
|---|---|---|
| Rulings 61, 62 | **done** — recorded first (`09` §3, CONTEXT) | numbered 61/62: 58–60 were taken by the lockouts session |
| **A1 measure** | **done** — `hex:N` + `type: secret` already exist; calibre-web has no rename command | — |
| **A2 build** | **done** — catalog `e9f50b5` (template, hu + en copy, the freeze for those 5 strings, FIRST-ADMIN) | no controller change |
| **A3 proof on 9202** | **done** | — |
| **A4 installed apps** | **done — and a defect found** (R-757); demo-hp renamed TWICE | the box invented a name for the installed app |
| **B prune rule** | **done** — `admin/misc-scripts` `c9d5ed5`; test red-proofed; live dry-run | the running hub added to "in use" (a version in use the ruling did not name) |
| B4 runbook line | **done** — `RUNBOOK-manual-build.md` §4.1a | HM-024 lives in homelab-manifests (outside the felhom fence) |
| **C release / golden** | **not needed** | A1 needed no controller change |
## Claims in the brief that turned out wrong (or right)
1. **"The controller can generate a login name"** — right: `generate: "hex:N"` (deploy.go:1187) gives lowercase a–f and
digits; `type: secret` is filled when empty and shown behind „Megjelenítés".
2. **"calibre-web can rename a user"** — **no command does**: `cps/cli.py` offers only `-s user:password`
(`ub.py:1350 password_change`). Its admin page renames by setting `user.name` (`admin.py:2789`, column `ub.py:264`,
unique). So `after_install` updates that column itself, then uses Calibre-Web's own `-s` for the password.
3. **"The OPDS door uses the same name"** — right: OPDS is limited per name (`cps/main.py:75`, `request_username`); a
stranger's tries on `admin` never touch the real name (measured: OPDS with the real name ok after 40 tries on `admin`).
4. **Where the prune script lives** — in a repo already: Gitea `admin/misc-scripts` (`~/git/misc-scripts`). The August
run is in its own log: `2026-08-22T16:02:20Z RUN action=prune … apply=true keep='7'`.
5. **"A template change reaches an installed calibre-web only through an Update"** — wrong in a way that matters: the
template reached demo-hp at the next sync (images equal), and the box then INVENTED the new field's value
(`InjectMissingFields`, R-757). My own first CHANGELOG line said "frozen until an Update" — also wrong.
## Part A — calibre-web
**9202 (drill catalog `4e18b3a`, identical to live `e9f50b5`)** — `A/A1-9202-calibre-generated-name.txt`:
install hold before the first start, opened by `after_install` at 11:02:17; a stranger polling `admin/admin123` from the
deploy press got in **0 of 31** times; `after_install` record `ok: true`; the name 10 lowercase hex characters (read
through the page's reveal); app.db: 2 users, 0 named `admin`; name + password: form ok, OPDS ok; `admin` + the right
password refused; **40 wrong tries on `admin` at 3/min (11:02–11:16) → the household at once: form ok, OPDS ok**; a wrong
password on the real name refused. Removed (drive data kept: R-756).
**demo-hp** — `A/A2-demo-hp-rename.txt`: renamed by the same method (values through stdin, never printed); a real login
over its traefik: name ok (form, OPDS), `admin` wrong. Then the box's sync injected a DIFFERENT `ADMIN_USER` into its
app.yaml (R-757, `A/A3…`); renamed again to the box's recorded value; verified (the earlier name and `admin` refused).
**The name is in `~/.config/credentials` as `DEMO_HP_CALIBRE_USER`** (backup `credentials.bak-20261001-calibre`); never in a repo.
**What any other installed calibre-web gets, and when:** at the next catalog sync (≤ 15 min) its `.felhom.yml` gains the
field and the box invents an `ADMIN_USER` for it; its login stays `admin` (after_install runs only after a fresh install).
No other box has calibre-web today (the N100 does not; Tester-2 has not registered).
## Part B — the prune rule
`tests/test-prune-plan.sh`: 7 checks pass (an in-use version older than the newest 20 is kept, with its reason; `--keep`
defaults to 20; dry-run; an unreadable in-use list → exit 3). Red-proofs: the same plan with an empty in-use list deletes
0.262.0; the in-use check removed from `is_protected` → 3 checks fail (`B/B1-test-and-red-proof.txt`).
Live dry-run (`B/B2-live-dry-run.txt`): in use — controller 0.285.0 (floor, golden's, baked), golden 0.285.0, agent
0.138.0 and 0.131.0, hub 0.126.0, felhom-samba 1.1.0. Would delete: felhom-controller 70, felhom-hub 8; every other
package nothing. **No `--apply`.** No token or password in any output (grepped for each value).
## Rows
**390 → 392.** Closed R-750, R-752. Opened R-756 (9202 remove-with-data refused), R-757 (the box invents a new secret
field's value for installed apps).
## Teardown
- **Machine:** 9202 back on the live catalog (`repo_url` read back), the same six containers; calibre-web removed through
the product (drive data kept, R-756). demo-hp: calibre-web's user renamed (the only change there).
- **Host:** nothing. **Hub:** read only (the Configuration page, for the dry-run). **Gitea:** read only; one repo push
(`misc-scripts`). Drill catalog reset to live (`e9f50b5`).
+17 -34
View File
@@ -2,52 +2,35 @@
**Ready for the first real tester (Tester-2): yes. You confirmed the tunnel route and the connect mails (2026-09-30).** **Ready for the first real tester (Tester-2): yes. You confirmed the tunnel route and the connect mails (2026-09-30).**
**Updated 2026-10-01 (afternoon). Both demo boxes run controller 0.285.0 and host agent 0.138.0. Hub 0.126.0. New installs get golden 0.285.0 with agent 0.138.0.** **Updated 2026-10-01 (late afternoon). Both demo boxes run controller 0.285.0 and host agent 0.138.0. Hub 0.126.0. New installs get golden 0.285.0 with agent 0.138.0.**
**Tester-2 — read only, from the hub.** The customer record exists. Tester-2's box has not registered yet. **Tester-2 — read only, from the hub.** The customer record exists. Tester-2's box has not registered yet.
## Decisions I took myself (you may reverse them) ## Your two decisions of this afternoon — built
- **wger: a stranger now locks only the name they try, for 5 minutes.** Before, ten wrong tries locked out every - **calibre-web gets a secret login name (your A).** The box makes the name at install and shows it on the app page,
household member for 30 minutes. Tested: the other member could still log in; the targeted one was back in after next to the password. Tested on the scratch box: a stranger tried 40 wrong passwords with `admin`, and the household
7.5 minutes. Short on purpose: in wger, every try during a lock restarts it, also the household's own. still logged in at once with its own name. The lock against guessing stays.
- **BookStack and Grafana: no change.** Their locks are already short: 1 minute and 5 minutes, measured. **The HP demo box's calibre-web has a new name too.** It is in your credentials file, under the same key as before.
- **mealie stays on the 1-hour lock**, as you said. - **The registry clean-up script keeps 20 versions and never one in use (your A).** "In use" means: the golden, the
floor, the approved agent, the running hub. I ran it only in "show me" mode: today it would delete 70 old controller
versions and 8 hub versions. **It deleted nothing.** If it cannot read what is in use, it refuses to run.
## What I measured ## What broke, and what I did
- **Behind the tunnel, every visitor looks like the same address** to an app. So an app that locks "by address" locks - **A box makes up a value when a template gains a new generated field.** On the HP demo box it made up a calibre-web
out everyone at once (that was wger). Apps cannot see who is who. I did not change this box-wide: the easy way would login name that the app never had. I fixed that box by giving the app that name. Written down. No other box has calibre-web today.
let a stranger fake an address. Written down. - On the scratch box, removing calibre-web "with its data" was refused, although the folder is there. Written down.
- **Old controller versions in the registry:** no automatic rule removed them. Someone ran a clean-up script by hand
on 22–23 August, when the registry disk was full. Nothing runs it on a schedule.
- **An installed app takes a new setting** at its next restart, update or night backup — not by itself.
## What else I found **Rows.** This afternoon: 2 closed, 2 opened. The list went from 390 to 392 rows.
- wger runs a development web server, not a production one. Written down.
- The design document says the tunnel runs on the host. It runs inside the box. One of the two is wrong. Written down.
**Rows.** Today (afternoon): 0 closed, 2 narrowed or answered, 3 opened. The list went from 387 to 390 rows.
## What needs you ## What needs you
1. **calibre-web: a stranger can lock the household out for a day** (40 wrong tries in 14 minutes). There is no setting 1. **Nothing urgent.** Two questions from earlier stay open, and nothing breaks if you wait: should apps see each
for a shorter lock. Two ways: visitor's real address (a bigger build), and is the design document or the build right about where the tunnel runs?
- **A — a secret login name** (recommended): the box makes one at install and shows it on the app page next to
the password. Strangers cannot aim at it. Cost: the household types a strange name.
- **B — switch the login limit off:** no lock at all. Cost: no limit on password guessing in the login form.
If you do nothing: a stranger who knows the name `admin` can keep the household out for a day.
2. **The registry clean-up script:** today it keeps only the newest 7 versions, about one day of controller releases,
and it does not protect the versions in use. Two ways:
- **A — a written rule** (recommended): keep the newest 20 releases, plus every version a golden, the floor or the
vouched agent names. Change the script to match.
- **B — keep it as it is:** run by hand when the disk fills.
If you do nothing: the next manual run can delete a version a box or a backup still needs.
3. **Smaller:** should apps see each visitor's real address (a bigger build), or do we keep fixing per app? And which is
right for the tunnel: the design document (on the host) or the build (inside the box)? Nothing breaks if you wait.
## Standing steps ## Standing steps
- **Monthly security re-test: last run 2026-10-01, next due ~2026-11-01.** (You start it with the standing brief.) - **Monthly security re-test: last run 2026-10-01, next due ~2026-11-01.** (You start it with the standing brief.)
- **Weekly:** the golden bake (around 8 October). - **Weekly:** the golden bake (around 8 October).
- **Registry clean-up:** only when the registry disk fills; "show me" mode first, then a person decides.
@@ -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 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 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. (`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 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 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 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 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. 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) ### 2026-10-01 (afternoon) — decided by CC unattended, operator may reverse (R-752)
@@ -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
@@ -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"))
+4 -2
View File
@@ -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-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-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-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-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-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-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-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. <!-- 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. 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.** **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`. 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) ### 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, **What was measured before the ruling:** 25 goldens in 26 days in August, almost one per release,