docs: SPIKE — offsite-backup connectivity: plain WireGuard wins the ladder (transport decision made)
P0-P7 probe ladder on real ends (demo-felhom PVE host <-> throwaway Hetzner). Verdict: GO at H1 — plain host-side WG, box-dials-out to a small public endpoint; Headscale is a separable fleet layer, not a traversal necessity. Line is plain-NAT (not CGNAT) with zero IPv6 — recorded honestly; CGNAT vantage deferred to Peti VM 110. Real 2 GiB worst-case PBS backup over the tunnel at the full home uplink, pin intact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
@@ -4,76 +4,69 @@
|
||||
|
||||
---
|
||||
|
||||
# REPORT — `felhom-app-catalog` skill (4th) + SparkyFitness finalized as its worked example (2026-07-03)
|
||||
# REPORT — SPIKE: offsite-backup connectivity (WireGuard, home-NAT traversal) (2026-07-03)
|
||||
|
||||
Companion half in `app-catalog-felhom.eu/REPORT.md` (the template changes + live-deploy evidence).
|
||||
Docs-only run; evidence doc: `documentation/audits/SPIKE-connectivity-wireguard-2026-07-03.md`.
|
||||
No production code, no Headscale on the real hub, no agent change, no persistent felhom-pve config.
|
||||
|
||||
## Baselines
|
||||
## Verdict
|
||||
|
||||
| Repo | Base → head |
|
||||
|---|---|
|
||||
| felhom.eu | `7ad111c` → this push (`skills/felhom-app-catalog/SKILL.md` NEW + docs) |
|
||||
| app-catalog-felhom.eu | `15cdf7b` → `c7490f7` (sparkyfitness finalized + REUSE.md probe-naming row) |
|
||||
| felhom-controller | READ-ONLY (sync.go + healthprobe.go consulted to state consumer behavior accurately) |
|
||||
**GO at the lowest rung (H1): plain WireGuard, host-side, box-dials-out to a small public
|
||||
endpoint.** No coordinator, no relay, no mesh. Headscale (P5: 0.29.2 stood up throwaway) is a
|
||||
*separable fleet-management layer* — TLS control plane + DB + tailscaled-per-box + Tailscale-hosted
|
||||
DERP by default — not a traversal necessity for hub-and-spoke backup.
|
||||
|
||||
## The skill
|
||||
## Probe ladder results (both ends real: demo-felhom PVE host ⟷ throwaway Hetzner CX23)
|
||||
|
||||
- **Workflow, not reference:** research → inspect-the-image healthcheck choice → compose →
|
||||
`.felhom.yml` → live UI deploy on demo → verify healthy → reconcile count → finish. Every field
|
||||
table is a POINTER to app-catalog `REUSE.md` §1–2 / `README.md` §format — the shipped SKILL.md
|
||||
contains no field tables at all (checked); the one new fact it teaches (probe-container naming)
|
||||
was placed in REUSE.md and is referenced from the skill.
|
||||
- **Installer:** no change needed — `install_skills.py` auto-discovers `skills/*/SKILL.md` dirs
|
||||
(line 69). Ran it: `felhom-app-catalog junction -> E:\git\felhom.eu\skills\felhom-app-catalog`.
|
||||
- **Fresh-session discovery:** `claude -p "List your custom skills…"` → all 4 listed
|
||||
(`felhom-app-catalog`, `felhom-build-deploy`, `felhom-testing`, `felhom-ui-design`).
|
||||
- **P0** — the operator's line is **plain single-NAT with a fixed public IPv4**
|
||||
(`37.191.56.193`, catv.fixed.one.hu, AS21334), *not* CGNAT (no 100.64/10 hop; source IP at the
|
||||
server = egress IP). **Zero IPv6** (no address, no egress, 15-min RA capture: 0 packets).
|
||||
- **P1 (H0 v6-direct)** — N/A on this line; keep the production endpoint dual-stack anyway.
|
||||
- **P2 (H1)** — plain WG v4 up on the **first try**; RTT ~30–40 ms; **11.4-min fully-idle hold**
|
||||
(keepalive-only, NAT mapping unmoved, instant ping after); PBS 4.2.2 API through the tunnel with
|
||||
the **exact-cert pin intact** (`pin.go` model) + negative wrong-pin proof; `:8007` tunnel-only
|
||||
by firewall (public probe: unreachable).
|
||||
- **P3** — uplink is the bottleneck: direct 42 Mbit/s up / 200–230 down; tunnel upload ~39 Mbit/s
|
||||
(~5% overhead); PBS TLS benchmark through tunnel **4.94 MB/s = the full uplink**; **real 2 GiB
|
||||
worst-case (urandom) backup: 4.26 MiB/s sustained, 8 min, zero stalls**. First backup 50–100 GB
|
||||
≈ 3–6 h (overnight window). Oddity recorded: tunnel *download* (565 Mbit/s, 0 retrans) beat
|
||||
direct TCP (230, 285 retrans) — UDP encapsulation dodges whatever shapes inbound TCP.
|
||||
- **P4 (H2)** — outbound UDP fine on **51820 and 443**; no TCP/443 fallback machinery needed.
|
||||
- **P6** — plain-WG isolation is topological: per-peer `/32` AllowedIPs + forwarding off at the
|
||||
endpoint; only the PBS port admitted from the WG interface.
|
||||
- **P7** — hypothesis **refuted with evidence**: kernel WG link creation **succeeds** inside the
|
||||
unprivileged guest (no `/dev/net/tun` needed — that's confirmed absent and only blocks
|
||||
tailscaled). Host placement therefore stands on architecture: backups originate host-side, the
|
||||
tunnel must survive guest death (cloudflared precedent), the guest keeps zero infra creds.
|
||||
|
||||
## Two verified facts the exercise added to the knowledge base
|
||||
## Architecture recommendation (input to the next task — the production spec)
|
||||
|
||||
1. **Probe-container naming (REUSE.md row, new):** the controller-side `healthcheck.checks[]`
|
||||
probe dials the container named EXACTLY like the stack dir; fallback = first running
|
||||
prefix-match — in a multi-container stack that can be the DB. Convention: exposed service's
|
||||
`container_name` = stack name (vaultwarden/rallly already comply; sparkyfitness now does).
|
||||
2. **BusyBox `ash` `command -v` takes ONE argument** — `command -v wget curl node python3`
|
||||
reported "wget only" in both SparkyFitness images while per-tool runs showed curl (and node in
|
||||
the server) present. The skill's inspect command is a per-tool loop for this reason.
|
||||
Host-side agent-managed WG service (cloudflared pattern); per-box keypair minted at install →
|
||||
pubkey to hub, private key 0600 in agent state dir + **joins the escrowed IdentityBundle** for DR;
|
||||
endpoint = small public dual-stack VM (€4–8/mo class), two shapes deferred to the spec: PBS-on-VM
|
||||
vs **VM-as-rendezvous-relay with DooPlex PBS dialing out too** (both ends outbound-only,
|
||||
ciphertext-only transit — also solves DooPlex's no-public-UDP). Trust model preserved: box only
|
||||
dials out; operator-inbound (B) and game-ingress (C) explicitly out of scope.
|
||||
|
||||
## SparkyFitness — markers resolved by inspection (no more VERIFY-BEFORE-FINALIZE)
|
||||
## Honest caveats
|
||||
|
||||
| Image (both tags RESOLVE) | wget | curl | node | python3 | Family chosen |
|
||||
|---|---|---|---|---|---|
|
||||
| `codewithcj/sparkyfitness:v0.17.2` (nginx SPA) | /usr/bin/wget | /usr/bin/curl | no | no | BusyBox-wget `--spider :80/` (kept — most common family) |
|
||||
| `codewithcj/sparkyfitness_server:v0.17.2` (Node) | /usr/bin/wget | /usr/bin/curl | /usr/local/bin/node v24.17.0 | no | node-exec HTTP GET `:3010/api/health` status<400 (real status check, kept) |
|
||||
CGNAT traversal is argued from mechanism, **not proven on this line** (it's plain-NAT); second-ISP
|
||||
confirmation deferred to Peti's VM 110 (currently unreachable). Single ISP/evening; no multi-day
|
||||
hold; no home-IP-rotation event observed; Headscale assessed server-side only (no tailscaled on
|
||||
the production host); no restore-direction PBS run.
|
||||
|
||||
`/api/health` existence proven live: `{"status":"UP"}`. `mem_limit` arithmetic verified:
|
||||
512 (db) + 1024 (server) + 256 (frontend) = **1792M** = the `.felhom.yml` value (comment added).
|
||||
## Cleanup (verified)
|
||||
|
||||
## Live validation (method stated per claim)
|
||||
demo-felhom: interface/keys/config/dataset gone (keys **shredded**), iperf3 purged, WG module
|
||||
unloaded, `/etc/wireguard` untouched-empty, guest probe link deleted — residue-free. Hetzner
|
||||
throwaway: WG + headscale + PBS torn down, secrets shredded, datastore deleted, box **powered
|
||||
off** (parked for the Peti follow-up or destruction; jarrs.eu box never touched).
|
||||
|
||||
SparkyFitness was ALREADY deployed on demo from the draft (4 h prior, three containers healthy) —
|
||||
so the live action was applying the FINALIZED template: "Sablonok frissítése" then the card's
|
||||
"Frissítés", both clicked in the real dashboard via the browser bridge (claude-in-chrome).
|
||||
Compose recreated only the renamed frontend (`sparkyfitness`, healthy in 21 s); server + db
|
||||
untouched (Up 4 h) → the two `data_key` secrets were NOT regenerated, as required. Verifications:
|
||||
git-sync file content, `docker ps`, `/api/health`, traefik Host-header curl (200), and
|
||||
`GET /api/stacks/sparkyfitness` (the endpoint the UI polls) — controller probe verdict
|
||||
`healthy: true, http :80/ → 200 (6ms)` against the renamed container. Kept deployed (7.3 option A;
|
||||
expendable demo box, real catalog app).
|
||||
## Repo changes (docs-only)
|
||||
|
||||
## App-count reconciliation (recorded, NOT fixed)
|
||||
|
||||
Real catalog: **53** templates. Website `gyik.html` claims "több mint 45" (true but lagging);
|
||||
`alkalmazasok.html` per-category `app-count` spans sum **49** ≠ 53. Website fixes are
|
||||
`felhom-ui-design` + `site_gates.py` territory — separate task.
|
||||
|
||||
## Observations (not acted on)
|
||||
|
||||
- The controller UI card and `docker ps` agreed throughout; the probe's DEBUG-level per-stack log
|
||||
lines are invisible at INFO — the `GET /api/stacks/<name>` endpoint is the reliable verdict
|
||||
source for validation.
|
||||
- `paperless-ngx`'s containers (`paperless-webserver`…) match NEITHER the exact-name nor the
|
||||
prefix fallback of its stack name `paperless-ngx` — its controller-side probe likely never runs
|
||||
(silently "skipped no container"). Candidate follow-up: rename or add a probe-container hint;
|
||||
NOT touched here.
|
||||
- The pre-rename fallback DID probe the right container on demo (probe was green before the
|
||||
update) — the rename converts order-dependent luck into the deterministic convention.
|
||||
- NEW `documentation/audits/SPIKE-connectivity-wireguard-2026-07-03.md` (P0–P7 verbatim + twelve
|
||||
§7 answers + recommendation).
|
||||
- `documentation/runbooks/day0-install.md` — the "LAN-only until Headscale/WireGuard lands"
|
||||
backlog line now points at the spike (decision made; spec is the remaining gate).
|
||||
- `CONTEXT.md` — transport decision recorded; next task = production connectivity spec.
|
||||
- `scripts/CHANGELOG.md` — docs entry.
|
||||
|
||||
Reference in New Issue
Block a user