Spring framework strategy — leftmost XFF into its sign-in audit record. Once traefik trusts the tunnel's fixed address (controller v0.286.0), the leftmost entry is what a
stranger writes; with the chain removed the app reads traefik's X-Real-Ip or its peer, as before — never forgeable.
Measured on 9202: a router with this middleware receives no X-Forwarded-For (felhom.eu audits/visitors-2026-10-01/A/P1).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
{'komga': 'gotson/komga:1.27.1'} -> {'komga': 'gotson/komga:1.28.0'}
The ONLY image move in this commit. Written by upgrade-test.py --write-ladder (catalog gates rc=0):
- bench LXC 9401 (harness v4): the seed read back before and after; 10-minute memory watch:
komga anon 64.5 % (cgroup 67.5 %); 0 kills, 0 restarts; the abort starts and serves the data;
- box 9202 (controller 0.283.1, the product's guarded Update, drill catalog): done in 44.2 s, the seed
read back through the app's own front door.
Evidence: felhom.eu/documentation/audits/pg-last-six-2026-09-30/F/ and .../box/komga/
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
Bench at 512M: proven but memory_tight (its own memory 94.6 %); per the
bar the limit is raised in the same commit and the watch re-run once:
at 768M 60 %, 0 kills, 11 940 requests all 200; abort starts-and-serves.
Box (9202, guarded Update, at 512M): done, read back, R-626 clean.
The komga fixture was fixed tonight (users/me lives at /api/v2).
09 decision 21.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
BusyBox wget (+ node/python/curl one-shots, incl mealie's socket tuple) resolve
localhost -> IPv6 ::1 with no cross-family fallback; an IPv4-only-binding app
reads docker-unhealthy while serving (vaultwarden, re-run 2026-07-06). Escalates
that instance to the class. Scoped strictly to healthcheck test: lines
(diff-reviewed: no env/config/label changed; .felhom.yml already clean). New
REUSE.md convention row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
The /api/v1 prefix is auth-gated → old HC got 401 → curl -f failed → container
reported unhealthy while serving fine. Diagnosed live on guest 9201.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Address security review: reverting to root removed the user:1000 boundary, so add
security_opt no-new-privileges:true to block SUID-based escalation. Full cap_drop
is intentionally NOT applied — the images' root-init needs CHOWN/SETUID/SETGID to
set up /config and /metadata (dropping them reproduces the crash we hit at user:1000).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Live try-then-fallback: pinning user 1000:1000 crash-loops both — their named
config/metadata volumes are Docker-created root-owned and the pinned process can't
write them (komga: SQLite /config open fails; audiobookshelf: EACCES mkdir
/metadata/logs; neither has a PUID-style root-init chown). Reverted to root; they
rely on the setgid 2775 userdata dirs (files land group 1000 → FileBrowser browses/reads).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
All 51 docker-compose.yml: replaced hardcoded subdomain.${DOMAIN}
with ${SUBDOMAIN}.${DOMAIN} in Traefik labels, app env vars, and
comments.
All 51 .felhom.yml: added SUBDOMAIN deploy field (type: subdomain)
with default matching existing subdomain metadata value.
Works with felhom-controller v0.27.0 which validates and stores the
user-chosen subdomain in app.yaml. Existing deployed apps get
SUBDOMAIN auto-injected via InjectMissingFields() on next sync.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>