docs(audit): SPIKE NAS verify mechanisms + squash matrix — Q1-Q5 answered, Route A accepted-with-caveats
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -2,59 +2,48 @@
|
||||
|
||||
> **Overwrite** this file with a summary of the most recent task only (uniform with the other repos; not cumulative). The cumulative hub history lives in [hub/CHANGELOG.md](hub/CHANGELOG.md); the scripts history lives in [scripts/CHANGELOG.md](scripts/CHANGELOG.md).
|
||||
|
||||
## TASK — PBS DR tier SLICE 1: ep0 tenantsync surface + hub provisioning (hub v0.44.0) — 2026-07-10
|
||||
## SPIKE — NAS verify mechanisms + NFS squash matrix — 2026-07-11
|
||||
|
||||
**LIVE on k3s (Synced/Healthy, image 0.44.0, startup logs `WG peer-sync enabled` + `PBS DR tenantsync
|
||||
enabled`). NOT live-provisioned — no real customer was provisioned from this task; slice 2 (the agent
|
||||
apply-bridge) + the supervised Peti enable are pending.** Builds on SPIKE-pbs-tier-provisioning
|
||||
(00afadc). Commits: ce6a566 (code) + 32ca1f6 (live fix + image bump).
|
||||
**Class:** spike (findings doc is the only artifact; no production code touched).
|
||||
**Record:** `documentation/audits/SPIKE-nas-verify-2026-07-11.md`.
|
||||
**Baselines (read-only):** felhom-agent `300f06722b` (v0.80.0, `netmount.go` = recipe under test),
|
||||
felhom-controller `3db9126121` (v0.112.0, the probe container), felhom.eu `8ca1272db6`.
|
||||
|
||||
### What shipped
|
||||
- **ep0 surface (🛑 Viktor-approved, installed + smoked live):** `scripts/felhom-tenantsync.sh` v1.0.0
|
||||
as a SECOND forced-command key for `felhom-peersync` (own sudoers drop-in; **peersync script +
|
||||
sudoers sha256 asserted byte-identical before/after**). Ops: `provision` (existing token → typed
|
||||
`token_exists`; own-ns self-check with one regen retry then rollback), `reissue` (delete→recreate→
|
||||
re-grant — delete purges token ACLs, the spike fact), `fingerprint`. Secret hygiene: the token
|
||||
secret rides stdout ONLY (all tool stdout → stderr; hub error paths never embed stdout).
|
||||
Smoke (throwaway `spike-smoke`, torn down): all ops green; **live fix caught by the smoke** —
|
||||
`proxmox-backup-client --output-format json` wraps as `{"data":[…]}` → ns-exists check fixed to
|
||||
`(.data // .)` and reinstalled before the hub ever used the surface.
|
||||
**Orphan cleanup (spike flag #1):** `root@pam!spike` token + `/datastore/scratch` DatastoreAdmin
|
||||
ACL removed; ep0 now holds exactly the real `demo-felhom-01` tenancy.
|
||||
- **Hub `internal/tenantsync`:** wgsync-twin SSH client (pinned host key, constrained
|
||||
HostKeyAlgorithms, per-op JSON exec, typed `ErrTokenExists`). Contract test
|
||||
`TestErrors_NeverEmbedStdout` pins the no-stdout-in-errors rule.
|
||||
- **Provisioning flow (`web/pbsdr.go`):** config-form section "PBS DR tier (ep0)" → on enable+save
|
||||
(detached-ctx atom): WG peer existence check (the agent self-registers its key; absence
|
||||
fail-closes with "has not reported a WG key yet") → tenantsync provision → secret stored
|
||||
consume-once HOST-scoped (`host_pbs_secrets`, the `one_time_secrets` twin) → non-secret `pbs_dr`
|
||||
descriptor (`enabled, storage_id, pbs_tunnel_ip, datastore, namespace, token_id, fingerprint`)
|
||||
merged into the HOST `desired_json` + `SetHostDesired` generation bump. Already-provisioned
|
||||
re-save = success-no-op; disable = `enabled:false` descriptor, tenancy kept; **Re-issue PBS
|
||||
credentials** button = the F4 precedent (fresh consume-once secret + bump).
|
||||
- **Consume endpoint:** `POST /api/v1/hosts/{id}/pbs/consume-token` — per-host key self-scoped
|
||||
(global = operator recovery); 200 exactly once → 404; a foreign key's 403 does NOT burn the
|
||||
secret. *(Deviation: task spec wrote `/host/{id}/…`; implemented under `/hosts/` matching every
|
||||
other agent-facing route — slice 2 must use the `/hosts/` form.)*
|
||||
- **Deploy:** `TENANTSYNC_SSH_KEY_FILE` env + optional `Secret/tenantsync` mount (key generated on
|
||||
180, moved into the Secret, local copy shredded); same endpoint addr + pinned host key as peersync.
|
||||
### Verdicts (all five questions answered live, evidence in the record)
|
||||
|
||||
### Tests + red-proofs (all run, mutations reverted, gate green)
|
||||
- Happy path: descriptor lands in desired_json + gen bump exactly 1; secret consumable once and
|
||||
absent from ConfigJSON/desired-state/captured logs (grep-asserts).
|
||||
- **Red-proof consume-once:** dropping the `consumed_at` UPDATE → store test re-served `"secret-1"`,
|
||||
API test returned 200 with the secret again → both FAIL as designed.
|
||||
- **Red-proof fail-closed:** swallowing the applyPBSDR error in handleConfigUpdate → all four
|
||||
fail-closed subtests FAIL with 303 (half-enabled save).
|
||||
- **Red-proof idempotency:** dropping the already-provisioned short-circuit → re-save re-keyed the
|
||||
token (2 provision calls), staged a fresh secret, bumped spuriously → FAIL.
|
||||
- Also covered: foreign-host 403 non-burning, reissue rotation (old secret dead), disable no-op on
|
||||
second save, storage-id edit updates descriptor without re-provision, tenantsync client
|
||||
round-trip against an in-process SSH server incl. wrong-host-key refusal.
|
||||
- UI leg: render tests (`TestPBSDR_FormRendersState` + suite) — the hub UI is password-gated, so
|
||||
render tests are the verification method per repo convention.
|
||||
- **Q1 — in-guest automount trigger: PASS (make-or-break).** A `pct exec` ls AND a docker
|
||||
bind-mount access from inside guest 9201 both wake the host systemd automount (~1 s); content
|
||||
correct, nfs4 propagates host→guest; idle-unmount + re-trigger proven. Q1c open item: whether a
|
||||
trigger installed BEFORE guest start carries through at boot needs a restart-window test (no
|
||||
restart permitted this spike).
|
||||
- **Q2 — controller uid-1000 probe: PASS.** `docker exec -u 1000:1000` chain proof + `setpriv
|
||||
--reuid 1000` mechanism proof (same syscalls as `syscall.Credential`); CapEff `a80425fb` has
|
||||
SETUID/SETGID; server-side files land `101000:101000`.
|
||||
- **Q3 — Route A: ACCEPTED-with-caveats.** `all_squash` to an arbitrary server uid (1050) gives the
|
||||
guest-uid-1000 app full read/create/rename/**hardlink**/delete. Caveats: guest sees
|
||||
`nobody:nogroup`; `chown` fails immediate-clean EPERM; `chmod` SUCCEEDS and persists server-side.
|
||||
q3c pinned the June WRONG case: `anonuid` without `all_squash` never applies to non-root traffic —
|
||||
wire uid 101000 fails on others-perms. Consumer guidance must require the "map/squash ALL users"
|
||||
mode, not just an anon-uid field.
|
||||
- **Q4 — error taxonomy + timing:** full table in the record. Headlines: NFSv4 returns the IDENTICAL
|
||||
`No such file or directory` for nonexistent vs not-permitted exports (merge those error
|
||||
categories); dead server = systemd `timeout` at **90 s** (systemd kills mount.nfs before its 2-min
|
||||
retry — no diagnostic string); **`retry=0` recommended for the PRODUCTION unit** (dead-server
|
||||
verify 90 s→3.1 s with clean `No route to host`; guest on-demand access 91 s→3.8 s ENODEV; happy
|
||||
path untouched; autofs retries fresh per access). SMB splits cleanly: `error(13)`=credentials,
|
||||
`error(2)`=share name. **Budget (Q4-vii):** verify cannot ride the agentapi global 15 s timeout
|
||||
(`client.go:87`) — worst case is systemd's 90 s; give AddNetStorage its own longer-lived path or
|
||||
async status-poll, ~120 s end-to-end, agent-side `systemctl start` bounded at ~95 s.
|
||||
- **Q5 — plain-user SMB: PASS.** Stock user + rw share, zero server-side uid config, all 8 probe
|
||||
steps pass INCLUDING hardlink (on Debian Samba/ext4 — re-verify on a real appliance); server-side
|
||||
ownership = the authenticated account; guest sees forced `1000:1000/0664`.
|
||||
|
||||
### Operator follow-ups
|
||||
- Slice 2 (agent apply-bridge) then the supervised Peti enable (floor + agent update first).
|
||||
- The demo host's missing §4b step-3 storage ACL grants (spike flag #2) — re-assert before slice 2
|
||||
codifies the self-grant.
|
||||
### Safety rails held
|
||||
Isolated scratch root `/srv/nas-spike2/` on 180 (backups `.bak-nasspike2`); no iptables/network
|
||||
manipulation (unreachable case = verified-unused 192.168.0.199); no guest restart; spike mounts only
|
||||
under `/mnt/felhom-drives/spike2-*`. **Teardown verified:** exports back to pre-count 2, smb.conf
|
||||
sections 6, spike users/scratch/units/creds/mountpoints all gone, 0 spike2 mounts on host and in
|
||||
guest, controller 0.112.0 healthy, spike-pulled alpine image removed.
|
||||
|
||||
**Next:** the production "NAS verify-before-commit + UI redesign" TASK is written by the operator
|
||||
side on these findings (STOP honored — no production work started).
|
||||
|
||||
Reference in New Issue
Block a user