# AUDIT — NAS-backed CWA deploy: storage mismatch + "not a separate drive" RCA (2026-07-11)

**Scope:** diagnostic only — no fixes applied, demo left as-found. Demo box felhom-pve
(192.168.0.162), guest 9201, controller v0.116.1, agent v0.83.0, hub v0.46.0. NFS export
`192.168.0.180:/mnt/5_hdd/felhom-data` (DooPlex). All times CEST unless marked UTC.

---

## TL;DR

The re-add of `nas-media` at 14:16 **worked end-to-end and verified against the real NFS** — the
new host automount propagated live into the running guest (shared→slave carries *new* mounts). CWA
was deployed correctly against the NAS (`app.yaml` `HDD_PATH: /mnt/felhom-drives/nas-media`) and
its library physically lives on the DooPlex export. What broke everything was a **second guest
reboot at 15:39:11** (`pct reboot 9201`, root@pam): the fresh guest namespace received the
`felhom-usb` ext4 submount but **not** the autofs trigger — since then the guest path is an empty,
root-owned local stub. CWA has been **down since 15:39** (Docker could not recreate the bind
source: `mkdir /mnt/felhom-drives/nas-media/userdata: permission denied` — which, silver lining,
also prevented a single byte from landing on the system drive). The UI showing "felhom-usb" is a
pure display bug: the deployed-app storage select marks `selected` by `IsDefault` only, never by
the stored `HDD_PATH`. The hub WARN was correct and was the **only** signal that fired.

**Blast radius: zero data loss, zero data misplaced.** Library (434 KB metadata.db + 3 authors,
5.4 MB total) safe on the export. CWA down ~4 h, silently.

---

## 1. Timeline (reconstructed from host journal, docker inspect, file mtimes)

| Time (CEST) | Event | Evidence |
|---|---|---|
| Jun 15 / Jul 6 | Old CWA cycle leftovers on felhom-usb (`userdata/import/calibre`, `backups/primary/calibre-web`) — unrelated to today | mtimes on felhom-usb |
| Jul 11 morning | Q1c supervised test: guest reboot #1, NAS trigger lost, old share later removed | Q1c findings (prior session) |
| 14:16:35 | `nas-media` re-added: agent `mkdir` + automount unit installed, agent verify OK ("netverify: mount verified") | host journal, agent log |
| 14:16:37 | Registered in controller settings (`kind: network`, mapped uid/gid 1000) | settings.json |
| 14:16:50 | **CWA deployed with `HDD_PATH: /mnt/felhom-drives/nas-media`** | app.yaml `deployed_at` |
| 14:18–15:36 | In-guest controller (comm `felhom-controll`, cgroup `/lxc/9201/...docker...`) triggers the **host** automount every ~5 min — proof the trigger was live *inside the guest* and the periodic checks saw the real NFS | host journal ("Got automount request … triggered by … (felhom-controll)") |
| 14:20–14:24 | Calibre library ingested (Pogátsa Zoltán, Robin Cook, Stephen King) onto the NFS | export file mtimes |
| 15:05:47 | CWA container (re)start — the last running instance | docker inspect StartedAt |
| **15:39:11** | **`pct reboot 9201` (root@pam) — reboot #2.** Automount requests from the guest stop dead at 15:36:54 and never resume | host journal UPID `vzreboot` |
| 15:39:15 | CWA exits (128); Docker restart (`unless-stopped`) fails permanently: `error while creating mount source path '/mnt/felhom-drives/nas-media/userdata/media/books': mkdir /mnt/felhom-drives/nas-media/userdata: permission denied` | docker inspect State |
| 17:13:25 | Controller container recreated (cause unrecorded; RestartCount=0 → not a crash loop) | docker inspect |
| ~19:40 | Audit evidence captured (below); marker test + host-retrigger probe run and cleaned up | this session |

## 2. Evidence

