Skip to content

Commit

Permalink
(#987) anon tls requires JWTs
Browse files Browse the repository at this point in the history
This arranges things that Anonymous TLS brokers require a JWT passed during the connection,
this is the same JWT that the AAA Signer signs.

It then verifies the JWT using the public certificate that signed it, checks validity etc and extracts
the callerid

This then create allow rules allowing subscribe only to *.reply.md5(caller).> ensuring that a user
with a JWT can only subscribe to reply subjects for his own user.

The connector and Message is modified to publish to this same reply subject pattern.

This effectively works around the lack of NATS private reply subjects and prevent random clients
connecting to an anonymous TLS server from reading replies or requests.

Signed-off-by: R.I.Pienaar <[email protected]>
  • Loading branch information
ripienaar committed Sep 12, 2020
1 parent ba746f3 commit f7932e2
Show file tree
Hide file tree
Showing 14 changed files with 600 additions and 65 deletions.
53 changes: 30 additions & 23 deletions CONFIGURATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,29 +59,30 @@ A few special types are defined, the rest map to standard Go types
|[plugin.choria.require_client_filter](#pluginchoriarequire_client_filter)|[plugin.choria.security.certname_whitelist](#pluginchoriasecuritycertname_whitelist)|
|[plugin.choria.security.privileged_users](#pluginchoriasecurityprivileged_users)|[plugin.choria.security.request_signer.token_environment](#pluginchoriasecurityrequest_signertoken_environment)|
|[plugin.choria.security.request_signer.token_file](#pluginchoriasecurityrequest_signertoken_file)|[plugin.choria.security.request_signer.url](#pluginchoriasecurityrequest_signerurl)|
|[plugin.choria.security.serializer](#pluginchoriasecurityserializer)|[plugin.choria.server.provision](#pluginchoriaserverprovision)|
|[plugin.choria.srv_domain](#pluginchoriasrv_domain)|[plugin.choria.ssldir](#pluginchoriassldir)|
|[plugin.choria.stats_address](#pluginchoriastats_address)|[plugin.choria.stats_port](#pluginchoriastats_port)|
|[plugin.choria.status_file_path](#pluginchoriastatus_file_path)|[plugin.choria.status_update_interval](#pluginchoriastatus_update_interval)|
|[plugin.choria.use_srv](#pluginchoriause_srv)|[plugin.nats.credentials](#pluginnatscredentials)|
|[plugin.nats.ngs](#pluginnatsngs)|[plugin.nats.pass](#pluginnatspass)|
|[plugin.nats.user](#pluginnatsuser)|[plugin.scout.overrides](#pluginscoutoverrides)|
|[plugin.scout.tags](#pluginscouttags)|[plugin.security.always_overwrite_cache](#pluginsecurityalways_overwrite_cache)|
|[plugin.security.certmanager.alt_names](#pluginsecuritycertmanageralt_names)|[plugin.security.certmanager.issuer](#pluginsecuritycertmanagerissuer)|
|[plugin.security.certmanager.namespace](#pluginsecuritycertmanagernamespace)|[plugin.security.certmanager.replace](#pluginsecuritycertmanagerreplace)|
|[plugin.security.cipher_suites](#pluginsecuritycipher_suites)|[plugin.security.client_anon_tls](#pluginsecurityclient_anon_tls)|
|[plugin.security.ecc_curves](#pluginsecurityecc_curves)|[plugin.security.file.ca](#pluginsecurityfileca)|
|[plugin.security.file.cache](#pluginsecurityfilecache)|[plugin.security.file.certificate](#pluginsecurityfilecertificate)|
|[plugin.security.file.key](#pluginsecurityfilekey)|[plugin.security.pkcs11.driver_file](#pluginsecuritypkcs11driver_file)|
|[plugin.security.pkcs11.slot](#pluginsecuritypkcs11slot)|[plugin.security.provider](#pluginsecurityprovider)|
|[plugin.yaml](#pluginyaml)|[publish_timeout](#publish_timeout)|
|[registerinterval](#registerinterval)|[registration](#registration)|
|[registration_collective](#registration_collective)|[registration_splay](#registration_splay)|
|[rpcaudit](#rpcaudit)|[rpcauditprovider](#rpcauditprovider)|
|[rpcauthorization](#rpcauthorization)|[rpcauthprovider](#rpcauthprovider)|
|[rpclimitmethod](#rpclimitmethod)|[securityprovider](#securityprovider)|
|[soft_shutdown](#soft_shutdown)|[soft_shutdown_timeout](#soft_shutdown_timeout)|
|[threaded](#threaded)|[ttl](#ttl)|
|[plugin.choria.security.request_signing_certificate](#pluginchoriasecurityrequest_signing_certificate)|[plugin.choria.security.serializer](#pluginchoriasecurityserializer)|
|[plugin.choria.server.provision](#pluginchoriaserverprovision)|[plugin.choria.srv_domain](#pluginchoriasrv_domain)|
|[plugin.choria.ssldir](#pluginchoriassldir)|[plugin.choria.stats_address](#pluginchoriastats_address)|
|[plugin.choria.stats_port](#pluginchoriastats_port)|[plugin.choria.status_file_path](#pluginchoriastatus_file_path)|
|[plugin.choria.status_update_interval](#pluginchoriastatus_update_interval)|[plugin.choria.use_srv](#pluginchoriause_srv)|
|[plugin.nats.credentials](#pluginnatscredentials)|[plugin.nats.ngs](#pluginnatsngs)|
|[plugin.nats.pass](#pluginnatspass)|[plugin.nats.user](#pluginnatsuser)|
|[plugin.scout.overrides](#pluginscoutoverrides)|[plugin.scout.tags](#pluginscouttags)|
|[plugin.security.always_overwrite_cache](#pluginsecurityalways_overwrite_cache)|[plugin.security.certmanager.alt_names](#pluginsecuritycertmanageralt_names)|
|[plugin.security.certmanager.issuer](#pluginsecuritycertmanagerissuer)|[plugin.security.certmanager.namespace](#pluginsecuritycertmanagernamespace)|
|[plugin.security.certmanager.replace](#pluginsecuritycertmanagerreplace)|[plugin.security.cipher_suites](#pluginsecuritycipher_suites)|
|[plugin.security.client_anon_tls](#pluginsecurityclient_anon_tls)|[plugin.security.ecc_curves](#pluginsecurityecc_curves)|
|[plugin.security.file.ca](#pluginsecurityfileca)|[plugin.security.file.cache](#pluginsecurityfilecache)|
|[plugin.security.file.certificate](#pluginsecurityfilecertificate)|[plugin.security.file.key](#pluginsecurityfilekey)|
|[plugin.security.pkcs11.driver_file](#pluginsecuritypkcs11driver_file)|[plugin.security.pkcs11.slot](#pluginsecuritypkcs11slot)|
|[plugin.security.provider](#pluginsecurityprovider)|[plugin.yaml](#pluginyaml)|
|[publish_timeout](#publish_timeout)|[registerinterval](#registerinterval)|
|[registration](#registration)|[registration_collective](#registration_collective)|
|[registration_splay](#registration_splay)|[rpcaudit](#rpcaudit)|
|[rpcauditprovider](#rpcauditprovider)|[rpcauthorization](#rpcauthorization)|
|[rpcauthprovider](#rpcauthprovider)|[rpclimitmethod](#rpclimitmethod)|
|[securityprovider](#securityprovider)|[soft_shutdown](#soft_shutdown)|
|[soft_shutdown_timeout](#soft_shutdown_timeout)|[threaded](#threaded)|
|[ttl](#ttl)|[](#)|


## activate_agents
Expand Down Expand Up @@ -631,6 +632,12 @@ Path to the token used to access a Central Authenticator

URL to the Signing Service

## plugin.choria.security.request_signing_certificate

* **Type:** string

The public certificate of the key used to sign the JWTs in the Signing Service

## plugin.choria.security.serializer

* **Type:** string
Expand Down
135 changes: 126 additions & 9 deletions broker/network/ipauth.go
Original file line number Diff line number Diff line change
@@ -1,46 +1,165 @@
package network

import (
"crypto/md5"
"crypto/rsa"
"fmt"
"io/ioutil"
"net"
"strings"

"github.com/dgrijalva/jwt-go"
"github.com/nats-io/nats-server/v2/server"
"github.com/sirupsen/logrus"
)

// IPAuth implements gnatsd server.Authentication interface and
// IPAuth implements Nats Server server.Authentication interface and
// allows IP limits to be configured, connections that do not match
// the configured IP or CIDRs are not allowed to publish to the
// network targets used by clients to request actions on nodes
// network targets used by clients to request actions on nodes.
//
// Additionally when the server is running in a mode where anonymous
// TLS connections is accepted then servers are entirely denied and
// clients are allowed but restricted based on the JWT issued by the
// AAA Service.
type IPAuth struct {
allowList []string
anonTLS bool
denyServers bool
jwtSigner string
log *logrus.Entry
}

// Check checks and registers the incoming connection
func (a *IPAuth) Check(c server.ClientAuthentication) (verified bool) {
user := a.createUser(c)

remote := c.RemoteAddress()
if remote != nil && !a.remoteInClientAllowList(c.RemoteAddress()) {
jwts := c.GetOpts().Token
caller := ""

var err error

if a.anonTLS {
if remote == nil {
a.log.Warn("Denying unknown remote client while in AnonTLS mode")
return false
}

caller, err = a.parseAnonTLSJWTUser(jwts)
if err != nil {
a.log.Warnf("Could not parse JWT from %s, denying client: %s", remote.String(), err)
return false
}
}

// only if allow lists are set else its a noop and all traffic is passed
switch {
case a.remoteInClientAllowList(remote):
a.setClientPermissions(user, caller)

case len(a.allowList) > 0:
a.setServerPermissions(user)

}

c.RegisterUser(user)

return true
}

func (a *IPAuth) parseAnonTLSJWTUser(jwts string) (string, error) {
if a.jwtSigner == "" {
return "", fmt.Errorf("anonymous TLS JWT Signer not set in plugin.choria.security.request_signing_certificate, denying all clients")
}

if jwts == "" {
return "", fmt.Errorf("no JWT received")
}

signKey, err := a.jwtSignerKey()
if err != nil {
return "", fmt.Errorf("signing key error: %s", err)
}

token, err := jwt.Parse(jwts, func(token *jwt.Token) (interface{}, error) {
return signKey, nil
})
if err != nil {
return "", fmt.Errorf("invalid JWT: %s", err)
}

claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return "", fmt.Errorf("invalid claims")
}

err = claims.Valid()
if err != nil {
return "", fmt.Errorf("invalid claims")
}

caller, ok := claims["callerid"].(string)
if !ok {
return "", fmt.Errorf("no callerid in claims")
}

if caller == "" {
return "", fmt.Errorf("empty callerid in claims")
}

return caller, nil
}

func (a *IPAuth) jwtSignerKey() (*rsa.PublicKey, error) {
certBytes, err := ioutil.ReadFile(a.jwtSigner)
if err != nil {
return nil, err
}

signKey, err := jwt.ParseRSAPublicKeyFromPEM(certBytes)
if err != nil {
return nil, err
}

return signKey, nil
}

func (a *IPAuth) setClientPermissions(user *server.User, caller string) {
if !a.anonTLS {
return
}

replys := "*.reply.>"
if caller != "" {
replys = fmt.Sprintf("*.reply.%x.>", md5.Sum([]byte(caller)))
}

user.Permissions.Subscribe = &server.SubjectPermission{
Allow: []string{
replys,
},
}

user.Permissions.Publish = &server.SubjectPermission{
Allow: []string{
"*.broadcast.agent.>",
"*.node.>",
"choria.federation.*.federation",
},
}
}

func (a *IPAuth) setServerPermissions(user *server.User) {
matchAll := []string{">"}

switch {
case a.denyServers:
user.Permissions.Subscribe = &server.SubjectPermission{
Deny: []string{">"},
Deny: matchAll,
}

user.Permissions.Publish = &server.SubjectPermission{
Deny: []string{">"},
Deny: matchAll,
}

default:
Expand All @@ -53,9 +172,7 @@ func (a *IPAuth) setServerPermissions(user *server.User) {
}

user.Permissions.Publish = &server.SubjectPermission{
Allow: []string{
">",
},
Allow: matchAll,

Deny: []string{
"*.broadcast.agent.>",
Expand Down
Loading

0 comments on commit f7932e2

Please sign in to comment.