hub v0.44.0: PBS DR tier SLICE 1 — felhom-tenantsync surface (script+client) + hub provisioning flow (consume-once host secret, pbs_dr desired-state descriptor, fail-closed + idempotent, re-issue)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-10 20:49:48 +02:00
parent 00afadc1fe
commit ce6a56691e
19 changed files with 1743 additions and 0 deletions
+40
View File
@@ -1,5 +1,45 @@
# Felhom Hub — Changelog
## v0.44.0 — PBS DR tier SLICE 1: ep0 tenantsync surface + hub provisioning (2026-07-10)
Builds on SPIKE-pbs-tier-provisioning (00afadc). The operator ticks "PBS DR tier (ep0)" on a
customer config → the hub verifies the host's WG peer (the agent self-registers it; absence is
fail-closed) → provisions the per-customer ep0 PBS namespace + privilege-separated token over the
NEW `felhom-tenantsync` forced-command surface (peersync untouched) → stores the token secret
CONSUME-ONCE, host-scoped → serves the non-secret descriptor via the host desired-state (generation
bump). The agent apply-bridge is SLICE 2 — nothing is live-provisioned yet.
- **`scripts/felhom-tenantsync.sh` v1.0.0** (installed on ep0 per runbook §10): JSON-on-stdin/stdout;
ops `provision` (existing token = hard error `token_exists` — re-issue is explicit), `reissue`
(delete-token purges ACLs → recreate → re-grant), `fingerprint`. Dual-grant per spike §3;
own-namespace self-check with one regen retry (the spike's transient-403 note) then rollback.
Secret hygiene: the token secret rides stdout ONLY (all tool stdout → stderr; never a file/argv).
NO deprovision op — namespace/data deletion stays a deliberate, separate decision.
- **`internal/tenantsync`**: the wgsync twin — pinned host key (exact-match, constrained
HostKeyAlgorithms), per-op JSON exec, typed `ErrTokenExists`. Divergence from wgsync: error
messages NEVER embed stdout (the secret channel) — red-proof-style contract test
(`TestErrors_NeverEmbedStdout`).
- **Store**: `host_pbs_secrets` (host-scoped consume-once, the `one_time_secrets` twin) +
`SaveHostPBSSecret`/`ConsumeHostPBSSecret` (same-tx mark; re-save resets).
- **API**: `POST /api/v1/hosts/{id}/pbs/consume-token` — per-host key, self-scoped (global key =
operator recovery); 200 exactly once → 404; a foreign key's 403 does NOT burn the secret.
(Task spec wrote `/host/{id}/…`; implemented under `/hosts/` for namespace consistency with
every other agent-facing route.)
- **Web**: config-form section "PBS DR tier (ep0)" (enable + storage-id, default `felhom-pbs`
the descriptor carries the id so the slice-2 bridge is name-agnostic and the demo's
`felhom-offsite` adoption dissolves the naming collision) + provisioned line + **Re-issue PBS
credentials** (the offsite F4 precedent). `applyPBSDR` merges the non-secret `pbs_dr` descriptor
into the HOST `desired_json` (admin-set path, `SetHostDesired` bump) — ConfigJSON never carries
it. Fail-closed on: no tenantsync key, no enrolled host, no WG peer, no endpoint record,
tenantsync error, `token_exists` (message points at Re-issue). Already-provisioned re-save =
success-no-op (no re-key, no second secret, no spurious bump). Disable = descriptor
`enabled:false`, tenancy kept.
- **Deploy**: `manifests/hub.yaml` gains `TENANTSYNC_SSH_KEY_FILE` + optional `Secret/tenantsync`
mount (same endpoint addr + pinned host key as peersync, its own key).
- Red-proofs run and recorded (REPORT.md): consume-once mark drop → the secret re-serves (store +
API layers); fail-closed guard swallow → 303 half-save; idempotency short-circuit drop →
token rotation + fresh secret + spurious bump.
## v0.43.1 — Git Sync form hint: credentials are optional (2026-07-10)
Pairs with controller v0.112.0 (anonymous registry self-update). The config editor's Git Sync section