### 2.1 Mount reality (H1) — guest first, then host

Guest 9201 (`pct exec`, before any host-side access this session):

```
/dev/mapper/pve-root /mnt/felhom-drives ext4 rw,relatime,errors=remount-ro 0 0
/dev/sdd1 /mnt/felhom-drives/felhom-usb ext4 rw,relatime 0 0
(no nfs4, no autofs anywhere)

/mnt/felhom-drives/nas-media: empty dir, drwxr-xr-x nobody:nogroup,
Device: 252,1 (pve-root), Birth == Modify == 2026-07-11 12:16:35 UTC  ← never written since creation
df: /dev/mapper/pve-root 98497780 blocks → the SYSTEM drive
```

Controller container (`docker exec felhom-controller`): identical — plain dir on pve-root.
This is the namespace the add-time probe and the 5-min health checks run in.

Host:

```
systemd-1 /mnt/felhom-drives/nas-media autofs rw,relatime,...,timeout=60,direct ...   ← armed trigger
mnt-felhom\x2ddrives-nas\x2dmedia.automount  loaded active waiting
mnt-felhom\x2ddrives-nas\x2dmedia.mount      loaded inactive dead      ← normal 60 s idle unmount
```

Propagation (mountinfo): host `/mnt/felhom-drives` = `shared:179`, autofs child = `shared:221`;
guest bind = `shared:262 master:179` (a proper slave!), and the guest **did** receive the
felhom-usb ext4 submount (`master:51`) — but not the autofs mount. Observed rule (matches Q1c):
the guest-start bind replicates ext4 submounts but **not autofs direct mounts**; new host mounts
created **while the guest runs** do propagate (proven at 14:16).

Marker test: `RCA-host-marker` created in the export on DooPlex → **NOT visible in the guest**.
Guest-side `touch` into the stub → `permission denied` (root-owned dir, unmapped in the userns) —
the guest path is not just misdirected, it is completely unwritable. Markers cleaned up.

### 2.2 Where CWA's data actually is (H3 vs H4)

- `app.yaml` (`/opt/docker/stacks/calibre-web/app.yaml`): **`HDD_PATH: /mnt/felhom-drives/nas-media`**,
  locked field, `deployed_at 2026-07-11T12:16:50Z`.
- `docker inspect calibre-web` binds: `/mnt/felhom-drives/nas-media/userdata/media/books
  → /calibre-library` and `.../userdata/import/calibre → /cwa-book-ingest`. Config in a named volume.
- DooPlex export: full CWA skeleton + **library** (`userdata/media/books/metadata.db` 434 KB,
  3 author dirs, `.calnotes`), plus `backups/primary/calibre-web` manifest+compose. 5.4 MB total.
  `metadata.db-shm/-wal` left behind by the reboot kill (routine SQLite recovery on next open).
- Guest stub: empty (mtime == birth). felhom-usb: only Jun 15 / Jul 6 leftovers from an earlier
  CWA cycle; `userdata/media/books` on felhom-usb is empty.

### 2.3 Observability shakedown (§3.3)

The controller/agent capture rings could **not** answer H3 after the fact:
- The controller container was recreated at 17:13 (in-memory ring + docker logs of the old
  container gone — the bootstrap deploy mechanism `docker run`s a fresh container).
- Even live, the ring wraps in **~10 minutes** on an idle demo box: earliest entry 17:39:30 UTC at
  a 17:47 UTC pull; the buffer is dominated by 10-second `[stacks] Status refresh` + periodic
  `[agentapi] GET /disks` DEBUG spam.
- Hub-side bundles (72 h custody) requested by the operator during the observability validation
  predate this incident's window; only a bundle pulled between 14:16 and 15:39 would hold the add
  and deploy flows. Worth a look on hub.felhom.eu, not counted on.

H3 was answered from durable artifacts instead (app.yaml + docker inspect + export contents).
The add-flow agent-side lines *were* recoverable — from the host **journal**, not the ring.

