Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
6.3 KiB
REPORT — strangers and lockouts (R-752), the one address behind the tunnel (R-753), the registry (R-750) — 2026-10-01 afternoon
Evidence: documentation/audits/lockouts-2026-10-01/ (A, B, C, T, tools).
Architecture read: 01-topology-and-trust.md §5, §7; 09 §3 decisions 45–47, 57; 06 (the tunnel is not described
there). Baselines (live Gitea ~10:55 CEST): controller c1b123c64955, agent d766666ff8cf, felhom.eu a6a9f0b2458e,
catalog 83636352ea10 — all matched. Register 387 rows; highest R-752; last decision 57.
The Part table
| Part | done / not done / changed | why |
|---|---|---|
| Operator note (decision 57 kept) | done — 09 §3 + CONTEXT |
first |
| A — the client address | done — measured; no box-wide fix (R-753) | trusting cloudflared would pass a client-written leftmost address; a single-address rewrite needs a plugin |
| A1 two outside addresses | changed — one (DooPlex 37.191.56.193; no IPv6 here) | the "same address for everyone" result does not depend on a second one |
| A1 demo-hp | done, read only — two GETs of a 404 path, then the logs | — |
| B1 calibre-web | measured; not fixed — operator decision (STATUS) | no knob for the daily lock; both fixes cost the household |
| B2 wger | done — decision 58, catalog 82fff32; control + two fix runs on 9202 |
the first fix (15 min) proved every try during a lock restarts it; changed to 5 min |
| B3 Grafana | done — decision 60: no change (5.0 min measured; trickle measured) | already short |
| B4 BookStack | done — decision 59: no change (1.0 min measured) | already short; APP_PROXIES would not help through the tunnel |
| B installed apps | done — measured on 9202 | see below |
| C — the registry | done, read only — cause found (R-750 answered) | — |
| D — release / golden | not done — not needed | Part A built nothing |
Claims in the brief that turned out wrong (or right)
- "Apps see traefik's address for every client" — half right. The app's TCP peer is traefik, but
X-Forwarded-Forcarries cloudflared's container address through the tunnel (the same for everyone) and the REAL address from the LAN. Apps that read it (calibre-web's ProxyFix,TRUSTED_PROXY_COUNT1) still see one address for every tunnel visitor. - "calibre-web has no env switch" — right for the limiter (a database setting,
config_ratelimiter); it has an envTRUSTED_PROXY_COUNT, irrelevant here (the login limit is keyed on the user name). - "BookStack's 60 s is hard-coded" — right (
ThrottlesLogins.php:825 tries,:901 minute). - "A Gitea cleanup rule removed the old versions" — wrong. No rule exists; a manual prune script did (HM-024).
- R-752's own claims: calibre-web "up to a day" — right (measured: still locked 2 min after the minute window; only a restart cleared it). My own earlier guess that calibre-web's OPDS door had no limit — wrong: 3/minute per name. Grafana "a slow trickle keeps it closed indefinitely" — not as measured: the household got in once the burst aged out, and a success resets the count. wger "everyone at once" — right (measured).
01§7 "cloudflared runs on the host" — the build differs: it runs in the guest (R-754).
Part A — the answer
| path | the app's TCP peer | X-Forwarded-For / X-Real-Ip | the real client is in | forgeable? |
|---|---|---|---|---|
| tunnel | traefik | cloudflared's container — same for every visitor | CF-Connecting-IP only |
XFF no (traefik drops it); CF-Connecting-IP not through the tunnel, yes from the LAN |
| LAN | traefik | the real LAN address | XFF / X-Real-Ip | no |
Part B — per app (9202, the public name, a stranger through traefik)
| app | setting (pinned tag) | measured before | fix | after |
|---|---|---|---|---|
| wger 2.7 | settings/main.py:268-272 (AXES_* env), settings_global.py:485 reset-on-failure True |
10 wrong → the second member locked too | username, 5 min, DB handler (decision 58) | other member fine; admin in at 7.5 min with one retry; wrong still refused |
| BookStack 26.09.1 | ThrottlesLogins.php:66,82,90 |
locked 1.0 min | none (59) | — |
| Grafana 13.2.3 | login_attempt.go:14,65-85, defaults.ini:498-507 |
locked 5.0 min; trickle: in after the burst aged | none (60) | — |
| calibre-web-automated v4.0.8 | cps/web.py:2218-2219 (3/min, 40/day per name), cps/main.py:75 (OPDS 3/min) |
form 1.2 min; 40 wrong in 14 min → refused 2+ min later; restart cleared | operator | — |
What an installed app gets, and when (measured with wger): a settings-only change reaches the app's stack file at the
next catalog sync (when its images equal the catalog's; ≤ 15 min); the RUNNING app keeps the old value until the next
compose up -d — the app page's Restart or Start (measured: the env changed exactly at Restart), an Update, or a
backup's restart of the app (backup.go:972, read, not measured). An app pinned to an older version than the catalog
gets nothing until its Update (the frozen render, 09 §5.4).
Part C — the registry (read only)
package_cleanup_rule empty; Gitea logs only to the console and the pod started 2026-08-23, so August logs are gone. The
cause is recorded in homelab-manifests HM-024: gitea-image-prune.sh --all --keep 7 --apply --reclaim the night of
2026-08-22/23 (the Gitea volume was full). Nothing schedules it. STATUS carries the decision (keep / a written rule, pick: a rule).
Rows
387 → 390. Opened R-753 (one address behind the tunnel), R-754 (01 §7 vs the build), R-755 (wger on runserver).
Narrowed R-752. Answered R-750 (waiting on the operator). Closed none.
Teardown
- Machine: 9202 back on the live catalog (
repo_urlread back), the same six containers as at the start; the apps this session installed (bookstack, grafana, calibre-web, wger ×5) removed through the product — calibre-web's drive data kept because the remove refused the drive path (R-442's fail-closed rule; its folder predates today); the echo container and both probe images removed. Drill catalog reset to live (82fff32). - Host: demo-hp untouched except two read-only GETs through its tunnel and log reads;
pct listunchanged. - Hub: nothing. Gitea / DooPlex: read only (one READ ONLY database transaction, config and log reads).