docs(audit): no-mercy full-system campaign report (2026-07-06)
Overnight unattended stress/regression on the demo node (9201). Platform resilience + safety strong: all fault-recovery + reboot/hard-reset recovery passed (58-126s; sysrq hard reset 103s), 2.5h soak 0 breaks, DR restore proven sound. Raw 25 FAIL triaged: ~10 collateral of one app finding (F1 vaultwarden SMTP crash-loop), ~8 harness mis-scores of correct product refusals (single-flight 409, needs-confirmation), rest harness-calibration incl. the P9 volid-format bug (DR mechanism itself works - recovered 9201 manually from R1). Genuine findings: F1 vaultwarden SMTP_HOST (HIGH), F2 decommission agent-403 (MED), F3 gitea-outage sync recovery (MED). No production code changed; demo left running+healthy at R1, notifications restored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
# CAMPAIGN — "No-mercy" unattended full-system test (2026-07-06)
|
||||
|
||||
> Unattended pilot-readiness stress + regression campaign on the demo node
|
||||
> (felhom-pve / guest 9201), run overnight while the operator slept. **No production code
|
||||
> changed.** Throwaway harness + evidence bundle live on DooPlex
|
||||
> (`~/campaign-nomercy-2026-07-05/`), never committed. This document is the one sanctioned
|
||||
> artifact.
|
||||
>
|
||||
> **Headline:** the platform's resilience and safety came out **strong** — every fault-injection
|
||||
> and crash-recovery scenario passed, the destroy-and-restore DR path is proven sound, the soak ran
|
||||
> 2.5 h with zero invariant breaks, and every safety gate held. The raw FAIL tally (25) is
|
||||
> **inflated and misleading**: ~10 FAILs are collateral of a *single* real app finding
|
||||
> (vaultwarden crash-loop), ~8 are the product **correctly refusing** an operation that the harness
|
||||
> then mis-scored, and the rest are harness-calibration bugs (incl. the one that made the P9 finale
|
||||
> read FAIL even though the restore itself works). The genuine product findings are a short list,
|
||||
> all MEDIUM-or-below, plus one HIGH-interest app-config bug.
|
||||
|
||||
## Run header
|
||||
|
||||
| Item | Value |
|
||||
|---|---|
|
||||
| Date run | 2026-07-06, ~07:40–12:13 CEST (P0 supervised 07:40; P1–P10 detached 08:09–12:13) |
|
||||
| Host | felhom-pve (192.168.0.162), node demo-felhom |
|
||||
| Guest | 9201 (demo-felhom), LAN 192.168.0.127 |
|
||||
| Controller / Agent / Hub | **0.100.0 / 0.72.0 / 0.35.0** (== baselines; git heads af7ea0b / 26664d6 / 61f4898) |
|
||||
| Auth mode | DISABLED by deployed config (no dashboard password → CSRF exempt); T-AUTH → XFAIL |
|
||||
| Runner | on DooPlex (survives the host reboots/hard-reset); detached `setsid nohup`; journal-resumable; each test in an isolated subshell |
|
||||
| RNG seed (soak) | 20260706 |
|
||||
| Restore points | R0 `ct/9201/2026-07-06T05:40:07Z` (verify ok), R0.5 (P2), **R1 `ct/9201/2026-07-06T06:47:23Z` (verify ok)** |
|
||||
|
||||
## End state (operator's first-coffee summary)
|
||||
|
||||
- **Guest 9201 was destroyed and restored** during the finale. The automated restore hit a harness
|
||||
bug (below); **CC recovered it manually from R1** — 9201 is **running**, controller **healthy**,
|
||||
all three USB drives **re-bound**, and the full app stack is back. State is as of **R1**
|
||||
(2026-07-06 06:47 UTC); anything written to guest volumes during P7/P8 is expected-lost (by DR
|
||||
design — restore returns to snapshot state).
|
||||
- **Only unhealthy container: `vaultwarden`** (crash-looping — see Finding F1). Everything else is
|
||||
healthy: nextcloud, navidrome, gitea, filebrowser, cloudflared, paperless, mealie, calcom,
|
||||
rallly, sparkyfitness, uptime-kuma, actualbudget, felhom-controller.
|
||||
- **Notifications restored** to the original 11 events (email nagyfenyvesi.viktor@gmail.com,
|
||||
cooldown 6 h) and verified. They were muted for the run and P10 could not un-mute (controller was
|
||||
down mid-finale); CC restored them manually.
|
||||
- **No chaos residue:** 0 leftover auto-revert timers, no disk fillers, no stray iptables DROP
|
||||
rules, USB drives re-authorized (device letters re-enumerated sdb↔sdd after the unplug tests —
|
||||
cosmetic; drives mount by stable name).
|
||||
- **Password status:** unchanged (auth remained disabled per the operator's kickoff choice).
|
||||
|
||||
## Result tally (raw, then triaged)
|
||||
|
||||
| Status | Raw count |
|
||||
|---|---|
|
||||
| PASS | 24 |
|
||||
| FAIL | 25 |
|
||||
| XFAIL | 2 |
|
||||
| SKIP | 4 |
|
||||
|
||||
**After triage the 25 FAILs decompose as:** ~10 *collateral* of Finding F1 (a single crash-looping
|
||||
app failing health-embedded assertions), ~8 *harness-calibration* mis-scores (the product behaved
|
||||
correctly), 3–4 *genuine findings*, and the P9/P10 pair caused by one harness bug (DR restore is
|
||||
sound). Details below; the matrix marks each FAIL's class.
|
||||
|
||||
---
|
||||
|
||||
## Genuine findings (product — evidence packages, hypotheses labelled)
|
||||
|
||||
### F1 — `vaultwarden` crash-loops: app-email shim sets `SMTP_FROM` without `SMTP_HOST` — **HIGH interest**
|
||||
- **Symptom:** vaultwarden container in `Restarting` throughout. Container log (verbatim):
|
||||
*"Error loading config: Both SMTP_HOST and SMTP_FROM need to be set for email support without USE_SENDMAIL"*.
|
||||
- **Repro:** deploy vaultwarden with `app_email.enabled=true` in settings (the demo's state). The
|
||||
mailrelay/app-email integration injects `SMTP_FROM=…@felhom.eu` into the compose env but not
|
||||
`SMTP_HOST` → vaultwarden refuses to start and crash-loops.
|
||||
- **Blast radius (why the FAIL count looks bad):** vaultwarden is the highest-stakes app and is
|
||||
embedded in many assertions, so its crash *collateral-failed*: T-DEPLOY-SET, T-CANARY-PLANT,
|
||||
T-LIFECYCLE, T-REMOVE-REDEPLOY, T-BAK-FULL, T-RST-VW/TIER2 (skipped — no vw canary), T-FLT-DOCKERD,
|
||||
and all four T-RBT-* reboot tests (their `assert_stack` waits on vaultwarden reaching health).
|
||||
- **Hypothesis (labelled):** the app-email SMTP shim (`internal/mailrelay`, catalog vaultwarden
|
||||
template) should inject **both** `SMTP_HOST` (the relay endpoint) and `SMTP_FROM`, or vaultwarden's
|
||||
template should gate SMTP on both being present. Compare with other SMTP apps — likely affects any
|
||||
app whose image hard-requires `SMTP_HOST` when `SMTP_FROM` is set. Cross-check against
|
||||
`SPIKE-smtp-app-relay-2026-06-28.md`.
|
||||
- **Not fixed** (report-only). This is the single most valuable finding of the run.
|
||||
|
||||
### F2 — `decommission (anyway)` on a user-data drive: agent returns HTTP 403 — **MEDIUM, verify**
|
||||
- **Symptom:** `POST /api/storage/decommission {mode:"anyway"}` → controller 502 wrapping
|
||||
`agentapi: POST /disks/decommission: HTTP 403`.
|
||||
- **Question to resolve:** is the agent's 403 **correct gating** (decommission is operator-signed /
|
||||
role-protected even for user-data) or a **real gap** (the customer-facing "decommission anyway"
|
||||
flow can't complete)? The storage-authz redesign makes system/backup operator-only, but this was a
|
||||
**user-data** drive, so a 403 here may be an over-broad gate. Evidence:
|
||||
`evidence/T-STG-DECOM-ANYWAY/decom_anyway.txt`.
|
||||
- **Note:** `T-STG-DECOM-MIGRATE` (409) is *not* this — it was a correct backup-in-progress refusal
|
||||
(see harness note H3).
|
||||
|
||||
### F3 — Gitea-outage: catalog sync does not recover cleanly after connectivity is restored — **MEDIUM, verify**
|
||||
- **Symptom:** with the Gitea IP blocked from the guest, `POST /api/sync` failed (expected/graceful,
|
||||
controller stayed up — good); **after** unblocking, the follow-up sync did not return 2xx within
|
||||
the test window. Evidence: `evidence/T-FLT-GITEA-OUT/sync_after.txt`.
|
||||
- **Hypothesis (labelled):** either a sync backoff/cooldown longer than the test's post-unblock
|
||||
wait, or a cached failure state not cleared on recovery. Contrast with **T-FLT-HUB-OUT which
|
||||
PASSED** (hub report resumed after unblock). Worth confirming whether catalog sync self-heals on a
|
||||
longer horizon.
|
||||
|
||||
### F4 — DB restore returned HTTP 000 — **LOW, likely transient/collateral**
|
||||
- `T-RST-DB` (paperless DB restore) recorded HTTP 000 (connection failed) at that instant — most
|
||||
likely a transient controller unavailability during the restore suite rather than a defect.
|
||||
Re-run in the targeted P1–P3 re-run (below) to confirm.
|
||||
|
||||
---
|
||||
|
||||
## Strong positive results (real signal — do not lose these under the FAIL count)
|
||||
|
||||
### Resilience / fault recovery — **6/6 PASS**, with measured recovery times
|
||||
| Scenario | Result | Time-to-recover |
|
||||
|---|---|---|
|
||||
| Controller `kill -9` (real SIGKILL of the process, mid-backup) | PASS — restart policy recovered, API 200 | ~ (container-up) |
|
||||
| Guest hard `pct stop` mid-backup (power-loss) | PASS — journal resume, canaries intact | 44 s |
|
||||
| Agent `kill -9` on host | PASS — systemd restart, clean binds | 6 s |
|
||||
| App crash-loop (DB dependency knocked over) | PASS — detected + alert + recovered on repair | 45 s |
|
||||
| Cloudflared down → external down → recover | PASS — external tunnel returned | (self-heal) |
|
||||
| Hub outage (iptables block, auto-revert) | PASS — controller kept serving, report resumed | — |
|
||||
| dockerd restart reconverge | 320 s | — |
|
||||
|
||||
### Host reboot storms — **recovery WORKED** (the T-RBT FAILs are collateral of F1)
|
||||
The `T-RBT-*` tests are marked FAIL only because `assert_stack` waits on vaultwarden (F1) reaching
|
||||
health. The **host + controller + apps actually recovered every time**, proven by the recorded
|
||||
times: **clean reboot 126 s, reboot mid-backup 69 s, reboot mid-migration 58 s, and a true
|
||||
`sysrq-b` HARD power-loss 103 s.** Filesystems came back clean; the demo node survived a hard reset
|
||||
and self-restored in under two minutes.
|
||||
|
||||
### Safety gates — **all held**
|
||||
- `T-STG-SEC-FORMAT` PASS: type-to-confirm enforced, non-candidate refused, and the **`/dev/sda`
|
||||
root-disk fence correctly refused** (the `FENCE`/FAIL journal line is that self-test *tripping the
|
||||
fence on purpose* — it is proof the fence works, not a failure).
|
||||
- `T-SEC-PROTECTED` PASS: traefik/cloudflared/felhom-controller/filebrowser refuse stop/remove and
|
||||
stay Up.
|
||||
- `T-BAK-SINGLEFLIGHT` PASS: second backup correctly 409'd.
|
||||
- `T-RST-NEG` PASS: bad restore ids/stacks refused cleanly with **zero writes** under target paths.
|
||||
- Backups never touched any namespace/group but `ct/9201` (`T-BAK-PBS` PASS).
|
||||
|
||||
### Stability & DR
|
||||
- **Soak: 76 ops over ~2.5 h, 0 invariant breaks** (seed 20260706) — controller stayed < 5 s
|
||||
responsive throughout, no unexpected 5xx, no state-model divergence, no RSS leak signal.
|
||||
- **CPU soak: 20 min stress-ng, worst page latency 172 ms** (< 5 s target).
|
||||
- **DR destroy-and-restore is proven sound.** The PBS whole-guest restore mechanism, transparent
|
||||
decryption, 3-volume layout, drive re-bind, and full app recovery all work — demonstrated by the
|
||||
successful manual recovery from R1. The finale's automated FAIL was purely the harness bug H1.
|
||||
|
||||
---
|
||||
|
||||
## Harness bugs found (fix before any re-run — some already fixed in the workspace copy)
|
||||
|
||||
| # | Bug | Effect | Status |
|
||||
|---|---|---|---|
|
||||
| H1 | P9 built the `pct restore` volid from the **epoch** backup-time, not the ISO-8601 form → `unable to parse PBS volume name`. Destroy succeeded, restore couldn't parse, node left down. | Caused T-P9-DESTROY-RESTORE FAIL and, downstream, T-P10-NOTIF-RESTORE FAIL (controller down during P10). **DR mechanism itself is fine.** | **FIXED** (epoch→ISO in the snapshot pointer) |
|
||||
| H2 | `assert_stack` / dockerd / reboot assertions hard-depend on a single app (vaultwarden) reaching health. | Turned F1 into ~10 collateral FAILs. | Fix: assert controller+agent+binds+tunnel; treat per-app health as informational. |
|
||||
| H3 | Storage lifecycle tests don't serialize around backups. | MIGRATE-ALL/APP + DECOM-MIGRATE got **correct** 409 "backup in progress" but were scored FAIL. | Fix: wait for `/api/backup/status` idle before storage ops. |
|
||||
| H4 | Enrollment test didn't handle the already-enrolled/mounted precondition. | T-STG-ENROLL scored FAIL on a **correct** 409 needs-confirmation. | Fix: pre-clean or expect the confirmation branch. |
|
||||
| H5 | Deploy-values builder sent `DOMAIN=<random secret>` (DOMAIN is auto-filled by the controller). | Latent — did **not** cause the vw crash (F1 did), but would misconfigure DOMAIN. | Fix: never send DOMAIN. |
|
||||
| H6 | `T-PAGE-ALL` treated `/debug` as must-be-200. | FAIL on `/debug`→404, which is **by-design** when debug mode is off (12/12 real pages 200). | Reclassify XFAIL. |
|
||||
| H7 | `T-RES-MEMGUARD` expected the 384 MB mem-request guard to fire, but the guest had ample free RAM (avail 10.2 GB) so it correctly didn't. | FAIL on absence of a refusal that wasn't warranted. | Fix: deploy enough to actually cross the reserve, or assert the accounting only. |
|
||||
| H8 | Journal notes with embedded newlines split a TSV row (one stray blank line). | Cosmetic. | Fix: strip newlines in `journal_set` notes. |
|
||||
|
||||
**Recommended follow-up:** with H1–H5 fixed and F1 worked around (set `SMTP_HOST` or disable
|
||||
app-email for the restore centrepiece), **re-run P1–P3 targeted** to get clean, real backup/restore
|
||||
(F3-volume-tar / C2 Tier-2) coverage on a healthy app set — this is the one piece of pilot-critical
|
||||
coverage the vaultwarden crash denied us tonight.
|
||||
|
||||
---
|
||||
|
||||
## Test matrix (id | class | result | note)
|
||||
|
||||
| Test | Verdict | Note |
|
||||
|---|---|---|
|
||||
| T-P0-INVENTORY / VERSIONS / R0 / NOTIF / SMOKE | **PASS** | preflight clean; R0 verified ok |
|
||||
| T-PAGE-ALL | FAIL → **harness (H6)** | only `/debug`→404 (by-design); 12/12 real pages 200 |
|
||||
| T-DEPLOY-SET | FAIL → **collateral F1** | 6/7 apps healthy; vaultwarden crash-loop |
|
||||
| T-CANARY-PLANT / T-LIFECYCLE / T-REMOVE-REDEPLOY | FAIL → **collateral F1** | vaultwarden-dependent |
|
||||
| T-SYNC-IDEMPOTENT | **PASS** | zero writes on re-sync |
|
||||
| T-SEC-PROTECTED | **PASS** | protected stacks refuse + stay Up |
|
||||
| T-AUTH-1 / T-AUTH-2 | **XFAIL** | auth disabled by deployed config (known) |
|
||||
| T-BAK-API / T-BAK-SINGLEFLIGHT / T-BAK-PBS | **PASS** | API truthful; single-flight 409; group-scoped |
|
||||
| T-BAK-FULL | FAIL → **collateral F1** | vaultwarden volume tar absent (app never ran) |
|
||||
| T-BAK-OFFBOX | SKIP | needs provisioned SFTP key (unattended) |
|
||||
| T-RST-NEG | **PASS** | bad restores refused, zero writes |
|
||||
| T-RST-DB | FAIL → **F4 (LOW)** | HTTP 000 (likely transient) |
|
||||
| T-RST-VW / T-RST-TIER2 | SKIP → **collateral F1** | no vaultwarden canary (app crashed) |
|
||||
| T-STG-SEC-FORMAT | **PASS** | type-to-confirm + `/dev/sda` fence + mkfs-never-ran |
|
||||
| T-STG-RECOMMISSION | **PASS** | drive recommissioned, apps recoverable |
|
||||
| T-STG-ENROLL | FAIL → **harness (H4)** | correct 409 needs-confirmation (device mounted) |
|
||||
| T-STG-MIGRATE-ALL / -APP / -NEG, T-STG-DECOM-MIGRATE | FAIL → **harness (H3)** | correct 409 "backup in progress" |
|
||||
| T-STG-DECOM-ANYWAY | FAIL → **F2 (MED)** | agent `POST /disks/decommission` 403 — verify |
|
||||
| T-STG-UNPLUG / T-STG-FULL | FAIL → **mixed** | partial; re-run after H3 |
|
||||
| T-STG-NET | SKIP | needs provisioned NFS/SMB export |
|
||||
| T-FLT-KILL9 / POWERLOSS-GUEST / AGENT / CRASHLOOP / CF / HUB-OUT | **PASS** | full fault recovery (times above) |
|
||||
| T-FLT-DOCKERD | FAIL → **collateral F1** | reconverge saw vw=restarting |
|
||||
| T-FLT-GITEA-OUT | FAIL → **F3 (MED)** | sync didn't recover in-window post-unblock |
|
||||
| T-RBT-CLEAN / MIDBACKUP / MIDMIGRATION / HARD | FAIL → **collateral F1 / H2** | host recovered 58–126 s (hard reset 103 s) |
|
||||
| T-RES-CPU | **PASS** | 20 min soak, latency 172 ms |
|
||||
| T-RES-MEMGUARD | FAIL → **harness (H7)** | ample RAM; guard correctly didn't fire |
|
||||
| T-SOAK-LOOP | **PASS** | 76 ops, 0 invariant breaks |
|
||||
| T-P9-DESTROY-RESTORE | FAIL → **harness (H1)** | volid format; **DR proven via manual recovery** |
|
||||
| T-P10-NOTIF-RESTORE | FAIL → **consequence of H1** | controller down during P10; **restored manually** |
|
||||
| T-P10-CLEANUP / ENDSTATE / REPORT | **PASS** | no residue; end state captured |
|
||||
|
||||
## What this campaign does NOT prove
|
||||
- **Offsite escrow restore with the operator-held recovery code R** — cannot be unattended.
|
||||
- **True-CGNAT WG tunnel behaviour** — no CGNAT vantage from the LAN runner.
|
||||
- **Clean backup/restore of the highest-stakes app** — denied tonight by F1; recover via the
|
||||
targeted P1–P3 re-run.
|
||||
- **Non-technical-user UX** — no human-in-the-loop signal.
|
||||
- Any assertion reading a best-effort endpoint (alert/monitoring HTML scrape, hub-report timestamp)
|
||||
is corroborating, not authoritative.
|
||||
|
||||
## Not changed
|
||||
No production code, versions, or configuration were changed. The single change is this document.
|
||||
The demo node was destroyed and restored (authorized) and left **running and healthy** at R1 state,
|
||||
with notifications restored. Evidence bundle retained on DooPlex, never committed; no secrets in
|
||||
this report.
|
||||
Reference in New Issue
Block a user