### 2.4 Code paths (all cited against v0.116.1 tree)

- **Probe (H2):** `controller/internal/web/netprobe.go:32-47` (`NetProbeChild`) — write + readback
  + delete only; `netprobe_linux.go:21-43` re-execs as uid/gid 1000 **in the controller
  container's namespace**. There is **no fstype / statfs-magic / device-identity check** anywhere
  in the probe. Orchestrated from `netstorage_job.go:129-138`.
- **Deploy persist (H3):** the dropdown submits `HDD_PATH` as the option `value` (the real path),
  saved to app.yaml as a locked field — verified correct on the box; no substitution point found or
  needed (live evidence already refutes H3).
- **Deploy view render (H4):** `internal/web/templates/deploy.html:562-571` — the select renders
  `{{if .IsDefault}}selected{{end}}` per option and, for a deployed app, just adds `disabled`
  (line 563). **The stored `HDD_PATH` is never compared.** Handler side
  (`internal/web/handlers.go:330-342`) passes only the schedulable-path list; the app's current
  value isn't passed into the select at all. Live page confirms: *neither* option carries
  `selected` (no path currently has `is_default` in settings.json), so the browser displays the
  first option — felhom-usb.
- **Hub warning (H5):** `internal/monitor/healthcheck.go:299-302` → `system.IsMountPoint`
  (`internal/system/mounts_linux.go:19-29`): `Stat_t.Dev` of path vs parent, evaluated in the
  controller container. With the stub both are 252:1 → WARN. **Correct positive.**
- **The masking (why nothing else fired):** `internal/web/handlers.go:1383-1436`
  (`networkStorageWarnings`) decides the dashboard "NAS unreachable" badge by asking the **agent**
  (`ListNetStorage`) — and the agent's *host-namespace* view is genuinely healthy (`idle`).
  Nothing evaluates the share from the namespace the apps actually consume it in.

### 2.5 Propagation-repair probe (§3.5)

Host-side access re-triggered the autofs fine (nfs4 mounted on the host) → guest still saw the
stub. The NFS mounts as a **child of the host's autofs mount** (peer group `shared:221`), which has
no counterpart in the guest — so re-triggering can never heal a guest that lost the trigger.
What must be re-established is the **autofs trigger mount itself** in the guest namespace. Two
facts from today constrain the fix:
1. A **newly created** host mount under `/mnt/felhom-drives` (`shared:179`) propagates into running
   guests' slave binds — proven by the 14:16 re-add healing the then-running guest.
2. Therefore **restarting the automount unit while guests run** (cheap: the NFS mount is
   idle-unmounted anyway) re-plants the trigger in every running guest in one shot. This is the
   natural `ReassertNetworkMounts` mechanism; it must fire after every guest start (and prudently
   verify guest-visibility afterwards rather than assume).

## 3. Hypothesis verdicts

| # | Hypothesis | Verdict |
|---|---|---|
| H1 | Guest-side stub | **TRUE, refined.** The re-add itself propagated into the live guest and verified against the real NFS. The stub was created by the *second* reboot (15:39) — a fresh guest namespace does not inherit a pre-existing autofs trigger (Q1c mechanism re-confirmed), while it *does* inherit ext4 submounts. |
| H2 | Probe hole (no fstype check) | **TRUE in code, did not fire here.** The probe verified the real NFS at add time. Today's stub would fail the probe anyway (`not_writable`, root-owned dir) — the hole only bites when a stub happens to be uid-1000-writable. The *operative* gap is broader: no guest/controller-namespace re-verification ever runs after add-time, and the periodic NAS health signal is measured host-side (see H5/masking). |
| H3 | Deploy persisted wrong path | **FALSE.** app.yaml, docker binds and physical data all say nas-media. |
| H4 | Display-resolution bug | **TRUE.** `deploy.html:567` selects by `IsDefault` only; stored `HDD_PATH` never consulted; with no default flagged, the browser shows the first option (felhom-usb). Display-only. |
| H5 | Hub warning correct | **TRUE.** Device-ID check (healthcheck.go:299) correctly detected path-on-system-device from the controller namespace — the only guard that caught the state. Promote its mechanism to add/deploy-time refusal and to the NAS-unreachable badge. |
| S-D | Garbled "TÁ*rhely" label | **REAL, data-level.** settings.json stores the felhom-usb label as `TĂˇrhely (felhom-usb)` ("Tárhely" UTF-8 bytes re-decoded as CP1250) since its registration on **2026-07-08**; nas-media's label (2026-07-11) is clean. Current label-generation code (`settings.go:891-893`) is clean UTF-8 — one-time corruption in whatever wrote the 07-08 registration. Needs data repair + writer RCA. |

