docs: S2 shipped — hub v0.33.0 CHANGELOG, peersync v1.0.1, 06 §3.2 wire shape + S2 status, CONTEXT, REPORT

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-04 01:20:10 +02:00
parent 99246c3c7d
commit 265f46c0e7
5 changed files with 164 additions and 82 deletions
+82 -76
View File
@@ -4,99 +4,105 @@
---
# REPORT — S1: offsite endpoint provisioning + hub→endpoint WG peer-sync (2026-07-04)
# REPORT — S2: box-facing WG registration + `wireguard` desired-state block + /offsite UI (2026-07-04)
Doc-06 roadmap row S1, shipped and live-validated end-to-end. Hub v0.31.0**v0.32.1** live on
k3s; dev endpoint (`felhom-hetzner`, 167.233.158.164) provisioned by the new runbook. The S1
design point is RESOLVED: **peer-sync = hub pushes over SSH** (pinned host key, forced-command
script) — pull/signed-manifest and an HTTPS push API rejected per the operator-locked decision.
Doc-06 roadmap row S2 shipped: hub v0.32.1**v0.33.0** live on k3s; peersync script v1.0.0 →
**v1.0.1** redeployed to the dev endpoint. The agent was NOT touched; the S3 contract travels
via the new golden only.
## 1. Baselines
Start: felhom.eu @ `7fb20d5` (hub v0.31.0), felhom-agent @ `4ba1b14` (untouched, read-only).
Start: felhom.eu @ `740cc42` (hub v0.32.1, peersync v1.0.0), felhom-agent @ `4ba1b14` (v0.63.0,
read-only). Live baseline captured before validation: `demo-felhom-01` at generation **2**,
desired-state `{"guests":[]}`, WG registry empty, guest 9201 running (controller 0.98.3
healthy), agent v0.63.0, poll interval 900 s.
## 2. Files created/modified
- `hub/internal/store/store.go` (migration: `wg_endpoints` + `wg_peers`), NEW `hub/internal/store/wg.go` + `wg_test.go`
- NEW `hub/internal/wgsync/{client,reconciler}.go` + tests (incl. in-process SSH server)
- NEW `hub/internal/api/wg.go` + `wg_test.go`; `hub/internal/api/handler.go` (5 routing cases + `wgSyncer` field)
- `hub/cmd/hub/main.go` (env wiring, disabled-INFO pattern)
- NEW `scripts/felhom-peersync.sh` (v1.0.0, shellcheck-clean on 180)
- `manifests/hub.yaml` (image 0.32.1, `WG_ENDPOINT_SSH_*` env, optional Secret mount `wg-endpoint-ssh`)
- NEW `documentation/runbooks/offsite-endpoint.md` (executed live; live-run corrections folded in)
- `hub/CHANGELOG.md`, `scripts/CHANGELOG.md`, `documentation/architecture/06-offsite-connectivity.md` (S1 row → SHIPPED), `CONTEXT.md`, this REPORT
- `hub/internal/store/store.go` (partial unique index `idx_wg_peers_host`; `BumpHostDesired`),
`hub/internal/store/wg.go` (+`allocateWGPeerTx` extraction, `RegisterWGPeerForHost`,
`GetWGPeerForHost`, `WGPeer.CreatedAt`), `wg_test.go` (Group A)
- `hub/internal/api/wg.go` (`handleRegisterHostWG`, `mergeWireguard`, delete-bumps-owner),
`handler.go` (routing case; merge call in `handleGetDesiredState`; `wireguard`-key rejection
in `handleAdminSetDesiredState`), `wg_test.go` (Group B), NEW
`testdata/desired-state-wireguard.golden.json` (**S3 cross-repo contract**)
- `hub/internal/web/offsite.go` + `templates/offsite.html` + `offsite_test.go` (Group C);
`server.go` routing; Offsite nav link in all 9 page templates
- `scripts/felhom-peersync.sh` v1.0.1 (exit-swallow fix)
- `manifests/hub.yaml` (image 0.33.0), `hub/CHANGELOG.md`, `scripts/CHANGELOG.md`,
`documentation/architecture/06-offsite-connectivity.md` (§3.2 concrete wire shape + S2 row
SHIPPED), `CONTEXT.md`, this REPORT
## 3. Commits (all pushed to `main`)
## 3. Commits (pushed to `main`)
1. `b18f6ae` store: tables + /32 allocator + Group-A tests
2. `fbeeacb` wgsync + /admin/wg API + main wiring + Groups B/C/D tests
3. `6f5fb19` peersync script + manifest surface
4. `0fa7ea1` **live-found fix**: constrain `HostKeyAlgorithms` to the pinned key type + regression test
5. `5e1933b` manifest → 0.32.1
6. `d2cc023` runbook (executed + corrected)
7. (this commit) CHANGELOGs + CONTEXT + 06 S1 status + REPORT
1. `fcf84a0` store: host binding + index + BumpHostDesired
2. `ba52005` API: registration + merge-at-read + hub-owned-key guard + golden
3. `13203c2` web: /offsite page + nav
4. (script commit) peersync v1.0.1 + `5e...` manifest 0.33.0 bump
5. (this commit) docs
## 4. Tests + red-proofs
## 4. Tests + the five §10 red-proofs
`cd hub && go build ./... && go vet ./... && go test ./...`**all green** (store/api/wgsync +
all pre-existing packages). Test count: 15 new test functions across 4 new test files
(`wg_test.go` ×2, `client_test.go`, `reconciler_test.go`); pre-existing tests untouched.
`cd hub && go build ./... && go vet ./... && go test ./...` — all green (12 packages).
New: 6 store tests (Group A), 5 API tests (Group B), 2 web render tests (Group C).
`TestDesiredStateGolden_RoundTripsThroughHub` passes **UNMODIFIED**; the S1 suites pass
unmodified through the allocator extraction.
All four §10 companion red-proofs run → **failed as required** → reverted → green:
- (a) allocator ignores existing rows → 3 tests fail (duplicate-IP surfaced by the UNIQUE backstop)
- (b) `isGlobal` gate dropped on POST → per-host-key 403 test fails ("sync ran despite 403")
- (c) `FixedHostKey``InsecureIgnoreHostKey` → wrong-host-key test fails ("the pin is dead")
- (d) full-list → delta-only push → removed-peer/full-list test fails ("surviving peer missing")
- (bonus) the new multi-hostkey regression test fails with the `HostKeyAlgorithms` fix removed
(that IS the live bug reproduced in-process).
Red-proofs, each run → FAILED as required → reverted:
- (a) self-scope check dropped → `cross-host register = 200, want 403`
- (b) merge made unconditional → caught by the no-peer-serves-no-block assertion
(`GET serves a wireguard block that was never legitimately registered`); the golden test
itself survived only via the fail-safe unmerged path (endpoint record absent in its setup) —
the pass-through property is covered either way
- (c) `wireguard`-key rejection dropped → `PUT with wireguard key = 200, want 400`
- (d) bump-on-idempotent → both negatives fail (`gen=2 want 1`, `push count 2 want 1`)
- (e) script exit-swallow (sandbox, stub `wg-quick` exit-1-after-partial + recording stub `wg`):
pre-fix shape invoked `wg` with rc=0 despite the strip failure; fixed shape errors first,
`wg` never invoked
## 5. Deployed + live validation (verbatim, secrets redacted)
## 5. Deploy + live validation (real pipeline, secrets redacted)
- Rollout: `deployment "hub" successfully rolled out`, image `felhom-hub:0.32.1`, log:
`[INFO] WG peer-sync enabled (endpoint 167.233.158.164:22, user felhom-peersync)`.
- **Live bug found by validation:** first push failed `ssh: host key mismatch` — stock sshd holds
multiple host keys and default negotiation presented ECDSA against our ed25519 pin. Fixed
(v0.32.1) + regression-tested. The pin itself was proven working by the very failure.
- Scenario A live: POST peer → `{"assigned_ip":"10.77.0.2/32","sync":"ok"}` → endpoint
`wg show wg0` lists exactly that pubkey/IP; conf persisted; `[Interface]` section byte-identical.
- Scenario B live: DELETE → `{"status":"ok","sync":"ok"}` → 0 peers in `wg show` AND conf;
repeat DELETE → 404 (no sync); wrong bearer → 403.
- Script negatives on the box: malformed JSON / bad pubkey / own-IP peer → exit 1 each,
`wg show` + conf hash **byte-identical** before/after.
- Reboot: wg0 active on 443 with persisted set, nftables drop policies up, PBS active; next hub
push converges (`sync:"ok"`).
- Tunnel smoke (after operator created DNS): client on demo-felhom via `ep0.felhom.eu:443`
ping 10.77.0.1 (0% loss, ~38 ms) → `curl https://10.77.0.1:8007/` returns the **PBS login
page** (wg0-only 8007 rule works) → public `https://167.233.158.164:8007` **unreachable**
(P2b-style re-proof).
- **Incident (handled):** the first WG server private key leaked into the session log via
`wg show wg0 dump | cut` (field 1 of the interface line IS the private key). Rotated
immediately (new keypair + conf rebuild + hub re-registration, zero-peer window); warning
added to the runbook. No key material in any committed file.
- hub 0.33.0 rolled out via ArgoCD (`deployment "hub" successfully rolled out`, image
`felhom-hub:0.33.0`, `[INFO] WG peer-sync enabled`); peersync v1.0.1 on the endpoint
(`head` shows the v1.0.1 header), shellcheck-clean on 180.
- **Registration through the REAL per-host auth path** (curl run ON felhom-pve; its key never
left the box; throwaway keypair minted + private key shredded unused):
`{"assigned_ip":"10.77.0.2/32","existed":false,"generation":3,"sync":"ok"}` — gen 2→3.
- Served desired-state carried exactly the golden-shaped block (endpoint coords + pubkey +
assigned_ip) alongside the untouched `guests` key; endpoint `wg show wg0` listed the pubkey
with `10.77.0.2/32`.
- **Old-agent tolerance (the S3 fleet-safety gate), verbatim journal:**
`time=2026-07-04T01:04:29.267+02:00 level=INFO msg="desired: updated from hub" generation=3 guests=0`
— the v0.63.0 typed parse dropped the unknown key; guest count unchanged; NO error/warn from
the desired/reconcile path (the only WARN in the window is the pre-existing, documented
`pbs: cannot read token secret` non-root permission issue — unrelated to S2).
Guest-state re-capture = byte-identical to baseline (9201 running, controller 0.98.3 healthy).
- **Removal:** DELETE → `{"status":"ok","sync":"ok"}` → generation 4; desired-state back to the
pure operator blob (`{"guests":[]}`, no wireguard key); registry `{"peers":[]}`; endpoint
`wg show` 0 peers. Gen-4 clean fetch by the agent, verbatim:
`time=2026-07-04T01:19:29.228+02:00 level=INFO msg="desired: updated from hub" generation=4 guests=0`
— no errors; the box is fully back to baseline except the two expected generation increments.
- `/offsite` route wired + auth-gated live (302 → /login); page rendering proven by Group C
unit tests — the live view is operator-password-gated (eyeball on next login).
## 6. demo-felhom cleanup assertion (spike-§6 style)
**Generation accounting for demo-felhom-01:** baseline 2 → 3 (registration) → 4 (removal) —
exactly the two expected increments; desired_json blob never modified (byte-identity asserted
in tests; live blob returned to the identical `{"guests":[]}`).
`wgs1` interface down+gone ("Device does not exist"); throwaway client key + conf **shredded**;
no `s1-test`/`wgs1` files in /tmp; `/etc/wireguard/` empty, mtime Apr 2025 (untouched);
`wireguard` module unloaded. Hub registry empty (`{"peers":[]}`); endpoint `wg show` 0 peers.
The only persistent state anywhere: the endpoint box itself (intended — it IS the deliverable),
the hub DB endpoint row, and `Secret/wg-endpoint-ssh` (key exists only there; local copy shredded).
## 6. NOT yet live-validated — awaiting S3+
## 7. NOT yet live-validated — awaiting later slices
- Box-facing registration + `wireguard` block on `WireDesiredState` + generation bumps + hub UI (**S2**).
- Agent-managed `wg-felhom` on a real box + the CGNAT/mobile-hotspot smoke (**S3**).
- A real PBS **backup** over the tunnel into a customer namespace (**S4** — the smoke only proved
API reachability through the tunnel).
- Agent keygen/registration/`wg-felhom` service + IdentityBundle escrow join + CGNAT/mobile
smoke (**S3**, risky/supervised).
- A real PBS backup over the tunnel into a namespace (**S4**).
- DR re-establishment consuming the escrowed WG key (**S5**).
- UI mutations + tunnel-health columns (**S3/S6**).
## 8. Observations (noticed, not acted on)
## 7. Observations (noticed, not acted on)
- The endpoint's **AAAA record is wrong**: `2a01:4f8:1c16:7aa1::` (subnet zero address) instead
of `…7aa1::1` — v4 works; a v6-preferring client would fail. **Operator: fix the AAAA.**
- `~/build/felhom-hub/build.sh` does its own source sync (epilogue references
`~/git/felhom.eu`); its epilogue still suggests `kubectl set image` — contradicts the GitOps
rule (cosmetic, build script only).
- Home-resolver DNS propagation lagged public DNS by ~2 min (first `wg-quick up` failed to
resolve; retry succeeded) — noted in the runbook.
- Website renders no hub version — N/A for this release (grep confirmed).
- The agent's 900 s poll makes desired-state validation slow-loop (two ≤16-min waits in this
run) — S3 test plans should budget for it or temporarily shorten the interval on the test box.
- `hub-config`'s `report_api_key` doubles as the only global admin key; a scoped operator token
would narrow the blast radius of the endpoint-mutation surface (pre-existing, S6-adjacent).
- The pre-existing `pbs: cannot read token secret` WARN on felhom-pve (non-root agent vs
`/etc/pve/priv/storage/felhom-pbs.pw`) fires every cycle — known BUNDLE-slice issue, gets
resolved naturally when S4 points PBS at the tunnel with per-customer tokens.