diff --git a/REPORT.md b/REPORT.md index 2521e00..6e50e10 100644 --- a/REPORT.md +++ b/REPORT.md @@ -1,55 +1,121 @@ -# REPORT — felhom-agent v0.43.0 (BUNDLE slice: canonical unit + Gitea publish) +# REPORT — Agent sudoers completeness audit + redeploy (restore multi-drive) -**Date:** 2026-06-28 · **Baseline:** v0.42.0 @ `aaa276a` → **v0.43.0** @ `a8d14fc` +**Date:** 2026-06-29 · **Class:** Risky/supervised (host-root allowlist edit + live redeploy) · +**Binary:** unchanged (no version bump) · **Baseline:** felhom-agent `main` @ `c3020ee` → this change +`55888e5`. -## What shipped +## What & why -Day-0 no longer needs a hand-installed agent. The binary is **published to Gitea** and the -host-bootstrap script fetches → verifies (sha256 vs the hub-vouched manifest) → installs it. This -commit adds the **canonical systemd unit** and the publish tooling; the binary is a version-only -rebuild (no behavioural change). +The 2026-06-28 root→non-root agent cutover left `configs/felhom-agent.sudoers` **incomplete**: +several commands the agent shells via `sudo -n` were never granted. The headline casualty — +`lxc-info` — broke `guestInitPID → GuestSeesMount → bound_under_parent`, so **every external drive was +reported absent**, and the controller drive-gate stopped each drive's apps (the "attach one, the other +drops" flapping; audit `felhom.eu/documentation/audits/SPIKE-multidrive-mutual-exclusion-2026-06-29.md`). +This pass audited **every** privileged command against the allowlist and closed all read-only/ +fixed-vector gaps in one go; arbitrary-execution gaps were **surfaced, not added**. -- **`configs/felhom-agent.service`** (NEW, canonical) — `User=felhom-agent` (the documented non-root - production model; `privileged.mode: "sudo"` + the sudoers allowlist), - `ExecStart=/usr/local/bin/felhom-agent --config /etc/felhom-agent/agent.json`, - `Restart=on-failure`, `StateDirectory=felhom-agent`. **No `NoNewPrivileges`** (would block the - `sudo` the agent needs) and **no mount-namespacing hardening** (would put the agent in a private - mount namespace, breaking `mount --make-shared`/`--bind` drive propagation into guests) — both - documented inline. -- **`scripts/publish-agent.sh`** (NEW) — build (optional) + PUT binary to - `/api/packages/admin/generic/felhom-agent//felhom-agent`, print `AGENT_VERSION`/`AGENT_SHA256`, - GET round-trip re-verify. Pinned version, idempotent (delete-then-PUT), asserts `--version` matches. -- **`configs/build-golden.sh`** — after vzdump, compute sha256 + PUT - `/api/packages/admin/generic/felhom-golden//golden.tar.zst`, print - `GOLDEN_VERSION`/`GOLDEN_SHA256`. Opt-in; local auto-discovery stays a fallback. -- **`configs/felhom-agent.sudoers`** (latent bug fix) — escaped the commas in the `lvs -o …` and - `lsblk -o …` argument lists. Bare commas are command separators in sudoers, so `visudo -cf` - **rejected** the file; it had never been visudo-validated live because the demo ran the agent - root+`direct` (sudoers unused). Surfaced by the live install's `visudo -cf` gate. -- **`cmd/felhom-agent/main.go`** — `version` 0.42.0 → 0.43.0. -- **README** — "Process model" section made canonical (non-root + publish/install). +## Audit table — every privileged command vector vs the allowlist -## Green gate -`go build ./... && go vet ./... && go test ./...` — all green. `scripts/publish-agent.sh` + -`configs/build-golden.sh`: `bash -n` + `shellcheck -S warning` clean (the one SC2034 in build-golden.sh -is a pre-existing DHCP-wait loop counter, not this change). +`runner` prepends `sudo -n `; the allowlist matches **path + arg vector**. -## Live (felhom-pve, demo-felhom) — proven -- Built + published **agent 0.43.0** via `publish-agent.sh` → sha256 `8b989917…`; GET round-trip OK. -- Published the existing **golden 0.85.1** (575 MiB) to Gitea → sha256 `f87031cc…`. -- **From-scratch install** (after moving the live root+direct agent fully aside): the script fetched - the binary from Gitea, **verified sha256 vs the hub manifest**, installed the non-root `felhom-agent` - user + binary + sudoers (`visudo -cf`-validated) + the canonical unit + config (0600 `felhom-agent`); - the service came up **active as non-root felhom-agent**, `--selftest=hub` landed a host-report, and - guest **9201 stayed managed** (present in the host-report). `sudo -n` confirmed live for - mkdir/smartctl/lvs (the escaped-comma sudoers works). -- **sha256 negative:** a deliberately-corrupted published binary made the install **abort** - ("Refusing to install (verify-before-use)", exit 1); the good binary was restored (sha re-verified). +| Command vector (shape) | Call site | Alias | Status | +|---|---|---|---| +| `lxc-info -n [0-9]* -p -H` | intermediary.go:256 `guestInitPID` | FELHOM_INTERMEDIARY | **GAP → CLOSED** | +| `mount --make-private /mnt/felhom-drives` | intermediary.go:110 `EnsureSharedParent` | FELHOM_INTERMEDIARY | **GAP → CLOSED** | +| `systemctl restart dnsmasq` | lanresolver `restartDnsmasq` | FELHOM_DNSMASQ | **GAP → CLOSED** | +| `pct set [0-9]* -onboot 1` | backhalf.go:162 (F3 autostart) | FELHOM_PROVISION | **GAP → CLOSED** | +| `pct reboot [0-9]*` | guestbind.go:100 `RebootGuest` (disks.go:448) | FELHOM_GUESTHOOK | **GAP → CLOSED** | +| `mount --make-shared /mnt/felhom-drives` | intermediary.go:113 | FELHOM_INTERMEDIARY | OK | +| `mount --bind /mnt/felhom-drives /mnt/felhom-drives` | intermediary.go:107 | FELHOM_INTERMEDIARY | OK | +| `mount --bind /mnt/*/felhom-data /mnt/felhom-drives/*` | intermediary.go:204 | FELHOM_INTERMEDIARY | OK | +| `umount /mnt/felhom-drives/*` | intermediary.go:199,340 | FELHOM_INTERMEDIARY | OK | +| `mkdir -p /mnt/felhom-drives[/*]` , `/mnt/*/felhom-data` | intermediary/backhalf | FELHOM_INTERMEDIARY | OK | +| `chown 100000:100000 /mnt/*/felhom-data` | intermediary.go:181 | FELHOM_INTERMEDIARY | OK | +| `install -m 0755/0644 -- /tmp/felhom-shared-parent.{sh,service} …` | intermediary.go:136,144 | FELHOM_INTERMEDIARY | OK | +| `systemctl enable felhom-shared-parent.service` | intermediary.go:150 | FELHOM_INTERMEDIARY | OK | +| `systemctl daemon-reload` | intermediary.go:147 | FELHOM_MOUNT | OK | +| `pct set [0-9]* -mp8 /mnt/felhom-drives*` | backhalf.go:184 | FELHOM_INTERMEDIARY | OK | +| `pct set [0-9]* -mp[0-9]* /var/lib/felhom-agent/guests/*` | backhalf.go:152 | FELHOM_PROVISION | OK | +| `chown -R 100000:100000 /var/lib/felhom-agent/guests/*` | backhalf.go:145 | FELHOM_PROVISION | OK | +| `install -m 0755 -- /tmp/felhom-guest-hook.sh /var/lib/vz/snippets/…` | guesthook install.go:45 | FELHOM_GUESTHOOK | OK | +| `pct set [0-9]* --hookscript local:snippets/felhom-guest-hook.sh` | guesthook install.go:55 | FELHOM_GUESTHOOK | OK | +| `pct set [0-9]* --delete mp[0-9]*` | decommission/eject | FELHOM_GUESTHOOK | OK | +| `smartctl -a -j /dev/` | privileged.go:150 `SMART` (observe.go:90) | FELHOM_DISK | OK | +| `lvs --reportformat json …` | storage | FELHOM_DISK | OK | +| `blkid -p -o export /dev/*` , `lsblk -J -o … /dev/*` | format | FELHOM_FORMAT | OK | +| `mkfs.ext4 -F /dev/*` , `mkfs.xfs -f /dev/*` | format | FELHOM_FORMAT | OK | +| `apt-get install -y -q dnsmasq` | lanresolver:107 | FELHOM_DNSMASQ | OK | +| `install -m /tmp/felhom-resolver-*.conf /etc/dnsmasq.d/felhom-*.conf` | lanresolver:266 | FELHOM_DNSMASQ | OK | +| `systemctl enable --now dnsmasq` , `reload dnsmasq` | lanresolver:117 | FELHOM_DNSMASQ | OK | +| `rm -f /etc/dnsmasq.d/felhom-*.conf` | lanresolver:178 | FELHOM_DNSMASQ | OK | +| `pct exec [0-9]* -- ip -4 -o addr show dev eth0` | lanresolver:187 | FELHOM_DNSMASQ | OK | +| `pct exec [0-9]* -- docker exec felhom-controller cat …/controller.yaml` | lanresolver:213 | FELHOM_DNSMASQ | OK | +| `sensors -j` | privileged.go:169 `Sensors` | — | **GAP — DEFERRED** (no caller + `lm-sensors` not installed; path unverifiable) | +| `mount UUID= ` (+`mkdir -p`) | privileged.go:139 `MountUSBByUUID` | — | no daemon caller (legacy) — not added | +| `pct create … --features nesting=1,keyctl=1 …` | privileged.go:124 `CreateGoldenLXC` | — | **SURFACED** (maintenance/broad; no daemon caller) | +| `pct exec [0-9]* -- {cat,docker,bash -c "",systemctl} …` | controllerswap.go GuestExec | — | **SECURITY FORK — SURFACED, NOT added** | -## Observations -- **Non-root + PBS:** the `felhom-agent` user cannot read `/etc/pve/priv/storage/*.pw` (root-only on - pmxcfs), so PBS datastores are skipped with a WARN (graceful; PBS cadence is 0 on the demo). A - production host using PBS needs the agent granted read access to that key (group/ACL on pmxcfs, or a - sudoers entry) — follow-up, out of this slice. -- **`sudo` package** must be present for the non-root model; the host-install script now - `apt-get install`s it (a root+`direct` host won't have it). +> Note: `guest_attached=false` in the `/disks` report is **not** a sudoers gap — `GuestConfig` is a +> Proxmox **API** call (`query.go:51`), and in the intermediary model `GuestAttached` (legacy per-mp +> bind detection) is expected-false; the gate keys on `BoundUnderParent`, the fixed signal. + +## Sudoers diff (additive) + +- **FELHOM_INTERMEDIARY:** `+ mount --make-private /mnt/felhom-drives`, `+ lxc-info -n [0-9]* -p -H` +- **FELHOM_DNSMASQ:** `+ systemctl restart dnsmasq` +- **FELHOM_PROVISION:** `+ pct set [0-9]* -onboot 1` +- **FELHOM_GUESTHOOK:** `+ pct reboot [0-9]*` + +`visudo -cf` clean (staged file + whole `/etc/sudoers` after install). + +## §5.5 — surfaced for operator decision (NOT executed) + +**`GuestExec` general `pct exec -- …`** (controller-swap self-update / Phase-2 managed +updates). The five vectors it runs: +1. `cat ` (read `/etc/felhom-controller-image`) +2. `docker image inspect ` +3. `bash -c "printf '%s\n' '' > "` ← interpolated write (arbitrary) +4. `systemctl restart ` +5. `docker inspect -f ` + +Granting `pct exec [0-9]* -- *` = **arbitrary root execution in any guest**; not added. **Consequence: +controller-swap / managed auto-update is currently BROKEN under the non-root agent** (manual +`bootstrap`-mechanism deploys still work). Operator decision needed: narrow per-vector grants (hard — +#3 interpolates the image string and uses `bash -c`), or run the swap through a different trust path +(e.g. a guest-side agent-authenticated endpoint). **Not blocking** day-to-day; surfaced for follow-up. + +Also surfaced: `pct create` (golden build — maintenance, broad args, no daemon caller) and +`mount UUID=…` (`MountUSBByUUID`, unreferenced/legacy — current USB mounts use systemd `.mount` units +via FELHOM_MOUNT). + +## Live validation (felhom-pve, guest 9201) — ALL PASS + +- **Deploy:** staged → `visudo -cf` OK → `install -m 0440 -o root -g root /etc/sudoers.d/felhom-agent` + → `visudo -cf /etc/sudoers` OK. **No agent restart** (sudo reads the drop-in per call; + `guestInitPID` runs `lxc-info` fresh each call). +- **`sudo -n` probes (as felhom-agent):** `lxc-info -n 9201 -p -H` → returns PID `2137736` (was + *"a password is required"*); `systemctl restart dnsmasq`, `pct reboot 9201`, `pct set 9201 -onboot 1` + → ALLOWED; `mount --make-private /mnt/felhom-drives` → matched (probed via `sudo -l`, **not** + executed — would churn the live parent). +- **Agent report flipped:** `/disks` now reports `bound_under_parent=true` for **both** `felhom-usb` + and `felhom-flash` (was false/false). +- **Gate quiesced:** controller logged `[gate] drive RETURNED …/felhom-usb — re-attached + restarted + gate-stopped apps` and the same for `felhom-flash`; **nextcloud auto-restarted** and held `Up + (healthy)`; **zero** `drive ABSENT` / disconnect / stop events across the following ~2 min. +- **Real UI (claude-in-chrome, `/settings`):** **both** drives show **Aktív** concurrently — + felhom-usb (915.8 GB, HD710 PRO, "1 alkalmazás használja") + felhom-flash (117.1 GB, Flash Drive). + The multi-drive mutual-exclusion symptom is **gone**. + +## Notes left for follow-up + +- **Live host-side "doubling"** (each submount under both the parent and root, because the live + `/mnt/felhom-drives` is in root's peer group `shared:1`) is **left as-is** — re-running + `make-private` now would churn the peer group and orphan the working guest slave (the very failure + `EnsureSharedParent` guards against). The new grant lets the **next clean host boot** establish the + parent correctly via `felhom-shared-parent.sh`; verify the parent has its own peer group after that + boot. Latent/harmless today (the guest sees both drives). +- **03-host-agent.md** carries only a high-level root-boundary narrative (§51), no line-level allowlist + table — nothing to sync. The sudoers file + CHANGELOG + this REPORT are the record. +- **Durable countermeasure (separate task):** an agent **capability self-probe** at startup (does each + critical `sudo -n` vector resolve?) → report denials to the hub. Would have caught all of these at + cutover.