## 4. The four RCA questions

1. **Guest-side reality / did the re-add restore propagation?** The guest path is an empty,
   root-owned local directory on the system device (252:1) with no NFS or autofs anywhere in the
   guest. The re-add **did** restore propagation into the then-running guest (14:16→15:36 proven
   healthy); the 15:39 `pct reboot` severed it again. Fresh adds heal running guests; nothing
   heals a rebooted guest today.
2. **Where is CWA's data / H3 xor H4?** Physically on the DooPlex export (5.4 MB, intact incl.
   metadata.db); `app.yaml` says nas-media. **Display bug (H4)**, not a persistence bug. CWA
   itself has been down since 15:39 — the operator's "runs and looked like working" was the
   pre-reboot state.
3. **Why did the verify pipeline pass?** Legitimately — at add time the mount was real in the
   probing namespace. The design gaps: (a) the probe asserts writability, never
   network-filesystem identity (statfs magic / device ≠ rootfs-device); (b) verification runs
   once, at add time, in a state that a guest reboot silently invalidates; (c) ongoing NAS health
   is asked of the agent (host namespace), so the guest-side stub is invisible. The minimal
   add-time check that would refuse today's state: statfs the path in the controller namespace and
   require the expected network fstype — cheap, and it also converts the post-reboot stub into a
   hard, attributable failure whenever any re-verification runs.
4. **Hub warning correct or false positive?** Correct behavior, right namespace, only working
   guard. Promote it: a path that fails the device-ID / fstype test must not be deployable-to, and
   should flip the NAS badge, not just a generic system WARN.

**Blast radius:** no data on the system drive (docker's own `mkdir` was blocked by the root-owned
stub — accidental but effective backstop); no data lost; CWA down ~4 h with no alert beyond the hub
storage WARN. **Customer-plausible: fully.** Any NAS-backed app + any guest reboot (agent update
rollout, host reboot, power blip) reproduces this on a real customer box: app down or — worse, if
the stub is ever writable (e.g. a future uid-1000-owned mountpoint dir) — silent writes to the
system drive. This ordering drives the fix list.

## 5. Prioritized fix tasks (each a separate follow-up; none implemented here)

1. **HIGH — agent `ReassertNetworkMounts` on guest start** (existing backlog
   `FOLLOWUP-nas-automount-guest-reboot-reassert.md`, now enriched): mechanism = restart/re-create
   the automount unit after a guest starts (new-mount propagation into running guests is proven);
   must cover all net shares, fire on every guest start (not only reboot-the-verb), and
   post-verify guest visibility instead of assuming. Host-side re-triggering alone provably
   cannot heal a guest.
