Live drive: §9 import/export PASS (F16); §7 restore CRITICAL — restore does not restore data (F17)
This commit is contained in:
@@ -137,5 +137,26 @@ The demo dashboard has **no password set**, so the controller's `RequireAuth` an
|
||||
- **Finding:** the runbook states the bootstrap service "restarts it within seconds." In reality: `felhom-controller-bootstrap.service` is `Type=oneshot, Restart=no` (it ran once and exited). The container has `--restart unless-stopped`, but Docker treats an explicit `docker kill`/`docker stop` as a **manual stop**, so `unless-stopped` does **not** restart it (observed: `RestartCount=0`, stayed `Exited (137)` indefinitely; I had to `docker start` it manually).
|
||||
- **Impact:** `unless-stopped` *will* restart the controller after a genuine unexpected death (panic/OOM/host reboot) — so real crash-recovery is covered. But there is **no watchdog** that recovers from a manual stop or a clean exit, and the bootstrap service does not re-run. An operator who `docker stop`s the controller (or a tool that does) will find it stays down. The "auto-restarts within seconds" expectation is only partially true. **Severity: MEDIUM** (recovery assumption is narrower than documented).
|
||||
|
||||
### F16 — Import/export round-trip + CTRL-001 path-traversal — PASS — INFO
|
||||
- **Export** (vikunja → `/mnt/sys_drive/felhom-data/exports/vikunja_*.fab`): estimate → start → status all clean; `.fab` is a gzip tar (`manifest.json`, `config/{.felhom.yml,app.yaml,docker-compose.yml}`, `data/volumes`); manifest readable via `POST /api/export/manifest` (app_name, volume_names, config_files, encrypted flag). **Works.**
|
||||
- **Import round-trip:** `POST /api/export/import` of the same `.fab` (over the live vikunja) succeeded; vikunja ended `running`, `tasks.demo-felhom.eu` → 200. **Works.**
|
||||
- **CTRL-001 — app_name traversal:** crafted `.fab` with `manifest.app_name = "../../../tmp/evil_appname"` → import **rejected at manifest parse**: `Manifest hiba: appexport: app_name "../../../tmp/evil_appname" must not contain a path separator`; verified **nothing written** to `/tmp`, `/opt`, `/opt/docker`. **PASS.**
|
||||
- **CTRL-001 — tar-member traversal (tar-slip):** crafted `.fab` with a valid app_name but a tar entry `../../../../tmp/evil_member` → the member was **not** written to `/tmp/evil_member` (extraction sanitized the traversal). **PASS.** Minor: that bundle left an **empty `/opt/docker/stacks/eviltar` dir** (import got partway then failed "stack not found") — cleanup debt, not a security issue (I removed it).
|
||||
- **Verdict:** export/import works; CTRL-001 is robustly enforced on both vectors with a clear error message. **Severity: INFO (PASS).**
|
||||
|
||||
### F17 — Per-app restore does NOT restore data (DB dump never imported, volume tars not captured) — CRITICAL
|
||||
- **Area:** §7 restore — the core disaster-recovery path.
|
||||
- **Method (real data-marker round-trip on romm/MariaDB):**
|
||||
1. Inserted a marker row into romm's DB: `felhom_restore_marker (1, 'MARKER-BEFORE-BACKUP-9921')`.
|
||||
2. `POST /api/backup/run` → confirmed the marker **is in the captured dump** (`grep MARKER… romm-mariadb.sql` → 1 hit).
|
||||
3. Dropped the table (simulated data loss) — confirmed gone.
|
||||
4. `POST /backup/restore` (`stack_name=romm`).
|
||||
5. **Result: the marker table did NOT return** — `ERROR 1146: Table 'romm.felhom_restore_marker' doesn't exist` after restore.
|
||||
- **What the restore actually did (from logs):** stopped romm (`compose down`, volumes kept), `secrets recovered=3/3`, "Redeploying romm from recovery unit", `compose up -d`, "Restore-from-unit completed". The DB dump was **never imported**; the app simply came back on its surviving (post-drop) volume.
|
||||
- **Code confirmation:** `backup/restore_unit.go: RestoreFromRecoveryUnit` → `stackProvider.RecreateStackFromUnit(...)` → (`main.go:953`) copies only `docker-compose.yml` + `.felhom.yml` from the unit and calls `RedeployFromEnv`. There is **no DB-import** (`pg_restore`/`mysql <`/`.sql` replay) anywhere in `internal/backup/*`, and recovery units **do not capture volume tars** (F13). So the per-app restore is **config + secrets only**.
|
||||
- **Impact:** the headline disaster-recovery feature does not recover data. A customer who loses DB content (corruption, bad migration, accidental delete) and clicks "restore" gets the app back with **whatever was already in the volume** — if the volume is intact, their loss is unchanged; if the volume is gone, the DB comes up **empty**. The carefully-captured DB dumps and recovery-unit manifests are effectively decorative for restore purposes. The only path that actually restores data is the **PBS whole-guest snapshot** (the agent's), which restores the entire LXC incl. the docker-data volume.
|
||||
- **What does work:** secret recovery (3/3 from the guest's own app.yaml), the fail-closed data-key gate (Phase 2b, proven previously), and the app coming back healthy. The orchestration is fine; the data-replay step is simply missing.
|
||||
- **Verdict:** broken. **Severity: CRITICAL** (restore advertised as data recovery does not restore data). Also: `/backup/restore` reads `snapshot_id` but **ignores it** (`RestoreFromRecoveryUnit` takes only the stack name) — there is no point-in-time selection; "restore" always means "re-create from the single latest unit."
|
||||
|
||||
### F4 — `/api/stacks/rescan` returns "stack not found: rescan" — LOW
|
||||
- The runbook's documented rescan endpoint `GET /api/stacks/rescan` is routed as a stack name lookup → `{"ok":false,"error":"stack not found: rescan"}`. Either the route was removed/renamed or the runbook is stale. (Sync/rescan is reachable via `POST /api/sync`.) Cosmetic but documents a stale/missing endpoint.
|
||||
|
||||
Reference in New Issue
Block a user