docs: v0.84.0 REPORT/CONTEXT/README — catalog-driven initial-login extraction
This commit is contained in:
+13
-1
@@ -7,7 +7,19 @@
|
||||
>
|
||||
> Ask Claude Code: "Please update CONTEXT.md with what we did today"
|
||||
|
||||
Last updated: 2026-06-26 (v0.83.0 — Traefik scoped serversTransport for self-signed HTTPS backends; crafty 502 fix)
|
||||
Last updated: 2026-06-26 (v0.84.0 — catalog-driven initial-login extraction; show an app's auto-generated password on its page)
|
||||
|
||||
> **2026-06-26 — v0.84.0 (deployed on 9201): show an app's auto-generated first-login on its page.**
|
||||
> General, catalog-driven mechanism: `.felhom.yml initial_credentials: {file, format json|regex|plain,
|
||||
> username_key/password_key | username_pattern/password_pattern, note}`. The controller reads the file
|
||||
> **live** from the container (`internal/stacks/initialcreds.go` `ReadInitialCredentials` + pure
|
||||
> `parseInitialCreds`, unit-tested), never persists it, and renders a "Kezdeti belépési adatok" card on
|
||||
> `/apps/{slug}` (masked password + reveal/copy). First consumer: crafty-controller (Crafty writes a
|
||||
> random admin password to `/crafty/app/config/default-creds.txt`). Live-verified on 9201: card shows
|
||||
> username `admin` + the real extracted password. Reuse for any future self-seeding app. Security: same
|
||||
> exposure class as the existing post-deploy reveal / default_creds card — relies on the prod dashboard
|
||||
> being auth-gated (demo public-unauth is the separate tracked issue). Backlog idea unchanged: a
|
||||
> `backend_scheme` hint for TLS backends (v0.83.0 line).
|
||||
|
||||
> **2026-06-26 — v0.83.0 (deployed on 9201): scoped Traefik backend transport for self-signed HTTPS apps.**
|
||||
> Crafty is the **first/only catalog app with an HTTPS backend** (self-signed TLS on `:8443`, no plain-HTTP
|
||||
|
||||
@@ -1,67 +1,57 @@
|
||||
# REPORT — Traefik scoped serversTransport for self-signed HTTPS backends (crafty 502 fix)
|
||||
# REPORT — Catalog-driven initial-login extraction (show an app's auto-generated password)
|
||||
|
||||
**Repo:** `felhom-controller` · **Version:** `v0.82.0` → **`v0.83.0`** · **Date:** 2026-06-26
|
||||
**Commit:** `2d6df0f` · **Paired catalog commit:** `app-catalog-felhom.eu` `1216a8a`
|
||||
**Repo:** `felhom-controller` · **Version:** `v0.83.0` → **`v0.84.0`** · **Date:** 2026-06-26
|
||||
**Commit:** `1705d71` · **Paired catalog commit:** `app-catalog-felhom.eu` `8fd9c00`
|
||||
**Deployed:** guest 9201 (`demo-felhom`) on `felhom-pve`, golden/bootstrap mechanism.
|
||||
|
||||
## Problem
|
||||
The crafty-controller healthcheck fix (catalog `68ce009`) un-withheld its Traefik route, exposing a
|
||||
pre-existing **502**: Traefik proxied **HTTP** to Crafty's **HTTPS-only** self-signed backend on `:8443`.
|
||||
Crafty is the only catalog app with an HTTPS backend (all others serve plain HTTP, so Traefik's default
|
||||
HTTP transport works for them). In Traefik v3 `insecureSkipVerify` cannot be set via Docker labels — it
|
||||
must live in static/file config — so the fix necessarily touched the controller (which generates the
|
||||
Traefik config), not just the catalog.
|
||||
Some apps generate a random first-login password into a file at first boot instead of taking it from a
|
||||
deploy field — Crafty writes one to `/crafty/app/config/default-creds.txt` (its built-in default is
|
||||
rejected as "too short"). Customers had to read the container logs to find it; the static
|
||||
`app_info.default_creds` hint can't carry a per-install secret.
|
||||
|
||||
## Fix (Option B — scoped; verification stays ON by default)
|
||||
A **named** serversTransport, opted into per-service — no global `insecureSkipVerify`.
|
||||
## Fix — a general, catalog-driven mechanism (Crafty is the first consumer)
|
||||
- **`.felhom.yml`** gains an optional `initial_credentials` block:
|
||||
`{file, format: json|regex|plain, container?, username_key/password_key (json),
|
||||
username_pattern/password_pattern (regex), note}`.
|
||||
- **`internal/stacks/metadata.go`** — new `InitialCredentials` struct + `Metadata.InitialCreds`
|
||||
(deep-copied in `deepCopyStack`).
|
||||
- **`internal/stacks/initialcreds.go`** — `ReadInitialCredentials(stack)` reads the file **live** from
|
||||
the running container (`docker exec <c> cat <file>`; path passed as a single arg, no shell) and parses
|
||||
it via the pure, unit-tested `parseInitialCreds` (json/regex/plain). Container defaults to the stack's
|
||||
main container (`findProbeContainer`). Returns a non-Available result (card hidden) when the container
|
||||
is down / file missing / parse fails. **Never persists the secret** to `app.yaml` — read on demand.
|
||||
- **`internal/web/handlers.go`** — `appDetailHandler` populates `InitialCreds` for deployed apps with a
|
||||
spec; **`app_info.html`** renders a "Kezdeti belépési adatok" card: username + masked password with
|
||||
Megjelenítés/Másolás (value read from a hidden element — never inlined into a JS literal), clearly
|
||||
labelled as the **initial** password (valid only until the customer changes it in-app).
|
||||
- **Tests:** `parseInitialCreds` json (+ default key) / regex (+ missing-pattern) / plain / unknown-format.
|
||||
`go build`, `go vet`, `go test ./internal/stacks/...` all green.
|
||||
|
||||
- `internal/infra/infra.go` — new pure `RenderServersTransports()` + exported const
|
||||
`ServersTransportInsecure = "insecure-skip-verify"`. Emits:
|
||||
```yaml
|
||||
http:
|
||||
serversTransports:
|
||||
insecure-skip-verify:
|
||||
insecureSkipVerify: true
|
||||
```
|
||||
- `internal/stacks/infra.go` — new `ensureServersTransports(traefikDir)` writes
|
||||
`dynamic/serverstransports.yml` (0644) idempotently (write-only-on-change, like `wireController`, so the
|
||||
traefik file-watcher doesn't reload each self-heal tick). Called from `EnsureBaseStack` **outside**
|
||||
`ensureTraefik` (which early-returns when traefik is already running) so an established node still
|
||||
materializes the file on the next self-heal tick / restart; the file-provider `watch: true` hot-loads it
|
||||
(no traefik restart).
|
||||
- Catalog (`1216a8a`): crafty service gains `loadbalancer.server.scheme=https` +
|
||||
`loadbalancer.serverstransport=insecure-skip-verify@file`.
|
||||
- Tests: `TestServersTransports` (named transport + insecureSkipVerify + valid YAML) and the new render
|
||||
added to the YAML-parse / no-`:latest` matrix. `go test ./internal/infra/... ./internal/stacks/...` green.
|
||||
## Catalog (`8fd9c00`)
|
||||
crafty-controller `.felhom.yml` gains the `initial_credentials` block (json, username/password keys, a
|
||||
note) and `first_steps` now points at the app page instead of "find it in the logs".
|
||||
|
||||
## Deploy
|
||||
Commit → build+push `felhom-controller:0.83.0` on 192.168.0.180 → guest 9201 (`docker pull` →
|
||||
`/etc/felhom-controller-image` → restart bootstrap svc). On controller startup, `EnsureBaseStack` wrote
|
||||
`dynamic/serverstransports.yml` automatically (logged: `[infra] wrote backend transports → … (insecure-skip-verify)`)
|
||||
— no manual file drop needed. Catalog sync (`POST /api/sync`) pulled the two labels; crafty recreated via
|
||||
`POST /api/stacks/crafty-controller/update`.
|
||||
## Security posture
|
||||
This surfaces a **live working credential** on the app page — same exposure class as the existing
|
||||
post-deploy password reveal and the `default_creds` card, and rendered with the same mask/reveal UX. It
|
||||
relies on the dashboard being **auth-gated in production**; the demo's public-unauth dashboard is a
|
||||
separate, pre-existing tracked issue (not introduced here). The secret is never written to controller
|
||||
state — only read live from the volume where it already lives.
|
||||
|
||||
## Verification (live, guest 9201)
|
||||
## Verification (live, guest 9201, controller v0.84.0)
|
||||
| Check | Result |
|
||||
|---|---|
|
||||
| Dynamic file present + loaded | `/opt/docker/stacks/traefik/dynamic/serverstransports.yml` written, no traefik parse error |
|
||||
| **A — public URL** | `https://minecraft.demo-felhom.eu` → **302** (was 502), via Cloudflare edge + in-guest Traefik |
|
||||
| **A — Traefik backend** | access log: `302 … "crafty-controller@docker" "https://172.18.0.6:8443"` (before: `502 … "http://172.18.0.6:8443"`) |
|
||||
| **B — other apps unaffected** | filebrowser (HTTP backend) still **200** via Traefik — scoped, no global change |
|
||||
| **C — scoping proof** | **no** `insecureSkipVerify` in static `traefik.yml`; present ONLY in `dynamic/serverstransports.yml` |
|
||||
| crafty health | `Up (healthy)`, dashboard state `running` |
|
||||
| Spec synced + parsed | on-disk `.felhom.yml` carries `initial_credentials`; `/api/stacks/crafty-controller` meta shows it |
|
||||
| Card renders | `/apps/crafty-controller` HTML contains the "Kezdeti belépési adatok" card |
|
||||
| Username | `initcred-user` cell renders `admin` |
|
||||
| Password (live-extracted) | hidden value element carries the exact password from the container's `default-creds.txt` (HTML-escaped `&`→`&`, decoded back by JS `textContent`) |
|
||||
| Controls | `Felhasználónév` label + `icRevealPw`/`icCopyPw` reveal/copy wired |
|
||||
|
||||
## Deployed versions (`docker ps`)
|
||||
```
|
||||
felhom-controller gitea.dooplex.hu/admin/felhom-controller:0.83.0 Up (healthy)
|
||||
crafty-controller registry.gitlab.com/crafty-controller/crafty-4:4.4.8 Up (healthy)
|
||||
```
|
||||
|
||||
## Verification method
|
||||
curl to the Cloudflare public edge + in-guest Traefik probe (`--resolve …:127.0.0.1`) + Traefik access
|
||||
logs. (No browser tool used this run.)
|
||||
**Method:** fetched the exact HTML the browser receives via the in-guest API (Host: felhom.demo-felhom.eu).
|
||||
The reveal/copy is client-side JS — wiring confirmed in markup, not browser-clicked this run.
|
||||
|
||||
## Observation (carry, not acted on)
|
||||
Generalize a `backend_scheme`/`backend_tls` hint in `.felhom.yml` so future HTTPS-backend apps get the
|
||||
`scheme=https` + `serverstransport=…@file` labels via the catalog convention rather than hand-added —
|
||||
own task.
|
||||
The whole-dashboard public-unauth exposure on the demo remains the right place to fix credential
|
||||
visibility broadly — tracked separately; this feature should not be read as endorsing an unauthenticated
|
||||
dashboard.
|
||||
|
||||
@@ -207,6 +207,16 @@ Each app can define rich metadata in `.felhom.yml`:
|
||||
- `app_info`: tagline, use_cases, first_steps, prerequisites, default_creds, docs_url
|
||||
- `optional_config`: groups of post-deploy configurable env vars (e.g., API keys for metadata providers)
|
||||
- `resources`: mem_request, mem_limit, pi_compatible, needs_hdd, hungarian_ui
|
||||
- `initial_credentials` (v0.84.0): for apps that **auto-generate a first-login password into a file** at
|
||||
first boot (vs taking it from a deploy field) — e.g. Crafty → `/crafty/app/config/default-creds.txt`.
|
||||
Fields: `file` (path inside the container), `format` (`json`|`regex`|`plain`), optional `container`
|
||||
(defaults to the stack's main container), `username_key`/`password_key` (json), `username_pattern`/
|
||||
`password_pattern` (regex, first capture group), `note`. `ReadInitialCredentials`
|
||||
(`internal/stacks/initialcreds.go`) reads the file **live** via `docker exec … cat` and parses it with
|
||||
the pure `parseInitialCreds`; the value is **never persisted** to `app.yaml`. `appDetailHandler`
|
||||
surfaces it on `/apps/{slug}` as a "Kezdeti belépési adatok" card (masked password + reveal/copy),
|
||||
labelled as the *initial* credential (stays valid only until the customer changes it in-app). Hidden
|
||||
when the container is down / file missing / parse fails. Reuse for any future self-seeding app.
|
||||
|
||||
The `/apps/{slug}` page renders hero section, screenshots, setup guide, and optional config form.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user