Repository navigation
feat: keel mesh makes a full mesh, keeps one identity, finds its endpoint - #76
Merged
Merged
Conversation
marcos-mendez
force-pushed
the
fix/mesh-full-mesh
branch
3 times, most recently
from
October 3, 2026 22:51
3968ad2 to
f5d41bf
Compare
…oint A test of keel 0.17.0 on three Keel Web nodes across two sites found four problems with keel mesh (handbook decision 0048). After the security review, members also verify admission evidence, and the members' channel is split and sandboxed the way an invite already is. Members learn of each other (0048, "Until etcd exists"). - The new node takes, as peers, the members named in the join answer whose evidence it can verify. - Once a join is confirmed, the inviter announces it over the overlay. - keel mesh sync, run by a timer at boot and every 15 minutes, pulls rosters. A fallback join runs it against the inviter. - The members' changes are applied in one change, under one 0018 window, and confirmed by a WireGuard handshake from a key added since the change. If that fails, the spec is put back and those members are left out for an hour. Only an admitted join adds a peer. - Each node has an Ed25519 signing key, made on the machine with openssl. - The inviter signs evidence of each admission: mesh identity, invite id, the new node's WireGuard and signing keys, its address and endpoint, and the time. That evidence travels in the HMAC-signed join answer and in every roster entry. - A member takes an entry only when its evidence names an invite and was signed by a key it already trusts, and chains are followed. Trusted keys are its own, its inviter's, keys of members admitted the same way, and roots set by `keel mesh create --adopt` or `keel mesh sync --adopt`. For a hand-built mesh, those --adopt commands make the spec's peers the roots. - A root's signing key is bound only from a roster the node fetched from the root's own overlay address, never from an announcement. No member, root or not, vouches for a node it did not admit. keel mesh remove removes a node before etcd, as 0048 says. - It signs a tombstone, drops the peer under the 0018 window (confirmed by a handshake from a peer it keeps), and sends its roster to the others. - A member takes a tombstone only from the node's admitter, a root, or the node itself. It then drops the peer through one window. - Tombstones are kept for good, capped at 1024 and at 64 per signer, and rosters carry them all. - Signing-key rotation is documented as follow-up work. The members' channel is split as keel#75 split invites. - The root helper, keel-mesh-members, holds the spec, the trust store and the signing key. - It starts keel-mesh-members-listen with DynamicUser, no capability and the invite listener's sandbox. The two talk over a 0600 socket in a 0700 runtime directory, checked with SO_PEERCRED against MainPID. - The listener serves TCP 51821 bound to wg0 only. It allows a 15 s deadline per request, 4 connections, 2 per source, 8 KiB of headers and 256 KiB of body, and blocks a source after repeated refusals. - Announcements wait in a bounded queue that keeps the latest one per sender. One mesh identity. Only a join or an operator's --adopt sets it. - invite refuses on a split, or when the node has peers but no identity. - keel mesh sync --adopt repairs a split. The endpoint on DHCP and SLAAC hosts. - With no static address declared, invite and join pick the uplink's stable global IPv6 address, plus a public IPv4. - They never pick ULA, deprecated or temporary addresses, or anything on wg*. The order is that of keel-core's banner picker. - With only a privacy or RFC 1918 address, invite refuses and asks for --endpoint. join, create and accept hold apply's "reverts unless keel network confirm" and print it only when their own confirmation fails. Tested at each seam with real openssl signatures and real sockets, and with the root helper and the listener in one process. The three-namespace test proves the full mesh, built on signed evidence and with sandboxed members' listeners, on clean links and at 250 ms +-25 ms with 2% loss.
marcos-mendez
force-pushed
the
fix/mesh-full-mesh
branch
from
October 3, 2026 22:58
f5d41bf to
4dae464
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the four problems a test of keel 0.17.0 found on three Keel Web nodes across two sites (decision 0048), plus the findings of the security review. keel 0.18.0. Not for merging before the security review.
1. A full mesh, on admission evidence (review: HIGH 1)
keel mesh sync, run at boot and every 15 min bykeel-mesh-sync.timer, pulls rosters. A fallback join pulls from the inviter.openssl genpkeyand kept in/var/lib/keel/mesh/node.key(0600). The inviter signs the admission: mesh identity, invite id, the new node's WireGuard and signing keys, its address and endpoint, and the time. The new node receives that evidence and the inviter's signing key in the HMAC-authenticated join (or confirm) answer.keel mesh create --adoptorkeel mesh sync --adopt(an explicit operator action) makes the spec's peers trust roots.pullfetched from the root's own overlay address, never from an announcement (2nd review: MEDIUM 1)./var/lib/keel/mesh/trust.json, not the spec: 0048 says there is nomeshsection in the spec.2. Sandbox (HIGH 2) and limits (MEDIUM 3)
The design follows keel#75:
keel-mesh-membersstartskeel-mesh-members-listenwith DynamicUser, no capabilities andLISTENER_PROPERTIES.3. Queue (MEDIUM 4)
The queue keeps the latest announcement per sender, with at most 64 senders; a full queue answers 503. One worker applies everything pending in one window.
4. Tombstones (MEDIUM 5; 2nd review: MEDIUM 3, 4, LOW 5)
keel mesh remove KEY|ADDRESSremoves a node from the mesh, as 0048 says:Each member takes the tombstone only if the signer is the node's admitter, a root, or the node itself. It then drops the peer in one window. If that change is not confirmed, the spec is put back and the next sync retries.
Tombstones are kept forever, at most 1024, and at most 64 per signer. Rosters carry them all, and sync never re-adds a tombstoned key.
Signing-key rotation is documented as follow-up work in handbook#44.
5. Identity (LOW 6, item 2)
Only a join or
--adoptsets the identity. Sync without an identity is refused.inviterefuses on a split, and when the node has peers but no identity.Repair: first
keel mesh create --adopton web-1, thenkeel mesh sync --adopt <web-1>on web-2 and on web-3. The timers then converge.6. Endpoint (LOW 7, item 3)
The endpoint is the uplink's stable global IPv6 address, plus a public IPv4. ULA, deprecated, temporary and wg* addresses are excluded; the order follows keel-core's banner picker, reimplemented in keel because the picker is shell in keel-core.
inviterefuses and asks for--endpoint.joinsends no endpoint in that case (the behind-NAT path), and says why.7. Join message (item 4)
The join prints "confirming over the overlay…". Apply's revert warning is shown only if the node's own confirmation fails.
Test plan
test_mesh_signingandtest_mesh_trust(chains, forgeries, untrusted signers, roots, tombstones);test_mesh_memberlink;test_mesh_memberd;test_mesh_sync,test_mesh_adoptandtest_mesh_remove.test_mesh_netns: A announces C to B, and B verifies the evidence A signed and confirms C. C pulls B from A, and B and C ping each other. The members listener runs under setpriv with CapEff 0. It passes on clean links and at 250 ms ±25 ms with 2% loss, three runs in a row.