Peering: trust store + admin API (#101) #119

Merged
Cordy merged 7 commits from feat/peer-registry into main 2026-08-04 23:48:25 +00:00
Owner

First build of the v0.5 peering track, per the decided design (IMPLEMENTED-PEERING.md). No gRPC yet — the transfer planes are #102, so the dependency tripwire (#118) stays untouched.

internal/peering — the two lists from the diagram, in an atomic JSON file (same pattern as holds/settings/app-passwords, no DB):

  • Issued keys: CAIRN-PEER1.<base32>, returned once, stored as an argon2id verifier only. Directed key pairs, so compromising this instance yields verifiers rather than credentials and a stolen state file cannot impersonate a peer to anyone else.
  • Peers: local alias, URL, the key they issued us (necessarily in the clear — we present it), and the sender allow-list.
  • CanSend implements #100 Q6: per-peering allow-lists of users and IdP groups, evaluated against the caller's live groups (#96). Fails closed on an empty list, an unknown peer, or a nil user — and admin status is deliberately not a pass: operating an instance is not the same as being authorized to move its data off it.

Admin API (/api/v1/admin/peering/…) — issue/list/revoke keys, register/list/remove peers, gated with the same pattern as the other admin surfaces. The peer's key is never echoed back by listPeers; issued secrets appear exactly once, at creation. URL validation refuses plain http unless the host is private/loopback, mirroring the Basic-auth-is-LAN-only stance. Audit events: peering-key-issue, peering-key-revoke, peering-peer-add, peering-peer-remove.

Config: peering.statePath. Empty (the default) means the feature is entirely off — no routes registered, no file created.

Built test-first. Nine tests written and watched fail (FAIL … [build failed], every symbol undefined) before any implementation existed, then implemented to green: key hashed-and-once, wrong/foreign key rejected, revocation stops verification, registration round-trips through a reopened file, removal, fail-closed on empty allow-list and for admins, allow by username, allow by group, unknown peer, duplicate name rejected. Full repo suite green, go vet clean.

Not in scope here (deliberately, per the issue): key-id lookup for inbound requests, the gRPC control plane, capability tokens, the Settings UI panel. #102 next.

First build of the v0.5 peering track, per the decided design (`IMPLEMENTED-PEERING.md`). **No gRPC yet** — the transfer planes are #102, so the dependency tripwire (#118) stays untouched. **`internal/peering`** — the two lists from the diagram, in an atomic JSON file (same pattern as holds/settings/app-passwords, no DB): - **Issued keys**: `CAIRN-PEER1.<base32>`, returned once, stored as an argon2id **verifier only**. Directed key pairs, so compromising this instance yields verifiers rather than credentials and a stolen state file cannot impersonate a peer to anyone else. - **Peers**: local alias, URL, the key *they* issued us (necessarily in the clear — we present it), and the sender allow-list. - **`CanSend`** implements #100 Q6: per-peering allow-lists of users **and** IdP groups, evaluated against the caller's live groups (#96). Fails closed on an empty list, an unknown peer, or a nil user — and **admin status is deliberately not a pass**: operating an instance is not the same as being authorized to move its data off it. **Admin API** (`/api/v1/admin/peering/…`) — issue/list/revoke keys, register/list/remove peers, gated with the same pattern as the other admin surfaces. The peer's key is never echoed back by `listPeers`; issued secrets appear exactly once, at creation. URL validation refuses plain `http` unless the host is private/loopback, mirroring the Basic-auth-is-LAN-only stance. Audit events: `peering-key-issue`, `peering-key-revoke`, `peering-peer-add`, `peering-peer-remove`. **Config**: `peering.statePath`. Empty (the default) means the feature is entirely off — no routes registered, no file created. **Built test-first.** Nine tests written and watched fail (`FAIL … [build failed]`, every symbol undefined) before any implementation existed, then implemented to green: key hashed-and-once, wrong/foreign key rejected, revocation stops verification, registration round-trips through a reopened file, removal, fail-closed on empty allow-list *and* for admins, allow by username, allow by group, unknown peer, duplicate name rejected. Full repo suite green, `go vet` clean. **Not in scope here** (deliberately, per the issue): key-id lookup for inbound requests, the gRPC control plane, capability tokens, the Settings UI panel. #102 next.
Cordy merged commit 7f2031d9a5 into main 2026-08-04 23:48:25 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Cordy/Cairn#119
No description provided.