Repository navigation
gorums: authenticate peers using node TLS identities - #359
Merged
Merged
Conversation
Member
Author
I have only used 78% of my AI credits!! Please learn to compute! |
meling
force-pushed
the
feat/325/peer-identity
branch
from
October 11, 2026 20:40
f8677d9 to
4cc9887
Compare
meling
added this pull request to stack #362
October 11, 2026 20:40
meling
force-pushed
the
feat/325/peer-identity
branch
from
October 12, 2026 20:10
4cc9887 to
38665fe
Compare
meling
force-pushed
the
feat/325/peer-identity
branch
from
October 12, 2026 20:22
38665fe to
6e77169
Compare
meling
force-pushed
the
feat/325/peer-identity
branch
2 times, most recently
from
October 17, 2026 19:29
d759c2b to
022fdef
Compare
Pad the last four rows of the Makefile targets table to the column widths of the rows above them. The table content is unchanged.
An outbound channel trusted whichever server answered at the dialed address. Add PeerVerifier and OutboundOptions.Verify: when set, the channel checks the remote end of each new stream against its node ID, using the stream's context, before it sends requests on the stream. A failed check closes the stream and fails the waiting request with codes.Unauthenticated; the channel then retries as for any failed stream. Reading the stream's context commits the stream to its connection. gRPC then cannot retry the stream on a connection that nobody checked, so every request sent on the stream reaches the checked peer.
meling
force-pushed
the
feat/325/peer-identity
branch
from
October 20, 2026 23:02
e97a1e6 to
8e8d531
Compare
This was referenced Oct 20, 2026
A server attached an inbound stream to peer X whenever the stream's metadata claimed X, so any client could pose as a configured peer. NewInboundManager now takes an optional PeerVerifier. With it, AcceptPeer attaches a stream that claims a peer's node ID only after the verifier accepts the claim, and refuses claims for unknown node IDs, both with codes.Unauthenticated. The verifier runs without the manager's lock. The outbound manager hands the same verifier to the channels of the configured peers that the server dials. WithNodes now captures each node's address and, for a node that implements TLSIdentity, its expected certificate identity, so later changes to the caller's map do not affect the source. NewPeerVerifier builds the TLS verifier from these identities. It accepts a stream for a node only if the verified certificate has a URI SAN equal to the node's URI identity, or a DNS SAN equal to its DNS identity without regard to case; wildcard DNS SANs, IP SANs, and the Common Name never match. It rejects mixed and duplicate identity sets, and identities that no SAN can match: empty or wildcard names, network addresses, and text that is not printable ASCII or is neither a URI nor a DNS name.
Servers configured with WithPeers trusted the node ID that a peer claimed when it connected, and whoever answered at a peer's address. So ServerContext.SenderID was not authenticated, and under WithStreamDedup a forged claim for a lower ID could redirect the server's own calls to the impostor. A node type passed to WithNodes may now implement TLSIdentity next to NodeAddress. If any node in the WithPeers source does, the server checks every peer on each stream, in both directions, before callbacks or handlers run, and NewServer panics if the identity set is invalid. Keeping each identity with its node's address means that membership and authentication cannot drift apart. Address-only sources and NewConfig keep their unauthenticated behavior. Document the activation, the certificate and TLS settings that replicas need, and which SenderID values are authenticated.
NewLocalServers built its peer source from listener addresses only, so a local cluster could not declare TLS identities. WithLocalNodes takes a factory that NewLocalServers calls once per node, in increasing ID order, after it has allocated every listener. Each returned node must use the supplied listener address. NewLocalServers returns an error, and closes the allocated listeners, if a node is invalid or the peer configuration is rejected. Test the factory with address-only and identity-bearing nodes, including the refusals of peers that connect without TLS.
Run peer groups whose servers declare TLS identities and present certificates from a test CA, with and without stream deduplication. The tests cover peers that connect and see each other's authenticated sender IDs, a server that presents another node's certificate (refused by the dial-side and the inbound check), an impostor that claims a lower peer's ID and must not take over its stream, and a client without a certificate that is still served.
meling
force-pushed
the
feat/325/peer-identity
branch
from
October 20, 2026 23:05
8e8d531 to
b469186
Compare
meling
commented
Oct 21, 2026
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.
Closes #325.
Builds on the local-server options from #361, which is merged.
Summary
Servers configured with
WithPeerstrusted the node ID claimed in stream metadata and whoever answered at a peer's address.Any client that could reach a server could pose as a configured peer.
Under
WithStreamDedup, a forged claim for a lower ID could also redirect the server's own calls to the attacker.This PR authenticates peers using TLS identities stored with their addresses in one node map.
A node value that implements
TLSIdentity() stringautomatically enables verification when its source is passed toWithPeers.There is no separate verifier option or identity map to keep in sync.
API and Configuration
NodeAddressstays unchanged;TLSIdentityis an optional companion interface.WithNodescaptures membership, addresses, and identities together.Later changes to the map or its node values do not affect the source.
Embedding a node source preserves its authentication requirement.
NewServerpanics at startup for a missing, empty, or duplicate identity, and for an identity that no SAN can match: a wildcard DNS name, an IP orhost:portaddress, or text that is not printable ASCII or is neither a URI nor a DNS name.An empty identity never disables authentication.
WithLocalNodes(func(id ID, addr string) NodeAddress)supplies node values for local clusters after all listeners are allocated.The factory runs once per node in ID order, and each node must use its supplied listener address.
NewLocalServersreturns configuration errors and closes allocated listeners on failure.Standalone
NewConfigdoes not activate peer identity checks; conn: should NewConfig check the TLS identities of the servers it dials? #370 asks whether it should.Verification Rules
Every peer stream is checked, with and without stream deduplication:
A failed check or an unknown claimed peer ID ends the stream with
codes.Unauthenticated.A failed check closes the stream and fails the waiting request with
codes.Unauthenticated.The TLS handshake must have verified the peer's certificate against a trusted CA.
An identity with a URI scheme, such as
spiffe://relab/node/2, matches only a URI SAN in the same canonical form.Other identities match only DNS SANs, without regard to case.
Wildcard DNS SANs, IP SANs, and the Common Name do not establish peer identity.
Duplicate identities are checked using the same matching rules.
Applications must still configure TLS credentials on the server and dialer, present each replica's certificate, and verify certificates against the cluster's CA.
Each replica's certificate must support both client and server authentication.
A stream without a verified TLS certificate is refused; verification never falls back to trusting the claimed ID.
The refusal names the TLS settings to check: the dialer's certificate, the server's
ClientAuth, andInsecureSkipVerify.Sender IDs and Clients
ServerContext.SenderIDis authenticated for configured peers when TLS identities are declared.It identifies only the last hop, not the original signer of a forwarded message.
Unknown claimed peer IDs have two distinct outcomes:
SenderID0.Clients that claim no peer ID are not subject to peer identity checks.
TLS settings and server interceptors remain responsible for client authentication and authorization.
Back-channel client IDs are assigned by the server and are not authenticated peer identities.
Commits
doc: align the Makefile targets table in the developer guide(whitespace only)stream: check the dialed peer with an optional verifierconn: verify peer claims and the peers a server dialsgorums: authenticate peers whose nodes declare TLSIdentitygorums: add WithLocalNodes for local peer recordsinternal/tests/tls: test per-replica peer certificatesDocumentation and Tests
SenderIDdoc comment.WithLocalNodesand per-replica certificates: successful calls and authenticated sender IDs, wrong certificates in both directions, lower-ID impersonation under deduplication, and ordinary clients without certificates.CI lint and tests pass. Validation run locally:
Each commit builds, passes
go vet ./..., and passes the tests ofinternal/stream,internal/conn,gorumstest,internal/tests/tls, and the root package.The
examplesandbenchkittests pass on the tip of #360, which contains this branch.Known Limitation
A peer refused by the remote server resends its pending two-way calls on new streams until their contexts end.
Those calls fail with the context's error rather than
codes.Unauthenticated, and the refusal reason is available inNode.LastErr.This retry policy predates this PR and is unchanged.
Calls between peers need deadlines.
#360 fixes this: calls on a refused stream fail with the refusal, and the dialer backs off before it opens a new stream.
Follow-ups: #369 (double
gorums:prefix in configuration errors) and #370 (identity checks forNewConfig).Related discussion: the design comment on #325.