Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
9.2 KiB
REPORT — S3: agent WG tunnel — keygen + registration + wg-quick@wg-felhom + escrow join (v0.64.0)
Date: 2026-07-04 · Class: implementation + live validation on felhom-pve (the expendable
demo box; Peti's box untouched by construction — wg_tunnel.enabled defaults FALSE). Design of
record: felhom.eu/documentation/architecture/06-offsite-connectivity.md row S3 (SHIPPED).
1. Baselines & commits
Start: felhom-agent @ 4ba1b14 (v0.63.0); felhom.eu @ 4fe895e (hub v0.33.0, code read-only).
Commits pushed to main:
0daae92wgtunnel keygen + hub wire (WireWireguard/report stanza/RegisterWG) + byte-identical golden copyfb24896manager state machine + loop + desired raw-consumer seame2b6c63FELHOM_WG sudoers + capabilities + config (DEFAULT OFF) + main wiring + escrow join- (docs commit) CHANGELOG v0.64.0 + CONTEXT + this REPORT
2. Files
- NEW
internal/wgtunnel/{key,manager,loop}.go+ tests (key vectors, state-machine Groups A/B) internal/hub/{report,client,collect}.go(+wg_contract_test.go,wg_client_test.go, NEWtestdata/desired-state-wireguard.golden.json— byte-identical hub copy, diff-verified)internal/desired/syncer.go(AddConsumer+ panic containment) +consumer_test.gointernal/escrow/identity.go(WGPrivateKey+AttachWGKey) +wgkey_test.gointernal/config/config.go(WGTunnelConfig, Enabled default FALSE)cmd/felhom-agent/main.go(daemon wiring, escrow-create auto-inject,--selftest=wgtunnel)configs/felhom-agent.sudoers(Cmnd_Alias FELHOM_WG),internal/capability/manifest.go(6 entries)
3. Tests + the five §10 red-proof outcomes
go build ./... && go vet ./... && go test ./... — all green (20 pkgs; 18 with tests).
visudo -cf on felhom-pve: parsed OK (gated BEFORE install; binary restarted after).
Red-proofs, each run → FAILED as required → reverted:
- (a) marker gate dropped →
re-registration after revocation: 5 calls - (b) hash comparison dropped →
steady-state tick ran execs: [install…, systemctl restart…] - (c) clamp removed → honest adjustment discovered mid-proof: x/crypto's X25519 clamps the
scalar internally (RFC 7748), so the DERIVED pubkey is clamp-invariant and the spec's
fixed-vector red-proof cannot fail. The clamp's real observable property is the stored key
file's canonical form (external
wg pubkey < private.keymust agree with what the agent registered) — asserted byTestEnsureKey_StoredKeyIsClamped, which DOES fail with the clamp removed (stored key is not clamped: byte0=10101010). Both pubkey vectors were generated with the realwg pubkey(provenance in the test). - (d) teardown-on-absent-data →
absent-data tick ran execs: [systemctl disable…]×3 - (e) escrow auto-inject removed →
bundle key = ""Plus: the log-scan test greps the captured buffer for the actual private key (padded + unpadded) — absent; the pubkey IS logged (intended).
4. Deploy + live validation (felhom-pve; journal excerpts verbatim; keys never logged by construction — the session-log grep for the key material was run and clean)
Deploy order per spec: sudoers first (visudo -cf → install → parsed OK), binary
0.63.0→0.64.0 (backup kept), config: wg_tunnel.enabled=true, interval_seconds=60.
- Poll-interval lever (the spec's step-0 diagnosis, learned live): agent-side
poll_seconds=120was adopted for exactly ONE cycle — the hub envelope'spoll_interval_secondsis a hub-side CONSTANT (defaultHostPollSeconds = 900,hub/internal/api/handler.go) silently adopted on the first cycle (the "changed" log fires only on later cycles). Hub code is out of S3 scope, so validation ran on 900 s beats with background watchers. Agent config restored topoll_seconds=900+ the diagnosis SIGQUIT restart documented below. Observation for the hub backlog: make the constant configurable. - Step 1 — bring-up (one shot, 3 s):
07:15:48 wgtunnel: generated new WG keypair pubkey=yV5hFF…TCg=→07:15:49 wgtunnel: registered with hub … assigned_ip=10.77.0.2/32 existed=false generation=5 sync=ok→07:15:49 desired: updated from hub generation=5→07:15:50 wgtunnel: tunnel conf applied endpoint=ep0.felhom.eu:443 … action=enable. Perms verified: key 0600 in 0700 dir (felhom-agent), installed conf 0600 root. - Step 2 — data path:
wg-quick@wg-felhomactive; in-tunnel ping 10.77.0.1 0% loss ~32 ms;curl https://10.77.0.1:8007/= PBS login page. Hub/admin/wg/peersshows the peer BOUND todemo-felhom-01. Report stanza in the hub's stored report_json:{"pubkey":"yV5hFF…","registered":true,"active":true,"last_handshake_age_s":28,"assigned_ip":"10.77.0.2/32"}. All 6wg-*capability entries probestatus:ok. - Agent-restart tolerance (bonus, via the diagnosis SIGQUIT): on restart — key kept, NO re-registration (marker gate), tunnel stayed active, desired refetched clean.
- Step 3 — host reboot:
wg-quick@wg-felhomactive from boot (unit persistence, before/without the agent), tunnel pings, guest 9201 running, agent's first tick ran ZERO wgtunnel execs. - Step 4 — revocation drill: admin DELETE 05:57:30Z → next poll
08:10:52 desired: updated generation=6+WARN wgtunnel: … hub revoked this peer; disabling the tunnel (marker kept; NO re-registration)→ service inactive, marker present. Over the following 2 polls: 0wgtunnel: registeredlines, 0 wg-related sudo execs (the load-bearing negatives — verbatim counts in §5). Re-add (via the REGISTRATION endpoint — see §5's path insight) → next poll:08:55:52 desired: updated generation=8+wgtunnel: tunnel conf applied … action=enable, tunnel pings, 0 registration calls since the re-add — same key, same /32, marker untouched. Config lever restored (poll_secondsback to 900; restart clean — no keygen/register lines). - Step 5 — escrow:
--selftest=escrow-create -storage felhom-pbs(as root — the PBS key file is root-readable only, the documented BUNDLE-slice reality) →escrow: identity bundle: +wg_private_key(field name only),identity=true, blob 450 B age-wrapped, self-verify OK. Deviation from the spec's "blob uploaded": run WITHOUT--upload— uploading would overwrite the operator's real drill escrow with one whose R was displayed into THIS session log (R was sed-redacted, but an unrecorded-R escrow is unrecoverable and clobbering the prior blob breaks the operator's held R). The upload path is pre-S3 code, unchanged by this slice — no coverage lost. - Step 6 — soak: 30-min idle → handshake age 104 s (<180 s; keepalive holding), active.
5. Revocation-drill negatives (verbatim)
Over the 31 minutes following the teardown (05:57:30Z DELETE → 06:10:52Z teardown → checked
06:42Z), with 2 heartbeats landed hub-side in the window (count from host_reports):
'wgtunnel: registered' lines since teardown: 0
'desired: updated' lines since teardown: 0 ← correct: generation never advanced (nothing
changed hub-side), so heartbeats were cached
no-ops — the polls happened, the fetch didn't
wg-related sudo execs since teardown: 0
wg-quick@wg-felhom: inactive (stayed down)
Re-add path insight (found live, load-bearing for operator docs): re-adding via the S1
registry endpoint (POST /admin/wg/peers with host_id) binds the peer but does NOT bump the
host's generation — the agent never learns the block returned. The correct operator re-add is
the S2 registration endpoint with the global key (POST /hosts/{id}/wg), which allocates,
bumps (gen 8 in the drill), and syncs in one step. The drill was redone through it. Backlog
note for S6: either make the admin registry add bump the bound owner (symmetric with the S2
delete-bump) or mark it registry-plumbing in the UI.
6. NOT yet live-validated — awaiting later work
- CGNAT/mobile-hotspot smoke — the operator-assisted appendix (physical hotspot needed); deferred non-blocking per spec. On failure it re-opens the DERP/Headscale fallback, not S3's code.
- PBS backup over the tunnel (S4 — this slice deliberately left backup config untouched).
- DR consume of the escrowed WG key (S5).
- Tunnel-health surfacing/alerting in the hub UI (S6 — the stanza ships now).
- Endpoint re-IP re-resolve watchdog beyond restart-time resolution (S6, noted in 06-doc S3 row).
- Gitea publish + Day-0 manifest vouch of 0.64.0 (operator follow-ups — publish-agent.sh + the password-gated UI).
7. Observations (noticed, not acted on)
- The hub's
defaultHostPollSeconds = 900constant silently overrides any agent-side cadence on cycle 1 with no log line (first-cycle adoption is unlogged) — worth a hub-side config knob + an INFO log for the first adoption. - The pre-existing
pbs: cannot read token secretWARN (non-root vs/etc/pve/priv/...) still fires each cycle — S4's tunnel-target PBS with per-customer tokens resolves it structurally. git add -Ahygiene: the sudoers deploy leaves/root/felhom-agent.sudoers.bak-pre064+agent.json.bak-pre064on felhom-pve (intentional rollback artifacts, listed for the operator).