feat(embeddednats): nkey CustomClientAuthentication against the allowlist

busauth.NewNkeyAuthenticator verifies a client's nkey signature over the
server nonce (decoding like nats-server: raw-url then std base64), maps the
nkey to its Ed25519 hex, and consults an injected IsAuthorized predicate.
Checking on every connection (rather than a static Options.Nkeys map) means
revoking a user denies its next connection with no restart. embeddednats
gains StartHostAuth(auth) and sets AlwaysEnableNonce so the server advertises
the nonce nkey clients need; Start/StartHost stay open (auth=nil) for dev.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-07 12:37:46 +02:00
parent 413dd61041
commit b09bafe242
2 changed files with 75 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
package busauth
import (
"encoding/base64"
server "github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nkeys"
)
// nkeyAuthenticator is a NATS server.Authentication that authorizes a client by
// verifying the nkey signature over the server-presented nonce and then
// consulting the bus user allowlist. Authorization is checked on every new
// connection via the injected predicate (not a static Options.Nkeys map), so
// revoking a user denies its next connection without restarting the server.
type nkeyAuthenticator struct {
// isAuthorized reports whether the lowercase-hex Ed25519 public key behind an
// nkey belongs to an active bus user. Injected (membership.Store.IsAuthorized)
// so this package stays free of the store dependency.
isAuthorized func(signPubHex string) bool
}
// NewNkeyAuthenticator builds a NATS custom authenticator backed by isAuthorized.
// Pass it to embeddednats so the data plane only accepts registered identities.
func NewNkeyAuthenticator(isAuthorized func(signPubHex string) bool) server.Authentication {
return &nkeyAuthenticator{isAuthorized: isAuthorized}
}
// Check verifies the client's nkey signature against the nonce the server
// presented, then maps the nkey to its allowlist key and checks authorization.
// Any malformed input or failed verification yields false (fail closed). The
// signature decoding mirrors nats-server's own (raw-url base64, then std base64
// fallback) so genuine clients using nats.Nkey are accepted unchanged.
func (a *nkeyAuthenticator) Check(c server.ClientAuthentication) bool {
opts := c.GetOpts()
if opts.Nkey == "" {
return false
}
sig, err := base64.RawURLEncoding.DecodeString(opts.Sig)
if err != nil {
sig, err = base64.StdEncoding.DecodeString(opts.Sig)
if err != nil {
return false
}
}
pub, err := nkeys.FromPublicKey(opts.Nkey)
if err != nil {
return false
}
if err := pub.Verify(c.GetNonce(), sig); err != nil {
return false
}
signPubHex, err := SignPubHexFromNkey(opts.Nkey)
if err != nil {
return false
}
return a.isAuthorized(signPubHex)
}
+18
View File
@@ -30,6 +30,16 @@ func Start(storeDir string, port int) (*server.Server, error) {
// to expose it to the LAN so remote peers (phones, other PCs) can connect. An
// empty host falls back to the nats-server default ("0.0.0.0", all interfaces).
func StartHost(storeDir, host string, port int) (*server.Server, error) {
return StartHostAuth(storeDir, host, port, nil)
}
// StartHostAuth is StartHost with an optional custom client authenticator. When
// auth is non-nil it is installed as Options.CustomClientAuthentication, so the
// data plane only accepts clients the authenticator approves (nkey signature +
// bus allowlist). When auth is nil the server accepts any client (the legacy,
// network-trusted behavior) — used by dev stacks and tests that have not enabled
// bus auth.
func StartHostAuth(storeDir, host string, port int, auth server.Authentication) (*server.Server, error) {
opts := &server.Options{
JetStream: true,
StoreDir: storeDir,
@@ -40,6 +50,14 @@ func StartHost(storeDir, host string, port int) (*server.Server, error) {
NoLog: true,
NoSigs: true,
}
if auth != nil {
opts.CustomClientAuthentication = auth
// A CustomClientAuthentication alone does not make the server advertise a
// nonce in its INFO line, and nats.go refuses to connect with an nkey to a
// server that does not ("nkeys not supported by the server"). Forcing the
// nonce makes nkey clients sign the challenge our authenticator verifies.
opts.AlwaysEnableNonce = true
}
ns, err := server.NewServer(opts)
if err != nil {