42bf76f540
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
176 lines
15 KiB
Markdown
176 lines
15 KiB
Markdown
# Changelog
|
||
|
||
## 2026-06-29 — App-email rollout: gitea + rallly (calcom/nextcloud/immich = findings)
|
||
- **gitea 1.23.4** — added `smtp_mapping` (STARTTLS via `GITEA__mailer__PROTOCOL=smtp+starttls` +
|
||
`FORCE_TRUST_SERVER_CERT=true` to trust the shim's self-signed cert; single `GITEA__mailer__FROM`). Compose
|
||
references the injected `GITEA__mailer__*` keys; env applied every boot.
|
||
- **rallly** — added `smtp_mapping` (Nodemailer STARTTLS, `SMTP_SECURE=false` + `SMTP_REJECT_UNAUTHORIZED=false`
|
||
to accept the self-signed cert; single `NOREPLY_EMAIL`). **Also fixed three pre-existing template bugs** that
|
||
made rallly undeployable (never caught because the bad pin never ran): (1) image pin `3.12.1` doesn't exist →
|
||
`3.11.2`; (2) healthcheck used `wget`, absent from the rallly image (exit 127) → container unhealthy →
|
||
**Traefik wouldn't route it** → replaced with a Node http check; (3) added required `SUPPORT_EMAIL` + a valid
|
||
`NOREPLY_EMAIL` default (rallly refuses to boot without them).
|
||
- **Both gitea and rallly send-tested live** end-to-end (app → shim → hub → Resend): gitea password-reset
|
||
(From `gitea@felhom.eu`) and rallly registration code (From `rallly@felhom.eu`) both delivered.
|
||
- **NOT wired — reported as findings** (`felhom.eu/documentation/audits/FINDING-app-email-rollout-2026-06-29.md`):
|
||
- **cal.com v4.8.7** — hard-codes TLS `rejectUnauthorized:true` with no override; opportunistic STARTTLS
|
||
against the self-signed shim fails. Needs a non-STARTTLS-advertising plaintext listener (mechanism change).
|
||
- **nextcloud 31** — no cert-skip env (same opportunistic-STARTTLS gap) **and** a split From
|
||
(`MAIL_FROM_ADDRESS`+`MAIL_DOMAIN`) the single-`from_var` mapping can't express.
|
||
- **immich v2.5.5** — no SMTP env vars at all; config is admin-UI/DB or an `IMMICH_CONFIG_FILE` JSON. Does not
|
||
fit env-injection; left for a future config-file-injection mechanism (or manual admin-UI setup).
|
||
|
||
## 2026-06-29 — App-email: smtp_mapping for Vaultwarden + Mealie
|
||
- Added the `smtp_mapping` block to `templates/vaultwarden/.felhom.yml` and `templates/mealie/.felhom.yml`,
|
||
enabling managed outbound email (app → in-controller shim → hub → Resend) for the two spike-proven apps
|
||
(`SPIKE-smtp-app-relay-2026-06-28`). The controller injects `SMTP_*` at deploy/redeploy when app-email is
|
||
on (global + per-app); the From address is `<app>@felhom.eu`. SMTP auth creds are intentionally left unset
|
||
(the shim accepts no-auth on the Docker network).
|
||
- **Vaultwarden:** STARTTLS (`SMTP_SECURITY=starttls`) + `SMTP_ACCEPT_INVALID_CERTS/HOSTNAMES=true` to
|
||
accept the shim's self-signed cert.
|
||
- **Mealie:** plaintext (`SMTP_AUTH_STRATEGY=NONE`) on :2525 — Mealie has no accept-invalid-cert option, so
|
||
STARTTLS to a self-signed shim would fail; plaintext to the Docker-network-only shim is the spike-validated
|
||
mode.
|
||
- Both `docker-compose.yml` files now reference the injected `${SMTP_*}` keys (with harmless defaults) so the
|
||
values reach the container; empty `SMTP_HOST` keeps mail disabled when the toggle is off.
|
||
- Documented the `smtp_mapping` pattern in `README.md` so further apps are easy adds.
|
||
|
||
## 2026-06-28 — Add SparkyFitness (v0.17.2) — nutrition/workout tracker
|
||
- New app `templates/sparkyfitness/{docker-compose.yml,.felhom.yml}`: a self-hosted nutrition/calorie +
|
||
workout/weight tracker (alternative to wger). Three containers — nginx **frontend** (SPA :80, the sole
|
||
Traefik ingress, proxies `/api`+`/uploads` internally) + Node **server** (:3010) + dedicated
|
||
**postgres:15-alpine**. Server + DB stay on the internal network with no Traefik labels.
|
||
- **Native email/password auth** (no OIDC/Authentik — that's DooPlex-specific); subdomain `sparky`
|
||
(deliberately ≠ wger's `fitness` to avoid a Host() collision). `pi_compatible: false`, `needs_hdd: false`.
|
||
- **Two DB roles**: `sparky` (POSTGRES superuser, runs init/migrations) + `sparkyapp` (limited app role the
|
||
server auto-creates on first boot) — separate `DB_PASSWORD`/`APP_DB_PASSWORD`. `PGDATA` in a `pgdata`
|
||
subdir of the named volume. Four auto-generated, `locked_after_deploy` secrets; `API_ENCRYPTION_KEY` +
|
||
`BETTER_AUTH_SECRET` carry `data_key: true` (restore recovers, never regenerates — both are 64-char hex).
|
||
- Transcribed from the validated k3s manifest `homelab-manifests/workout-system/sparkyfitness.yaml`
|
||
(pinned image tags, two-DB-role model, never-change crypto keys, `/api/health`, pg15 + PGDATA subdir).
|
||
- **Image-probe findings (build server, v0.17.2):** server keeps the `node -e` `/api/health` probe (node
|
||
present); frontend keeps the `wget --spider` probe (both `wget` and `curl` present). No probe changes needed.
|
||
- **Live-validated on guest 9201 (controller v0.87.0):** synced via "Sablonok frissítése"; deployed through
|
||
the real dashboard flow (Domain auto, Subdomain `sparky`, 4 secrets auto-gen). All 3 containers healthy;
|
||
server log shows clean migrations + `sparkyapp` role created + RLS applied, no crash loop, no uploads
|
||
EACCES; `GET /api/health` through the public edge returns `{"status":"UP"}`; login/register page serves
|
||
over a valid TLS cert at `https://sparky.demo-felhom.eu`.
|
||
|
||
## 2026-06-26 — crafty-controller: image bump 4.4.8→4.10.7 + publish Java port range + connection guidance
|
||
- **Image bump** `crafty-4:4.4.8` → `4.10.7` (latest stable; 4.10.8/4.11.0 don't exist in the registry).
|
||
6 minor versions of fixes incl. security CVEs. **Java 25 verified present** in 4.10.7
|
||
(`/usr/lib/jvm/java-25-openjdk-amd64`, default `java -version` = openjdk 25.0.3; 8/11/17/21 also
|
||
available) — so the latest-Minecraft (`26.x`, needs Java 25) blocker is resolved. Healthcheck + Traefik
|
||
https-backend labels unchanged (Crafty still serves HTTPS on 8443).
|
||
- **Published the Java game-port range** `25565-25575:25565-25575` (TCP, 11 ports = up to 11 Java
|
||
servers; first server 25565, rest 25566–25575). No `network_mode: host` (would break Traefik routing).
|
||
Bedrock UDP 19132 intentionally out of scope.
|
||
- **App-page guidance** (`.felhom.yml` first_steps + prerequisites): how to set the server port within
|
||
25565–25575, how to connect on the LAN (manual IP:port — "scan for LAN" won't auto-list), and that
|
||
internet access needs operator port-forwarding. (Static text — can't show the live LAN IP.)
|
||
- **Live-verified on guest 9201:** 4.10.7 healthy; public URL 302; the guest's bridged LAN IP
|
||
`192.168.0.121` reaches the real Crafty "test" server on `25565` (TCP OPEN + Minecraft SLP handshake
|
||
returns JSON status); `:25575` reachable, `:25600` closed (negative control). In-place upgrade preserved
|
||
the admin, the operator's configured MFA, and the test server.
|
||
- **Correction (earlier draft was wrong):** an earlier note here claimed the upgrade "locked out the
|
||
admin (TOTP)." That was a misdiagnosis — the `totp_data` row + recovery codes were **operator-configured
|
||
MFA**, so the 401 on a password-only login was correct behaviour, NOT an upgrade bug. There is **no
|
||
upgrade regression**; the bump preserves data and MFA correctly.
|
||
|
||
## 2026-06-26 — crafty-controller: seed a felhom-generated admin password (replaces Crafty's ugly random one)
|
||
- **crafty-controller**: instead of reading Crafty's auto-generated (long, symbol-laden) random admin
|
||
password, we now **seed** a clean felhom-generated one — same pattern as gokapi, so initial passwords are
|
||
consistent across the catalog.
|
||
- Crafty's image ships `app/config_original/default.json = {"username":"admin","password":"crafty"}`;
|
||
"crafty" is 6 chars < Crafty's 8-char minimum, so Crafty rejected it and generated a random password.
|
||
- New `CRAFTY_PASSWORD` deploy field (`type: password`, `generate: password:24`, locked after deploy —
|
||
mirrors gokapi's `GOKAPI_PASSWORD`). The compose **entrypoint** overwrites the `default.json` template
|
||
with this password before the launcher runs; on fresh install Crafty creates the `admin` user with it.
|
||
- `initial_credentials.file` repointed `default-creds.txt` → `default.json` (same json/username/password
|
||
keys), so the controller's app-page "Kezdeti belépési adatok" card shows the **seeded** password — the
|
||
customer sees the same value at deploy time and on the app page.
|
||
- Catalog-only change (reuses felhom-controller v0.84.0's initial_credentials reader + the gokapi-style
|
||
seed). Requires a fresh install to take effect (the seed is only read on first run).
|
||
|
||
## 2026-06-26 — crafty-controller: surface the auto-generated initial admin password on the app page
|
||
- **crafty-controller**: Crafty writes a random admin password to `/crafty/app/config/default-creds.txt`
|
||
at first boot (its built-in default is rejected as "too short"). Customers had to read the container
|
||
logs to find it. Added an `initial_credentials` block (new general felhom-controller v0.84.0 mechanism):
|
||
`file` + `format: json` + `username_key`/`password_key` + a `note`. The controller reads the file live
|
||
from the container and shows username + password (masked, reveal/copy) on the app's page under "Kezdeti
|
||
belépési adatok". Requires felhom-controller ≥ v0.84.0.
|
||
- Updated `first_steps` to point at the app page for the initial login instead of "find it in the logs".
|
||
|
||
## 2026-06-26 — crafty-controller: Traefik https backend + scoped skip-verify (fixes 502)
|
||
- **crafty-controller**: the healthcheck fix un-withheld the Traefik route, exposing a pre-existing
|
||
**502** — Traefik proxied `http://…:8443` to Crafty's **HTTPS-only** self-signed backend (Crafty serves
|
||
no plain-HTTP panel; `:8000` only redirects). Added two service labels:
|
||
- `loadbalancer.server.scheme=https` — Traefik now speaks HTTPS to the backend.
|
||
- `loadbalancer.serverstransport=insecure-skip-verify@file` — references the **named**
|
||
serversTransport defined in the controller-managed Traefik dynamic config (felhom-controller v0.83.0),
|
||
which skips verifying Crafty's per-container self-signed cert. Verification stays ON for every other
|
||
backend (scoped Option B; no global `insecureSkipVerify`). The `@file` suffix is the cross-provider
|
||
reference from the docker provider to the file-provider transport.
|
||
- Requires felhom-controller ≥ v0.83.0 (which renders the `insecure-skip-verify` transport). `port=8443`
|
||
and the router/tls labels are unchanged.
|
||
|
||
## 2026-06-26 — crafty-controller healthcheck fix (curl-absent + http-vs-TLS probe)
|
||
- **crafty-controller**: container was permanently `unhealthy` → route withheld (`routeUnpublished`).
|
||
Two independent healthcheck root causes, both fixed in one change:
|
||
- **Docker healthcheck** ran `curl -fk https://localhost:8443`, but the `crafty-4:4.4.8` image has
|
||
**no `curl` and no `wget`** (`exec: "curl": not found`, FailingStreak 150). Replaced with a
|
||
dependency-free **python3 TLS-socket** liveness probe (`/usr/bin/python3` is present): completes a
|
||
TLS handshake to `127.0.0.1:8443` (unverified context mirrors the old `-k`; Crafty's cert is
|
||
self-signed). `start_period` 30s → 60s for cold-boot headroom (cert gen + migrations).
|
||
- **Controller-side probe** (`.felhom.yml healthcheck.checks`) was `type: http` against Crafty's
|
||
**TLS-only** 8443 → `probeHTTP` sent plaintext HTTP, got a TLS record → `HealthProbe.Healthy=false`,
|
||
which `manager.go` re-applies to override Docker's verdict back to `unhealthy`. Changed `http` →
|
||
`tcp` (`probeTCP` dial succeeds against a TLS listener). Both layers had to change together.
|
||
- Live-validated on guest 9201 (`demo-felhom`): synced → recreated via the update path → Docker
|
||
`State.Health: healthy` (ExitCode 0), `health_probe.healthy: true` (tcp :8443, 5ms), http-vs-TLS
|
||
WARNs stopped, stable green 3+ min, Traefik now **publishes** the route (`crafty-controller@docker`).
|
||
- **Known follow-up (separate, out of this fix's scope):** the public URL still returns **502** — a
|
||
distinct pre-existing bug the un-withheld route exposed: Traefik proxies `http://…:8443` to Crafty's
|
||
HTTPS-only backend. Needs a Traefik HTTPS-backend + self-signed `serversTransport`
|
||
(`insecureSkipVerify`) in the controller-generated Traefik config — tracked separately.
|
||
|
||
## 2026-06-23 — gokapi: index redirect + admin username display
|
||
- **gokapi**: seed `RedirectUrl` repointed from Gokapi's GitHub default → `https://${SUBDOMAIN}.${DOMAIN}/admin`.
|
||
Gokapi's bare root `/` redirects to `RedirectUrl`; the controller's "Megnyitás" link is always the bare
|
||
subdomain root, so it was landing on Gokapi's GitHub instead of the app. Now `/` → `/admin` → login.
|
||
Applied to the live demo (config.json edit + restart) and the seed (future deploys).
|
||
- **gokapi**: added `app_info.default_creds` ("Felhasználó: admin · jelszó a Beállítások oldalon") so the
|
||
app-info page shows the initial admin user like other apps; fixed `first_steps` (no more setup wizard).
|
||
|
||
## 2026-06-23 — gokapi reproducible headless setup (fixes public "maintenance mode")
|
||
- **gokapi**: was stuck in "maintenance mode" on the public URL since first deploy — Gokapi's one-time
|
||
`/setup` wizard was never completed, and (verified against the docs + the v1.9.6 binary) **no Gokapi
|
||
version supports env-var headless setup** for admin credentials. Worse, the unconfigured `/setup` was
|
||
publicly reachable = an unauthenticated admin-takeover window.
|
||
- Fix: the compose `entrypoint` now seeds a `config.json` on first boot (admin user, this app's public
|
||
URL `https://${SUBDOMAIN}.${DOMAIN}/`, local storage, Encryption Level 0 so it restarts without a
|
||
prompt) with `Password`/`SaltAdmin`/`SaltFiles` cleared, then runs Gokapi's documented
|
||
`--deployment-password` one-shot to set the **felhom-generated** admin password **before** the
|
||
server starts serving. The admin account is claimed at first boot → `/setup` is never exposed.
|
||
- `.felhom.yml`: new `GOKAPI_PASSWORD` deploy field (`type: password`, `generate: password:24`,
|
||
shown to the customer, locked after deploy). Admin username is `admin`.
|
||
- Seed is pinned to Gokapi **v1.9.6** (`ConfigVersion 21`) — re-capture the seed if the image is bumped.
|
||
- Live-validated on guest 9201: fresh remove+redeploy → headless auto-config, public login works, no
|
||
maintenance page, admin claimed at first boot (browser-verified login).
|
||
|
||
## 2026-06-22 — gitea healthcheck fix (unattended test campaign)
|
||
- **gitea**: healthcheck probe repointed `/api/v1/version` → `/api/healthz` (docker HC + controller
|
||
`.felhom.yml` probe), `start_period` 30s → 90s.
|
||
- Surfaced during the Phase-2 deploy sweep: a fresh gitea reported `unhealthy` because
|
||
`/api/v1/version` returns 404 until the install wizard / INSTALL_LOCK completes, while the
|
||
container was serving fine on :3000 (`/api/healthz` → 200). Same class as the komga fix.
|
||
|
||
## 2026-06-22 — komga healthcheck fix (unattended test campaign)
|
||
- **komga**: healthcheck probe repointed `/api/v1/actuator/health` → `/actuator/health`.
|
||
- Root cause: komga's Spring Boot actuator endpoint is served unauthenticated at `/actuator/health`
|
||
(HTTP 200), while everything under the `/api/v1` prefix is auth-gated — so the old probe got
|
||
HTTP 401, `curl -f` exited 22, and the container reported `unhealthy` despite serving normally
|
||
on :25600. Diagnosed live on guest 9201 (probe matrix: `/`, `/actuator/health`,
|
||
`/api/v1/oauth2/providers`, `/login` all 200; `/api/v1/actuator/health` → 401).
|
||
- The `gotson/komga:1.20.0` image ships `curl` (verified), so the probe tool is unchanged.
|