diff --git a/server/auth.go b/server/auth.go index bb62b884..be3903b0 100644 --- a/server/auth.go +++ b/server/auth.go @@ -296,23 +296,19 @@ func (s *Server) checkAuthentication(c *client) bool { // isClientAuthorized will check the client against the proper authorization method and data. // This could be nkey, token, or username/password based. func (s *Server) isClientAuthorized(c *client) bool { - // Snapshot server options by hand and only grab what we really need. - s.optsMu.RLock() - customClientAuthentication := s.opts.CustomClientAuthentication - authorization := s.opts.Authorization - username := s.opts.Username - password := s.opts.Password - tlsMap := s.opts.TLSMap - s.optsMu.RUnlock() + opts := s.getOpts() // Check custom auth first, then jwts, then nkeys, then // multiple users with TLS map if enabled, then token, // then single user/pass. - if customClientAuthentication != nil { - return customClientAuthentication.Check(c) + if opts.CustomClientAuthentication != nil { + return opts.CustomClientAuthentication.Check(c) } - // Grab under lock but process after. + return s.processClientOrLeafAuthentication(c) +} + +func (s *Server) processClientOrLeafAuthentication(c *client) bool { var ( nkey *NkeyUser juc *jwt.UserClaims @@ -320,6 +316,7 @@ func (s *Server) isClientAuthorized(c *client) bool { user *User ok bool err error + opts = s.getOpts() ) s.mu.Lock() @@ -364,7 +361,7 @@ func (s *Server) isClientAuthorized(c *client) bool { } } else if hasUsers { // Check if we are tls verify and are mapping users from the client_certificate - if tlsMap { + if opts.TLSMap { var euser string authorized := checkClientTLSCertSubject(c, func(u string) bool { var ok bool @@ -448,7 +445,9 @@ func (s *Server) isClientAuthorized(c *client) bool { } nkey = buildInternalNkeyUser(juc, acc) - c.RegisterNkeyUser(nkey) + if err := c.RegisterNkeyUser(nkey); err != nil { + return false + } // Generate an event if we have a system account. s.accountConnectEvent(c) @@ -481,7 +480,9 @@ func (s *Server) isClientAuthorized(c *client) bool { c.Debugf("Signature not verified") return false } - c.RegisterNkeyUser(nkey) + if err := c.RegisterNkeyUser(nkey); err != nil { + return false + } return true } @@ -497,13 +498,21 @@ func (s *Server) isClientAuthorized(c *client) bool { return ok } - if authorization != "" { - return comparePasswords(authorization, c.opts.Authorization) - } else if username != "" { - if username != c.opts.Username { - return false + if c.kind == CLIENT { + if opts.Authorization != "" { + return comparePasswords(opts.Authorization, c.opts.Authorization) + } else if opts.Username != "" { + if opts.Username != c.opts.Username { + return false + } + return comparePasswords(opts.Password, c.opts.Password) } - return comparePasswords(password, c.opts.Password) + } else if c.kind == LEAF { + // There is no required username/password to connect and + // there was no u/p in the CONNECT or none that matches the + // know users. Register the leaf connection with global account + // or the one specified in config (if provided). + return s.registerLeafWithAccount(c, opts.LeafNode.Account) } return false @@ -607,141 +616,58 @@ func (s *Server) isGatewayAuthorized(c *client) bool { return comparePasswords(opts.Gateway.Password, c.opts.Password) } -// isLeafNodeAuthorized will check for auth for an inbound leaf node connection. -func (s *Server) isLeafNodeAuthorized(c *client) bool { - // FIXME(dlc) - This is duplicated from client auth, should be able to combine - // and not fail so bad on DRY. - - // Grab under lock but process after. - var ( - juc *jwt.UserClaims - acc *Account - user *User - ok bool - err error - ) - - s.mu.Lock() - // Check if we have trustedKeys defined in the server. If so we require a user jwt. - if s.trustedKeys != nil { - if c.opts.JWT == "" { - s.mu.Unlock() - c.Debugf("Authentication requires a user JWT") - return false - } - // So we have a valid user jwt here. - juc, err = jwt.DecodeUserClaims(c.opts.JWT) +func (s *Server) registerLeafWithAccount(c *client, account string) bool { + var err error + acc := s.globalAccount() + if account != _EMPTY_ { + acc, err = s.lookupAccount(account) if err != nil { - s.mu.Unlock() - c.Debugf("User JWT not valid: %v", err) + s.Errorf("authentication of user %q failed, unable to lookup account %q: %v", + c.opts.Username, account, err) return false } - vr := jwt.CreateValidationResults() - juc.Validate(vr) - if vr.IsBlocking(true) { - s.mu.Unlock() - c.Debugf("User JWT no longer valid: %+v", vr) - return false - } - } else if s.users != nil { - if c.opts.Username != "" { - user, ok = s.users[c.opts.Username] - if !ok { - s.mu.Unlock() - return false - } - } } - s.mu.Unlock() - - // If we have a jwt and a userClaim, make sure we have the Account, etc associated. - // We need to look up the account. This will use an account resolver if one is present. - if juc != nil { - issuer := juc.Issuer - if juc.IssuerAccount != "" { - issuer = juc.IssuerAccount - } - if acc, err = s.LookupAccount(issuer); acc == nil { - c.Debugf("Account JWT lookup error: %v", err) - return false - } - if !s.isTrustedIssuer(acc.Issuer) { - c.Debugf("Account JWT not signed by trusted operator") - return false - } - if juc.IssuerAccount != "" && !acc.hasIssuer(juc.Issuer) { - c.Debugf("User JWT issuer is not known") - return false - } - if acc.IsExpired() { - c.Debugf("Account JWT has expired") - return false - } - // Verify the signature against the nonce. - if c.opts.Sig == "" { - c.Debugf("Signature missing") - return false - } - sig, err := base64.RawURLEncoding.DecodeString(c.opts.Sig) - if err != nil { - // Allow fallback to normal base64. - sig, err = base64.StdEncoding.DecodeString(c.opts.Sig) - if err != nil { - c.Debugf("Signature not valid base64") - return false - } - } - pub, err := nkeys.FromPublicKey(juc.Subject) - if err != nil { - c.Debugf("User nkey not valid: %v", err) - return false - } - if err := pub.Verify(c.nonce, sig); err != nil { - c.Debugf("Signature not verified") - return false - } - - nkey := buildInternalNkeyUser(juc, acc) - if err := c.RegisterNkeyUser(nkey); err != nil { - return false - } - - // Generate an event if we have a system account. - s.accountConnectEvent(c) - - // Check if we need to set an auth timer if the user jwt expires. - c.checkExpiration(juc.Claims()) - return true - } - - if user != nil { - ok = comparePasswords(user.Password, c.opts.Password) - // If we are authorized, register the user which will properly setup any permissions - // for pub/sub authorizations. - if ok { - c.RegisterUser(user) - // Generate an event if we have a system account and this is not the $G account. - s.accountConnectEvent(c) - } - return ok - } - - // FIXME(dlc) - Add ability to support remote account bindings via - // other auth like user or nkey and tlsMapping. - - // For now this means we are binding the leafnode to the global account. - c.registerWithAccount(s.globalAccount()) - - // Snapshot server options. - opts := s.getOpts() - - if opts.LeafNode.Username == "" { - return true - } - if opts.LeafNode.Username != c.opts.Username { + if err = c.registerWithAccount(acc); err != nil { return false } - return comparePasswords(opts.LeafNode.Password, c.opts.Password) + return true +} + +// isLeafNodeAuthorized will check for auth for an inbound leaf node connection. +func (s *Server) isLeafNodeAuthorized(c *client) bool { + opts := s.getOpts() + + isAuthorized := func(username, password, account string) bool { + if username != c.opts.Username { + return false + } + if !comparePasswords(password, c.opts.Password) { + return false + } + return s.registerLeafWithAccount(c, account) + } + + // If leafnodes config has an authorization{} stanza, this takes precedence. + // The user in CONNECT mutch match. We will bind to the account associated + // with that user (from the leafnode's authorization{} config). + if opts.LeafNode.Username != _EMPTY_ { + return isAuthorized(opts.LeafNode.Username, opts.LeafNode.Password, opts.LeafNode.Account) + } else if len(opts.LeafNode.Users) > 0 { + // This is expected to be a very small array. + for _, u := range opts.LeafNode.Users { + if u.Username == c.opts.Username { + return isAuthorized(u.Username, u.Password, u.Account.Name) + } + } + return false + } + + // We are here if we accept leafnode connections without any credential. + + // Still, if the CONNECT has some user info, we will bind to the + // user's account or to the specified default account (if provided) + // or to the global account. + return s.processClientOrLeafAuthentication(c) } // Support for bcrypt stored passwords and tokens. diff --git a/server/config_check_test.go b/server/config_check_test.go index 262d6345..191e0faf 100644 --- a/server/config_check_test.go +++ b/server/config_check_test.go @@ -1236,6 +1236,37 @@ func TestConfigCheck(t *testing.T) { errorLine: 14, errorPos: 71, }, + { + name: "mixing single and multi users in leafnode authorization", + config: ` + leafnodes { + authorization { + user: user1 + password: pwd + users = [{user: user2, password: pwd}] + } + } + `, + err: errors.New("can not have a single user/pass and a users array"), + errorLine: 3, + errorPos: 20, + }, + { + name: "dulpicate usernames in leafnode authorization", + config: ` + leafnodes { + authorization { + users = [ + {user: user, password: pwd} + {user: user, password: pwd} + ] + } + } + `, + err: errors.New(`duplicate user "user" detected in leafnode authorization`), + errorLine: 3, + errorPos: 20, + }, } checkConfig := func(config string) error { diff --git a/server/leafnode.go b/server/leafnode.go index f5c763e2..f6810096 100644 --- a/server/leafnode.go +++ b/server/leafnode.go @@ -88,6 +88,9 @@ func (s *Server) remoteLeafNodeStillValid(remote *leafNodeCfg) bool { // Ensure that leafnode is properly configured. func validateLeafNode(o *Options) error { + if err := validateLeafNodeAuthOptions(o); err != nil { + return err + } if o.LeafNode.Port == 0 { return nil } @@ -102,6 +105,26 @@ func validateLeafNode(o *Options) error { return nil } +// Used to validate user names in LeafNode configuration. +// - rejects mix of single and multiple users. +// - rejects duplicate user names. +func validateLeafNodeAuthOptions(o *Options) error { + if len(o.LeafNode.Users) == 0 { + return nil + } + if o.LeafNode.Username != _EMPTY_ { + return fmt.Errorf("can not have a single user/pass and a users array") + } + users := map[string]struct{}{} + for _, u := range o.LeafNode.Users { + if _, exists := users[u.Username]; exists { + return fmt.Errorf("duplicate user %q detected in leafnode authorization", u.Username) + } + users[u.Username] = struct{}{} + } + return nil +} + func (s *Server) reConnectToRemoteLeafNode(remote *leafNodeCfg) { delay := s.getOpts().LeafNode.ReconnectInterval select { diff --git a/server/leafnode_test.go b/server/leafnode_test.go index dfbe9109..4f92be62 100644 --- a/server/leafnode_test.go +++ b/server/leafnode_test.go @@ -22,6 +22,8 @@ import ( "sync/atomic" "testing" "time" + + "github.com/nats-io/nats.go" ) type captureLeafNodeRandomIPLogger struct { @@ -492,3 +494,233 @@ func TestLeafNodeRTT(t *testing.T) { checkRTT(t, sa) checkRTT(t, sb) } + +func TestLeafNodeValidateAuthOptions(t *testing.T) { + opts := DefaultOptions() + opts.LeafNode.Username = "user1" + opts.LeafNode.Password = "pwd" + opts.LeafNode.Users = []*User{&User{Username: "user", Password: "pwd"}} + if _, err := NewServer(opts); err == nil || !strings.Contains(err.Error(), + "can not have a single user/pass and a users array") { + t.Fatalf("Expected error about mixing single/multi users, got %v", err) + } + + // Check duplicate user names + opts.LeafNode.Username = _EMPTY_ + opts.LeafNode.Password = _EMPTY_ + opts.LeafNode.Users = append(opts.LeafNode.Users, &User{Username: "user", Password: "pwd"}) + if _, err := NewServer(opts); err == nil || !strings.Contains(err.Error(), "duplicate user") { + t.Fatalf("Expected error about duplicate user, got %v", err) + } +} + +func TestLeafNodeBasicAuthSingleton(t *testing.T) { + opts := DefaultOptions() + opts.LeafNode.Port = -1 + opts.LeafNode.Account = "unknown" + if s, err := NewServer(opts); err == nil || !strings.Contains(err.Error(), "cannot find") { + if s != nil { + s.Shutdown() + } + t.Fatalf("Expected error about account not found, got %v", err) + } + + template := ` + port: -1 + accounts: { + ACC1: { users = [{user: "user1", password: "user1"}] } + ACC2: { users = [{user: "user2", password: "user2"}] } + } + leafnodes: { + port: -1 + authorization { + %s + account: "ACC1" + } + } + ` + for iter, test := range []struct { + name string + userSpec string + lnURLCreds string + shouldFail bool + }{ + {"no user creds required and no user so binds to ACC1", "", "", false}, + {"no user creds required and pick user2 associated to ACC2", "", "user2:user2@", false}, + {"no user creds required and unknown user should fail", "", "unknown:user@", true}, + {"user creds required so binds to ACC1", "user: \"ln\"\npass: \"pwd\"", "ln:pwd@", false}, + } { + t.Run(test.name, func(t *testing.T) { + + conf := createConfFile(t, []byte(fmt.Sprintf(template, test.userSpec))) + defer os.Remove(conf) + s1, o1 := RunServerWithConfig(conf) + defer s1.Shutdown() + + // Create a sub on "foo" for account ACC1 (user user1), which is the one + // bound to the accepted LN connection. + ncACC1 := natsConnect(t, fmt.Sprintf("nats://user1:user1@%s:%d", o1.Host, o1.Port)) + defer ncACC1.Close() + sub1 := natsSubSync(t, ncACC1, "foo") + natsFlush(t, ncACC1) + + // Create a sub on "foo" for account ACC2 (user user2). This one should + // not receive any message. + ncACC2 := natsConnect(t, fmt.Sprintf("nats://user2:user2@%s:%d", o1.Host, o1.Port)) + defer ncACC2.Close() + sub2 := natsSubSync(t, ncACC2, "foo") + natsFlush(t, ncACC2) + + conf = createConfFile(t, []byte(fmt.Sprintf(` + port: -1 + leafnodes: { + remotes = [ { url: "nats-leaf://%s%s:%d" } ] + } + `, test.lnURLCreds, o1.LeafNode.Host, o1.LeafNode.Port))) + defer os.Remove(conf) + s2, _ := RunServerWithConfig(conf) + defer s2.Shutdown() + + if test.shouldFail { + // Wait a bit and ensure that there is no leaf node connection + time.Sleep(100 * time.Millisecond) + checkFor(t, time.Second, 15*time.Millisecond, func() error { + if n := s1.NumLeafNodes(); n != 0 { + return fmt.Errorf("Expected no leafnode connection, got %v", n) + } + return nil + }) + return + } + + checkLeafNodeConnected(t, s2) + + nc := natsConnect(t, s2.ClientURL()) + defer nc.Close() + natsPub(t, nc, "foo", []byte("hello")) + // If url contains known user, even when there is no credentials + // required, the connection will be bound to the user's account. + if iter == 1 { + // Should not receive on "ACC1", but should on "ACC2" + if _, err := sub1.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout { + t.Fatalf("Expected timeout error, got %v", err) + } + natsNexMsg(t, sub2, time.Second) + } else { + // Should receive on "ACC1"... + natsNexMsg(t, sub1, time.Second) + // but not received on "ACC2" since leafnode bound to account "ACC1". + if _, err := sub2.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout { + t.Fatalf("Expected timeout error, got %v", err) + } + } + }) + } +} + +func TestLeafNodeBasicAuthMultiple(t *testing.T) { + conf := createConfFile(t, []byte(` + port: -1 + accounts: { + S1ACC1: { users = [{user: "user1", password: "user1"}] } + S1ACC2: { users = [{user: "user2", password: "user2"}] } + } + leafnodes: { + port: -1 + authorization { + users = [ + {user: "ln1", password: "ln1", account: "S1ACC1"} + {user: "ln2", password: "ln2", account: "S1ACC2"} + ] + } + } + `)) + defer os.Remove(conf) + s1, o1 := RunServerWithConfig(conf) + defer s1.Shutdown() + + // Make sure that we reject a LN connection if user does not match + conf = createConfFile(t, []byte(fmt.Sprintf(` + port: -1 + leafnodes: { + remotes = [{url: "nats-leaf://wron:user@%s:%d"}] + } + `, o1.LeafNode.Host, o1.LeafNode.Port))) + defer os.Remove(conf) + s2, _ := RunServerWithConfig(conf) + defer s2.Shutdown() + // Give a chance for s2 to attempt to connect and make sure that s1 + // did not register a LN connection. + time.Sleep(100 * time.Millisecond) + if n := s1.NumLeafNodes(); n != 0 { + t.Fatalf("Expected no leafnode connection, got %v", n) + } + s2.Shutdown() + + ncACC1 := natsConnect(t, fmt.Sprintf("nats://user1:user1@%s:%d", o1.Host, o1.Port)) + defer ncACC1.Close() + sub1 := natsSubSync(t, ncACC1, "foo") + natsFlush(t, ncACC1) + + ncACC2 := natsConnect(t, fmt.Sprintf("nats://user2:user2@%s:%d", o1.Host, o1.Port)) + defer ncACC2.Close() + sub2 := natsSubSync(t, ncACC2, "foo") + natsFlush(t, ncACC2) + + // We will start s2 with 2 LN connections that should bind local account S2ACC1 + // to account S1ACC1 and S2ACC2 to account S1ACC2 on s1. + conf = createConfFile(t, []byte(fmt.Sprintf(` + port: -1 + accounts { + S2ACC1 { users = [{user: "user1", password: "user1"}] } + S2ACC2 { users = [{user: "user2", password: "user2"}] } + } + leafnodes: { + remotes = [ + { + url: "nats-leaf://ln1:ln1@%s:%d" + account: "S2ACC1" + } + { + url: "nats-leaf://ln2:ln2@%s:%d" + account: "S2ACC2" + } + ] + } + `, o1.LeafNode.Host, o1.LeafNode.Port, o1.LeafNode.Host, o1.LeafNode.Port))) + defer os.Remove(conf) + s2, o2 := RunServerWithConfig(conf) + defer s2.Shutdown() + + checkFor(t, 5*time.Second, 100*time.Millisecond, func() error { + if nln := s2.NumLeafNodes(); nln != 2 { + return fmt.Errorf("Expected 2 connected leafnodes for server %q, got %d", s2.ID(), nln) + } + return nil + }) + + // Create a user connection on s2 that binds to S2ACC1 (use user1). + nc1 := natsConnect(t, fmt.Sprintf("nats://user1:user1@%s:%d", o2.Host, o2.Port)) + defer nc1.Close() + + // Create an user connection on s2 that binds to S2ACC2 (use user2). + nc2 := natsConnect(t, fmt.Sprintf("nats://user2:user2@%s:%d", o2.Host, o2.Port)) + defer nc2.Close() + + // Now if a message is published from nc1, sub1 should receive it since + // their account are bound together. + natsPub(t, nc1, "foo", []byte("hello")) + natsNexMsg(t, sub1, time.Second) + // But sub2 should not receive it since different account. + if _, err := sub2.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout { + t.Fatalf("Expected timeout error, got %v", err) + } + + // Now use nc2 (S2ACC2) to publish + natsPub(t, nc2, "foo", []byte("hello")) + // Expect sub2 to receive and sub1 not to. + natsNexMsg(t, sub2, time.Second) + if _, err := sub1.NextMsg(100 * time.Millisecond); err != nats.ErrTimeout { + t.Fatalf("Expected timeout error, got %v", err) + } +} diff --git a/server/opts.go b/server/opts.go index 17bf5649..1192b8c9 100644 --- a/server/opts.go +++ b/server/opts.go @@ -107,6 +107,8 @@ type LeafNodeOpts struct { Port int `json:"port,omitempty"` Username string `json:"-"` Password string `json:"-"` + Account string `json:"-"` + Users []*User `json:"-"` AuthTimeout float64 `json:"auth_timeout,omitempty"` TLSConfig *tls.Config `json:"-"` TLSTimeout float64 `json:"tls_timeout,omitempty"` @@ -288,6 +290,7 @@ type authorization struct { user string pass string token string + acc string // Multiple Nkeys/Users nkeys []*NkeyUser users []*User @@ -1039,20 +1042,21 @@ func parseLeafNodes(v interface{}, opts *Options, errors *[]error, warnings *[]e case "host", "net": opts.LeafNode.Host = mv.(string) case "authorization": - auth, err := parseAuthorization(tk, opts, errors, warnings) + auth, err := parseLeafAuthorization(tk, errors, warnings) if err != nil { *errors = append(*errors, err) continue } - if auth.users != nil { - err := &configErr{tk, fmt.Sprintf("Leafnode authorization does not allow multiple users")} - *errors = append(*errors, err) - continue - } opts.LeafNode.Username = auth.user opts.LeafNode.Password = auth.pass opts.LeafNode.AuthTimeout = auth.timeout - + opts.LeafNode.Account = auth.acc + opts.LeafNode.Users = auth.users + // Validate user info config for leafnode authorization + if err := validateLeafNodeAuthOptions(opts); err != nil { + *errors = append(*errors, &configErr{tk, err.Error()}) + continue + } case "remotes": // Parse the remote options here. remotes, err := parseRemoteLeafNodes(mv, errors, warnings) @@ -1095,6 +1099,114 @@ func parseLeafNodes(v interface{}, opts *Options, errors *[]error, warnings *[]e return nil } +// This is the authorization parser adapter for the leafnode's +// authorization config. +func parseLeafAuthorization(v interface{}, errors *[]error, warnings *[]error) (*authorization, error) { + var ( + am map[string]interface{} + tk token + auth = &authorization{} + ) + _, v = unwrapValue(v) + am = v.(map[string]interface{}) + for mk, mv := range am { + tk, mv = unwrapValue(mv) + switch strings.ToLower(mk) { + case "user", "username": + auth.user = mv.(string) + case "pass", "password": + auth.pass = mv.(string) + case "timeout": + at := float64(1) + switch mv := mv.(type) { + case int64: + at = float64(mv) + case float64: + at = mv + } + auth.timeout = at + case "users": + users, err := parseLeafUsers(tk, errors, warnings) + if err != nil { + *errors = append(*errors, err) + continue + } + auth.users = users + case "account": + auth.acc = mv.(string) + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: mk, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + } + continue + } + } + return auth, nil +} + +// This is a trimmed down version of parseUsers that is adapted +// for the users possibly defined in the authorization{} section +// of leafnodes {}. +func parseLeafUsers(mv interface{}, errors *[]error, warnings *[]error) ([]*User, error) { + var ( + tk token + users = []*User{} + ) + tk, mv = unwrapValue(mv) + // Make sure we have an array + uv, ok := mv.([]interface{}) + if !ok { + return nil, &configErr{tk, fmt.Sprintf("Expected users field to be an array, got %v", mv)} + } + for _, u := range uv { + tk, u = unwrapValue(u) + // Check its a map/struct + um, ok := u.(map[string]interface{}) + if !ok { + err := &configErr{tk, fmt.Sprintf("Expected user entry to be a map/struct, got %v", u)} + *errors = append(*errors, err) + continue + } + user := &User{} + for k, v := range um { + tk, v = unwrapValue(v) + switch strings.ToLower(k) { + case "user", "username": + user.Username = v.(string) + case "pass", "password": + user.Password = v.(string) + case "account": + // We really want to save just the account name here, but + // the User object is *Account. So we create an account object + // but it won't be registered anywhere. The server will just + // use opts.LeafNode.Users[].Account.Name. Alternatively + // we need to create internal objects to store u/p and account + // name and have a server structure to hold that. + user.Account = NewAccount(v.(string)) + default: + if !tk.IsUsedVariable() { + err := &unknownConfigFieldErr{ + field: k, + configErr: configErr{ + token: tk, + }, + } + *errors = append(*errors, err) + continue + } + } + } + users = append(users, user) + } + return users, nil +} + func parseRemoteLeafNodes(v interface{}, errors *[]error, warnings *[]error) ([]*RemoteLeafOpts, error) { tk, v := unwrapValue(v) ra, ok := v.([]interface{}) diff --git a/server/server.go b/server/server.go index 2c949615..22aad78b 100644 --- a/server/server.go +++ b/server/server.go @@ -305,9 +305,29 @@ func NewServer(opts *Options) (*Server, error) { return nil, err } - // In local config mode, if remote leafs are configured, - // make sure that if they reference local accounts, they exist. - if len(opts.TrustedOperators) == 0 && len(opts.LeafNode.Remotes) > 0 { + // In local config mode, check that leafnode configuration + // refers to account that exist. + if len(opts.TrustedOperators) == 0 { + checkAccountExists := func(accName string) error { + if accName == _EMPTY_ { + return nil + } + if _, ok := s.accounts.Load(accName); !ok { + return fmt.Errorf("cannot find account %q specified in leafnode authorization", accName) + } + return nil + } + if err := checkAccountExists(opts.LeafNode.Account); err != nil { + return nil, err + } + for _, lu := range opts.LeafNode.Users { + if lu.Account == nil { + continue + } + if err := checkAccountExists(lu.Account.Name); err != nil { + return nil, err + } + } for _, r := range opts.LeafNode.Remotes { if r.LocalAccount == _EMPTY_ { continue