96abb75a2e
Closes the residual the 0004 hardening deferred: the NATS authenticator can now confine a registered peer to the subjects of the rooms it belongs to, instead of letting any registered identity sub/pub on any subject. The dynamic-membership reconnection model the audit named is provided by client.RefreshSession. pkg/busauth: - verifyNkey factors out the shared nkey verification. - NewNkeyAuthenticatorACL + PermissionsFunc: an authenticator that, after authorizing, derives and RegisterUser()s per-subject permissions. A derivation error denies the connection (fail closed). pkg/membership: - SubjectACLFor(store) maps a signing pubkey to the subjects it may use: the subject of every room it belongs to, plus the client infrastructure subjects (_INBOX.>, $JS.API.> for request/reply and the persisted plane). pkg/client: - RefreshSession() rebuilds the data-plane connection so the authenticator re-derives permissions after a membership change (NATS freezes permissions at connect time). It retains the seeds/options to reconnect; active subscriptions are dropped and must be re-made (documented). Tests (DoD: isolation + refresh): - TestSubjectACLIsolation: alice (member of room.A) may sub/pub room.A but is DENIED sub and pub on room.B (permissions violation), and never reads bob's room.B traffic; bob never receives alice's cross-room publish. - TestRefreshSessionGainsNewRoom: alice has no permission for room B until she is added and calls RefreshSession; the reconnect grants the subject and she then receives room B traffic. Scope note: the per-subject ACL authenticator is opt-in (NewServer/ membershipd keep the open authenticator by default) and is wired in with the decentralized boot path; auto-RefreshSession on every membership change (fully transparent) remains for 0003f. Master behavior unchanged.
115 lines
4.6 KiB
Go
115 lines
4.6 KiB
Go
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).
|
|
func (a *nkeyAuthenticator) Check(c server.ClientAuthentication) bool {
|
|
signPubHex, ok := verifyNkey(c)
|
|
if !ok {
|
|
return false
|
|
}
|
|
return a.isAuthorized(signPubHex)
|
|
}
|
|
|
|
// verifyNkey performs the shared nkey verification: it checks the client's
|
|
// signature against the server-presented nonce and returns the lowercase-hex
|
|
// Ed25519 public key behind the nkey. ok is false on any malformed input or
|
|
// failed verification (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 verifyNkey(c server.ClientAuthentication) (signPubHex string, ok 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 signPubHex, true
|
|
}
|
|
|
|
// PermissionsFunc maps a connecting identity (lowercase-hex Ed25519 signing key)
|
|
// to the NATS permissions it should be granted for this connection. Returning an
|
|
// error denies the connection (fail closed). It is how the data plane enforces
|
|
// per-subject access from room membership (issue 0003e, audit H4 residual).
|
|
type PermissionsFunc func(signPubHex string) (*server.Permissions, error)
|
|
|
|
// nkeyAuthenticatorACL is the nkey authenticator that ALSO scopes the connection
|
|
// to per-subject permissions derived from room membership. NATS evaluates
|
|
// permissions once, at connect time, so a peer that joins a room after
|
|
// connecting must reconnect (client.RefreshSession) to gain that room's subject
|
|
// — the dynamic-membership reconnection model the audit deferred to this issue.
|
|
type nkeyAuthenticatorACL struct {
|
|
isAuthorized func(signPubHex string) bool
|
|
perms PermissionsFunc
|
|
}
|
|
|
|
// NewNkeyAuthenticatorACL builds an authenticator that authorizes by the bus
|
|
// allowlist AND registers per-subject permissions from perms. A registered but
|
|
// permission-less peer can no longer subscribe to or publish on arbitrary
|
|
// subjects: it is confined to the subjects of the rooms it belongs to (plus the
|
|
// client infrastructure subjects perms includes). This is the per-subject ACL
|
|
// the 0004 hardening left as a residual.
|
|
func NewNkeyAuthenticatorACL(isAuthorized func(signPubHex string) bool, perms PermissionsFunc) server.Authentication {
|
|
return &nkeyAuthenticatorACL{isAuthorized: isAuthorized, perms: perms}
|
|
}
|
|
|
|
// Check verifies the nkey, authorizes against the allowlist, then derives and
|
|
// registers the connection's subject permissions. A permissions-derivation
|
|
// error denies the connection (fail closed) rather than granting open access.
|
|
func (a *nkeyAuthenticatorACL) Check(c server.ClientAuthentication) bool {
|
|
signPubHex, ok := verifyNkey(c)
|
|
if !ok {
|
|
return false
|
|
}
|
|
if !a.isAuthorized(signPubHex) {
|
|
return false
|
|
}
|
|
perms, err := a.perms(signPubHex)
|
|
if err != nil {
|
|
return false // fail closed: never grant open access on a derivation error
|
|
}
|
|
c.RegisterUser(&server.User{Permissions: perms})
|
|
return true
|
|
}
|