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:
@@ -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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user