2. **HIGH — verify in the consuming namespace, continuously:** (a) add-time probe gains a statfs
   fstype/device check (H2 fix — a stub must never verify); (b) deploy-time refusal: a network
   `HDD_PATH` whose in-namespace fstype is not the expected network fs is not selectable/deployable
   (promotes the H5 check); (c) `networkStorageWarnings` must combine the agent's host view with a
   controller-namespace check so a guest-side stub raises the NAS badge (today's masking).
3. **MEDIUM — boot-time app-start failure must alert:** CWA sat in `Exited(128)`/restart-failed
   for hours with no issue event. The restart error string ("error while creating mount source
   path") is exactly the signal to surface (app-issue event → hub), likely reusing the shipped
   app-log issue pipeline.
4. **MEDIUM — H4 display fix:** deployed-app storage select must mark `selected` by the stored
   `HDD_PATH` (fallback label when the path is no longer in the schedulable list), not by
   `IsDefault`. Also check why no storage path currently carries `is_default`.
5. **LOW — S-D label repair + writer RCA:** fix the stored felhom-usb label (one relabel call),
   then find/red-proof the 2026-07-08 write path that CP1250-mangled it (drive attach/registration
   flow).
6. **LOW — ring usefulness:** exclude/rate-limit the 10-second status-refresh DEBUG spam from the
   capture ring (10-minute effective window defeats after-the-fact RCA), and note that bootstrap
   container recreation discards both ring and docker logs — the host journal and hub bundles are
   the only durable channels.

## 6. Remediation — PERFORMED (operator GO given in-session, executed ~20:14–20:20 CEST)

The proposed recipe ran exactly as written and **doubles as a successful live pre-test of the
fix-1 mechanism**:

1. `systemctl restart 'mnt-felhom\x2ddrives-nas\x2dmedia.automount'` on the host (guest running)
   → the autofs trigger appeared in the guest namespace **immediately** (propagation of a fresh
   host mount into a running guest's slave bind, re-proven).
2. In-guest `ls` woke the automount; guest AND controller container now show the `nfs4` mount with
   the export content (`backups/`, `userdata/`, correct uid-1000 mapping).
3. `docker start calibre-web` → `running`, healthcheck `healthy`; `/calibre-library` shows
   `metadata.db` + all three author dirs; SQLite WAL recovered cleanly (0-byte wal, shm re-touched).
4. uid-1000 write probe from inside the CWA container through the NFS: **WRITE-OK**.
5. App answers over traefik (`Host: books.demo-felhom.eu`): 301→302 login redirect chain, alive.

No rsync was needed (nothing was misplaced). The controller health WARN (S-B) is expected to clear
on the next 5-minute monitor cycle now that `IsMountPoint` sees nfs4 ≠ pve-root.

**Fix-1 design note validated live:** restarting the `.automount` unit is a sufficient, cheap,
data-safe reassert mechanism for running guests; the agent follow-up only needs to invoke it after
guest starts and post-verify guest visibility.

## 7. Follow-up — fixes 1+2+4 SHIPPED (2026-07-11 late)

Agent **v0.84.0** (`felhom-agent` commit `474b858`: `ReassertNetworkMounts` — guest-hook post-start
leg + agent-startup sweep, §8 decision table, stop+enable-now on the granted verbs) + controller
**v0.117.0** (`felhom-controller` commits `c0f3e12`+`4a9c54a`: statfs namespace classifier, probe
`not_network_fs` assertion, deploy-time stub refusal, dashboard stub badge, deploy-view
selected-by-stored-HDD_PATH rider). Q1c re-run live (`pct reboot 9201`, operator-GO): the hook
re-armed and verified the share DURING the start task; CWA auto-started healthy; health ok; no
badge. **Q1c is green — reboot survival is automatic.** Fixes 3 (boot-failure alerting), 5 (label
repair), 6 (ring revision) remain queued. Feature doc updated:
`documentation/controller/network-storage-nas.md` §Reboot survival.

---
*Evidence gathered non-destructively 2026-07-11 ~19:35–19:50 CEST; only artifacts created were the
two `RCA-` markers (removed). Sources: host journal, pct exec, docker inspect, settings.json,
app.yaml, DooPlex export listing, live page fetch, v0.116.1 source.*
