[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:
Matthias Hanel
2021-08-20 17:30:07 -04:00
parent 059aa5e23a
commit 0447f1c64f
5 changed files with 148 additions and 20 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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, &lt)
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, &lt)
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":

View File

@@ -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")
}