mirror of
https://github.com/gogrlx/nats-server.git
synced 2026-04-02 03:38:42 -07:00
[added] operator option to ensure user are signed by certain accounts
option name: resolver_pinned_accounts Contains a list of public account nkeys. Connecting user of leaf nodes need to be signed by this. The system account will always be able to connect. Signed-off-by: Matthias Hanel <mh@synadia.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user