diff --git a/server/auth.go b/server/auth.go index d6e7fbfe..51945231 100644 --- a/server/auth.go +++ b/server/auth.go @@ -393,10 +393,11 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo return true } var ( - username string - password string - token string - noAuthUser string + username string + password string + token string + noAuthUser string + pinnedAcounts map[string]struct{} ) tlsMap := opts.TLSMap if c.kind == CLIENT { @@ -441,7 +442,7 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo // Check if we have trustedKeys defined in the server. If so we require a user jwt. if s.trustedKeys != nil { - if c.opts.JWT == "" { + if c.opts.JWT == _EMPTY_ { s.mu.Unlock() c.Debugf("Authentication requires a user JWT") return false @@ -460,12 +461,13 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo c.Debugf("User JWT no longer valid: %+v", vr) return false } + pinnedAcounts = opts.resolverPinnedAccounts } // Check if we have nkeys or users for client. hasNkeys := len(s.nkeys) > 0 hasUsers := len(s.users) > 0 - if hasNkeys && c.opts.Nkey != "" { + if hasNkeys && c.opts.Nkey != _EMPTY_ { nkey, ok = s.nkeys[c.opts.Nkey] if !ok || !c.connectionTypeAllowed(nkey.AllowedConnectionTypes) { s.mu.Unlock() @@ -477,17 +479,17 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo authorized := checkClientTLSCertSubject(c, func(u string, certDN *ldap.DN, _ bool) (string, bool) { // First do literal lookup using the resulting string representation // of RDNSequence as implemented by the pkix package from Go. - if u != "" { + if u != _EMPTY_ { usr, ok := s.users[u] if !ok || !c.connectionTypeAllowed(usr.AllowedConnectionTypes) { - return "", ok + return _EMPTY_, ok } user = usr return usr.Username, ok } if certDN == nil { - return "", false + return _EMPTY_, false } // Look through the accounts for a DN that is equal to the one @@ -520,13 +522,13 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo return usr.Username, true } } - return "", false + return _EMPTY_, false }) if !authorized { s.mu.Unlock() return false } - if c.opts.Username != "" { + if c.opts.Username != _EMPTY_ { s.Warnf("User %q found in connect proto, but user required from cert", c.opts.Username) } // Already checked that the client didn't send a user in connect @@ -584,9 +586,15 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo return false } issuer := juc.Issuer - if juc.IssuerAccount != "" { + if juc.IssuerAccount != _EMPTY_ { issuer = juc.IssuerAccount } + if pinnedAcounts != nil { + if _, ok := pinnedAcounts[issuer]; !ok { + c.Debugf("Account not listed as operator pinned account") + return false + } + } if acc, err = s.LookupAccount(issuer); acc == nil { c.Debugf("Account JWT lookup error: %v", err) return false @@ -617,7 +625,7 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo // FIXME: if BearerToken is only for WSS, need check for server with that port enabled if !juc.BearerToken { // Verify the signature against the nonce. - if c.opts.Sig == "" { + if c.opts.Sig == _EMPTY_ { c.Debugf("Signature missing") return false } @@ -677,7 +685,7 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo } if nkey != nil { - if c.opts.Sig == "" { + if c.opts.Sig == _EMPTY_ { c.Debugf("Signature missing") return false } @@ -715,9 +723,9 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo } if c.kind == CLIENT { - if token != "" { + if token != _EMPTY_ { return comparePasswords(token, c.opts.Token) - } else if username != "" { + } else if username != _EMPTY_ { if username != c.opts.Username { return false } diff --git a/server/jwt.go b/server/jwt.go index 478cf4f5..e7a5babb 100644 --- a/server/jwt.go +++ b/server/jwt.go @@ -144,6 +144,17 @@ func validateTrustedOperators(o *Options) error { return fmt.Errorf("trusted Keys %q are required to be a valid public operator nkey", key) } } + if len(o.resolverPinnedAccounts) > 0 { + for key := range o.resolverPinnedAccounts { + if !nkeys.IsValidPublicAccountKey(key) { + return fmt.Errorf("pinned account key %q is not a valid public account nkey", key) + } + } + // ensure the system account (belonging to the operator can always connect) + if o.SystemAccount != _EMPTY_ { + o.resolverPinnedAccounts[o.SystemAccount] = struct{}{} + } + } return nil } diff --git a/server/jwt_test.go b/server/jwt_test.go index 2c8d7c49..5e487e19 100644 --- a/server/jwt_test.go +++ b/server/jwt_test.go @@ -5742,6 +5742,65 @@ func TestJWTMappings(t *testing.T) { test("foo2", "bar2", true) } +func TestJWTOperatorPinnedAccounts(t *testing.T) { + kps, pubs, jwts := [4]nkeys.KeyPair{}, [4]string{}, [4]string{} + for i := 0; i < 4; i++ { + kps[i], pubs[i] = createKey(t) + jwts[i] = encodeClaim(t, jwt.NewAccountClaims(pubs[i]), pubs[i]) + } + sysCreds := newUser(t, kps[0]) + defer removeFile(t, sysCreds) + + dirSrv := createDir(t, "srv") + defer removeDir(t, dirSrv) + + cfgCommon := fmt.Sprintf(` + listen: -1 + operator: %s + system_account: %s + resolver: MEM + resolver_preload: { + %s:%s + %s:%s + %s:%s + %s:%s + }`, ojwt, pubs[0], pubs[0], jwts[0], pubs[1], jwts[1], pubs[2], jwts[2], pubs[3], jwts[3]) + cfgFmt := cfgCommon + ` + resolver_pinned_accounts: [%s, %s] + ` + conf := createConfFile(t, []byte(fmt.Sprintf(cfgFmt, pubs[1], pubs[2]))) + defer removeFile(t, conf) + srv, _ := RunServerWithConfig(conf) + defer srv.Shutdown() + + connectPass := func(keys ...nkeys.KeyPair) { + for _, kp := range keys { + nc, err := nats.Connect(srv.ClientURL(), createUserCreds(t, srv, kp)) + require_NoError(t, err) + defer nc.Close() + } + } + connectFail := func(key nkeys.KeyPair) { + _, err := nats.Connect(srv.ClientURL(), createUserCreds(t, srv, key)) + require_Error(t, err) + require_Contains(t, err.Error(), "Authorization Violation") + } + + connectPass(kps[0], kps[1], kps[2]) // make sure user from accounts listed work + connectFail(kps[3]) // make sure the other user does not work + // reload and test again + reloadUpdateConfig(t, srv, conf, fmt.Sprintf(cfgFmt, pubs[2], pubs[3])) + connectPass(kps[0], kps[2], kps[3]) // make sure user from accounts listed work + connectFail(kps[1]) // make sure the other user does not work + // completely disable and test again + reloadUpdateConfig(t, srv, conf, cfgCommon) + connectPass(kps[0], kps[1], kps[2], kps[3]) // make sure every account can connect + // re-enable and test again + reloadUpdateConfig(t, srv, conf, fmt.Sprintf(cfgFmt, pubs[2], pubs[3])) + connectPass(kps[0], kps[2], kps[3]) // make sure user from accounts listed work + connectFail(kps[1]) // make sure the other user does not work +} + func TestJWTNoSystemAccountButNatsResolver(t *testing.T) { dirSrv := createDir(t, "srv") defer removeDir(t, dirSrv) diff --git a/server/opts.go b/server/opts.go index 471ba675..15ffd176 100644 --- a/server/opts.go +++ b/server/opts.go @@ -290,8 +290,9 @@ type Options struct { inCmdLine map[string]bool // private fields for operator mode - operatorJWT []string - resolverPreloads map[string]string + operatorJWT []string + resolverPreloads map[string]string + resolverPinnedAccounts map[string]struct{} // private fields, used for testing gatewaysSolicitDelay time.Duration @@ -1120,8 +1121,7 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error for key, val := range mp { tk, val = unwrapValue(val, <) if jwtstr, ok := val.(string); !ok { - err := &configErr{tk, "preload map value should be a string JWT"} - *errors = append(*errors, err) + *errors = append(*errors, &configErr{tk, "preload map value should be a string JWT"}) continue } else { // Make sure this is a valid account JWT, that is a config error. @@ -1134,6 +1134,33 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error o.resolverPreloads[key] = jwtstr } } + case "resolver_pinned_accounts": + switch v := v.(type) { + case string: + o.resolverPinnedAccounts = map[string]struct{}{v: {}} + case []string: + o.resolverPinnedAccounts = make(map[string]struct{}) + for _, mv := range v { + o.resolverPinnedAccounts[mv] = struct{}{} + } + case []interface{}: + o.resolverPinnedAccounts = make(map[string]struct{}) + for _, mv := range v { + tk, mv = unwrapValue(mv, <) + if key, ok := mv.(string); ok { + o.resolverPinnedAccounts[key] = struct{}{} + } else { + err := &configErr{tk, + fmt.Sprintf("error parsing resolver_pinned_accounts: unsupported type in array %T", mv)} + *errors = append(*errors, err) + continue + } + } + default: + err := &configErr{tk, fmt.Sprintf("error parsing resolver_pinned_accounts: unsupported type %T", v)} + *errors = append(*errors, err) + return + } case "no_auth_user": o.NoAuthUser = v.(string) case "system_account", "system": diff --git a/server/opts_test.go b/server/opts_test.go index b42f999a..6955fe64 100644 --- a/server/opts_test.go +++ b/server/opts_test.go @@ -3200,3 +3200,26 @@ func TestQueuePermissions(t *testing.T) { } } + +func TestResolverPinnedAccountsFail(t *testing.T) { + cfgFmt := ` + operator: %s + resolver: URL(foo.bar) + resolver_pinned_accounts: [%s] + ` + dirSrv := createDir(t, "srv") + defer removeDir(t, dirSrv) + + conf := createConfFile(t, []byte(fmt.Sprintf(cfgFmt, ojwt, "f"))) + defer removeFile(t, conf) + srv, err := NewServer(LoadConfig(conf)) + defer srv.Shutdown() + require_Error(t, err) + require_Contains(t, err.Error(), " is not a valid public account nkey") + + conf = createConfFile(t, []byte(fmt.Sprintf(cfgFmt, ojwt, "1, x"))) + defer removeFile(t, conf) + _, err = ProcessConfigFile(conf) + require_Error(t, err) + require_Contains(t, "parsing resolver_pinned_accounts: unsupported type") +}