From 6f6f22e9a76b050e4d2adfbbe7c1a4ea51294c2c Mon Sep 17 00:00:00 2001 From: Matthias Hanel Date: Thu, 20 May 2021 17:00:09 -0400 Subject: [PATCH] [added] pinned_cert option to tls block hex(sha256(spki)) (#2233) * [added] pinned_cert option to tls block hex(sha256(spki)) When read form config, the values are automatically lower cased. The check when seeing the values programmatically requires lower case to avoid having to alter the map at this point. Signed-off-by: Matthias Hanel --- server/auth.go | 68 ++++++++++++++++++++++++++++++++--- server/gateway.go | 3 ++ server/leafnode.go | 3 ++ server/mqtt.go | 3 ++ server/opts.go | 38 +++++++++++++++++++- server/opts_test.go | 88 +++++++++++++++++++++++++++++++++++++++++++++ server/reload.go | 15 +++++++- server/server.go | 19 ++++++++-- server/websocket.go | 3 ++ test/tls_test.go | 45 +++++++++++++++++++++++ 10 files changed, 276 insertions(+), 9 deletions(-) diff --git a/server/auth.go b/server/auth.go index 1d7b45e4..f9bdb4ae 100644 --- a/server/auth.go +++ b/server/auth.go @@ -14,10 +14,12 @@ package server import ( + "crypto/sha256" "crypto/tls" "crypto/x509/pkix" "encoding/asn1" "encoding/base64" + "encoding/hex" "fmt" "net" "net/url" @@ -185,6 +187,19 @@ func (s *Server) checkAuthforWarnings() { // Warning about using plaintext passwords. s.Warnf("Plaintext passwords detected, use nkeys or bcrypt") } + // only warn about client connections others use bidirectional TLS + if len(s.opts.TLSPinnedCerts) > 0 && !(s.opts.TLSVerify || s.opts.TLSMap) { + s.Warnf("Pinned Certs specified but no verify or verify_and_map that would require presenting a client cert") + } + if len(s.opts.Websocket.TLSPinnedCerts) > 0 && !(s.opts.Websocket.TLSMap) { + s.Warnf("Websocket Pinned Certs specified but no verify_and_map that would require presenting a client cert") + } + if len(s.opts.MQTT.TLSPinnedCerts) > 0 && !(s.opts.MQTT.TLSMap) { + s.Warnf("MQTT Pinned Certs specified but no verify_and_map that would require presenting a client cert") + } + if len(s.opts.LeafNode.TLSPinnedCerts) > 0 && !(s.opts.LeafNode.TLSMap) { + s.Warnf("Leaf Pinned Certs specified but verify_and_map that would require presenting a client cert") + } } // If Users or Nkeys options have definitions without an account defined, @@ -344,6 +359,25 @@ func (s *Server) isClientAuthorized(c *client) bool { return true } +// returns false if the client needs to be disconnected +func (s *Server) matchesPinnedCert(c *client, tlsPinnedCerts PinnedCertSet) bool { + if tlsPinnedCerts == nil { + return true + } + tlsState := c.GetTLSConnectionState() + if tlsState == nil || len(tlsState.PeerCertificates) == 0 || tlsState.PeerCertificates[0] == nil { + c.Debugf("Failed pinned cert test as client did not provide a certificate") + return false + } + sha := sha256.Sum256(tlsState.PeerCertificates[0].RawSubjectPublicKeyInfo) + keyId := hex.EncodeToString(sha[:]) + if _, ok := tlsPinnedCerts[keyId]; !ok { + c.Debugf("Failed pinned cert test for key id: %s", keyId) + return false + } + return true +} + func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) bool { var ( nkey *NkeyUser @@ -366,11 +400,6 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo authRequired = s.websocket.authOverride } } - if !authRequired { - // TODO(dlc) - If they send us credentials should we fail? - s.mu.Unlock() - return true - } var ( username string password string @@ -378,12 +407,14 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo noAuthUser string ) tlsMap := opts.TLSMap + tlsPinnedCerts := opts.TLSPinnedCerts if c.kind == CLIENT { switch c.clientType() { case MQTT: mo := &opts.MQTT // Always override TLSMap. tlsMap = mo.TLSMap + tlsPinnedCerts = mo.TLSPinnedCerts // The rest depends on if there was any auth override in // the mqtt's config. if s.mqtt.authOverride { @@ -397,6 +428,7 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo wo := &opts.Websocket // Always override TLSMap. tlsMap = wo.TLSMap + tlsPinnedCerts = wo.TLSPinnedCerts // The rest depends on if there was any auth override in // the websocket's config. if s.websocket.authOverride { @@ -409,7 +441,18 @@ func (s *Server) processClientOrLeafAuthentication(c *client, opts *Options) boo } } else { tlsMap = opts.LeafNode.TLSMap + tlsPinnedCerts = opts.LeafNode.TLSPinnedCerts } + if !s.matchesPinnedCert(c, tlsPinnedCerts) { + s.mu.Unlock() + return false + } + if !authRequired { + // TODO(dlc) - If they send us credentials should we fail? + s.mu.Unlock() + return true + } + if !ao { noAuthUser = opts.NoAuthUser username = opts.Username @@ -872,6 +915,10 @@ func (s *Server) isRouterAuthorized(c *client) bool { return s.opts.CustomRouterAuthentication.Check(c) } + if !s.matchesPinnedCert(c, opts.Cluster.TLSPinnedCerts) { + return false + } + if opts.Cluster.TLSMap || opts.Cluster.TLSCheckKnownURLs { return checkClientTLSCertSubject(c, func(user string, _ *ldap.DN, isDNSAltName bool) (string, bool) { if user == "" { @@ -907,6 +954,10 @@ func (s *Server) isGatewayAuthorized(c *client) bool { // Snapshot server options. opts := s.getOpts() + if !s.matchesPinnedCert(c, opts.Gateway.TLSPinnedCerts) { + return false + } + // Check whether TLS map is enabled, otherwise use single user/pass. if opts.Gateway.TLSMap || opts.Gateway.TLSCheckKnownURLs { return checkClientTLSCertSubject(c, func(user string, _ *ldap.DN, isDNSAltName bool) (string, bool) { @@ -969,6 +1020,10 @@ func (s *Server) isLeafNodeAuthorized(c *client) bool { return s.registerLeafWithAccount(c, account) } + if !s.matchesPinnedCert(c, opts.LeafNode.TLSPinnedCerts) { + return false + } + // If leafnodes config has an authorization{} stanza, this takes precedence. // The user in CONNECT must match. We will bind to the account associated // with that user (from the leafnode's authorization{} config). @@ -1050,6 +1105,9 @@ func comparePasswords(serverPassword, clientPassword string) bool { } func validateAuth(o *Options) error { + if err := validatePinnedCerts(o.TLSPinnedCerts); err != nil { + return err + } for _, u := range o.Users { if err := validateAllowedConnectionTypes(u.AllowedConnectionTypes); err != nil { return err diff --git a/server/gateway.go b/server/gateway.go index d9ed2889..b5662267 100644 --- a/server/gateway.go +++ b/server/gateway.go @@ -296,6 +296,9 @@ func validateGatewayOptions(o *Options) error { return fmt.Errorf("gateway %q has no URL", g.Name) } } + if err := validatePinnedCerts(o.Gateway.TLSPinnedCerts); err != nil { + return fmt.Errorf("gateway %q: %v", o.Gateway.Name, err) + } return nil } diff --git a/server/leafnode.go b/server/leafnode.go index 6404381f..be88167c 100644 --- a/server/leafnode.go +++ b/server/leafnode.go @@ -308,6 +308,9 @@ func validateLeafNode(o *Options) error { if o.SystemAccount == "" { return fmt.Errorf("leaf nodes and gateways (both being defined) require a system account to also be configured") } + if err := validatePinnedCerts(o.LeafNode.TLSPinnedCerts); err != nil { + return fmt.Errorf("leafnode: %v", err) + } return nil } diff --git a/server/mqtt.go b/server/mqtt.go index 5ed0a72e..eb147b62 100644 --- a/server/mqtt.go +++ b/server/mqtt.go @@ -554,6 +554,9 @@ func validateMQTTOptions(o *Options) error { o.LeafNode.Port == 0 && len(o.LeafNode.Remotes) == 0 { return errMQTTStandaloneNeedsJetStream } + if err := validatePinnedCerts(mo.TLSPinnedCerts); err != nil { + return fmt.Errorf("mqtt: %v", err) + } return nil } diff --git a/server/opts.go b/server/opts.go index 639d0c74..dedb8be4 100644 --- a/server/opts.go +++ b/server/opts.go @@ -53,6 +53,9 @@ func NoErrOnUnknownFields(noError bool) { atomic.StoreInt32(&allowUnknownTopLevelField, val) } +// Set of lower case hex-encoded sha256 of DER encoded SubjectPublicKeyInfo +type PinnedCertSet map[string]struct{} + // ClusterOpts are options for clusters. // NOTE: This structure is no longer used for monitoring endpoints // and json tags are deprecated and may be removed in the future. @@ -68,6 +71,7 @@ type ClusterOpts struct { TLSConfig *tls.Config `json:"-"` TLSMap bool `json:"-"` TLSCheckKnownURLs bool `json:"-"` + TLSPinnedCerts PinnedCertSet `json:"-"` ListenStr string `json:"-"` Advertise string `json:"-"` NoAdvertise bool `json:"-"` @@ -91,6 +95,7 @@ type GatewayOpts struct { TLSTimeout float64 `json:"tls_timeout,omitempty"` TLSMap bool `json:"-"` TLSCheckKnownURLs bool `json:"-"` + TLSPinnedCerts PinnedCertSet `json:"-"` Advertise string `json:"advertise,omitempty"` ConnectRetries int `json:"connect_retries,omitempty"` Gateways []*RemoteGatewayOpts `json:"gateways,omitempty"` @@ -123,6 +128,7 @@ type LeafNodeOpts struct { TLSConfig *tls.Config `json:"-"` TLSTimeout float64 `json:"tls_timeout,omitempty"` TLSMap bool `json:"-"` + TLSPinnedCerts PinnedCertSet `json:"-"` Advertise string `json:"-"` NoAdvertise bool `json:"-"` ReconnectInterval time.Duration `json:"-"` @@ -226,6 +232,7 @@ type Options struct { TLSKey string `json:"-"` TLSCaCert string `json:"-"` TLSConfig *tls.Config `json:"-"` + TLSPinnedCerts PinnedCertSet `json:"-"` AllowNonTLS bool `json:"-"` WriteDeadline time.Duration `json:"-"` MaxClosedClients int `json:"-"` @@ -316,6 +323,9 @@ type WebsocketOpts struct { // If true, map certificate values for authentication purposes. TLSMap bool + // When present, accepted client certificates (verify/verify_and_map) must be in this list + TLSPinnedCerts PinnedCertSet + // If true, the Origin header must match the request's host. SameOrigin bool @@ -361,6 +371,8 @@ type MQTTOpts struct { TLSMap bool // Timeout for the TLS handshake TLSTimeout float64 + // Set of allowable certificates + TLSPinnedCerts PinnedCertSet // AckWait is the amount of time after which a QoS 1 message sent to // a client is redelivered as a DUPLICATE if the server has not @@ -470,6 +482,7 @@ type TLSConfigOpts struct { Timeout float64 Ciphers []uint16 CurvePreferences []tls.CurveID + PinnedCerts PinnedCertSet } var tlsUsage = ` @@ -827,7 +840,7 @@ func (o *Options) processConfigFileLine(k string, v interface{}, errors *[]error } o.TLSTimeout = tc.Timeout o.TLSMap = tc.Map - + o.TLSPinnedCerts = tc.PinnedCerts case "allow_non_tls": o.AllowNonTLS = v.(bool) case "write_deadline": @@ -1272,6 +1285,7 @@ func parseCluster(v interface{}, opts *Options, errors *[]error, warnings *[]err opts.Cluster.TLSConfig = config opts.Cluster.TLSTimeout = tlsopts.Timeout opts.Cluster.TLSMap = tlsopts.Map + opts.Cluster.TLSPinnedCerts = tlsopts.PinnedCerts opts.Cluster.TLSCheckKnownURLs = tlsopts.TLSCheckKnownURLs case "cluster_advertise", "advertise": opts.Cluster.Advertise = mv.(string) @@ -1389,6 +1403,7 @@ func parseGateway(v interface{}, o *Options, errors *[]error, warnings *[]error) o.Gateway.TLSTimeout = tlsopts.Timeout o.Gateway.TLSMap = tlsopts.Map o.Gateway.TLSCheckKnownURLs = tlsopts.TLSCheckKnownURLs + o.Gateway.TLSPinnedCerts = tlsopts.PinnedCerts case "advertise": o.Gateway.Advertise = mv.(string) case "connect_retries": @@ -1612,6 +1627,7 @@ func parseLeafNodes(v interface{}, opts *Options, errors *[]error, warnings *[]e } opts.LeafNode.TLSTimeout = tc.Timeout opts.LeafNode.TLSMap = tc.Map + opts.LeafNode.TLSPinnedCerts = tc.PinnedCerts case "leafnode_advertise", "advertise": opts.LeafNode.Advertise = mv.(string) case "no_advertise": @@ -3444,6 +3460,24 @@ func parseTLS(v interface{}, isClientCtx bool) (t *TLSConfigOpts, retErr error) at = mv } tc.Timeout = at + case "pinned_certs": + ra, ok := mv.([]interface{}) + if !ok { + return nil, &configErr{tk, "error parsing tls config, expected 'pinned_certs' to be a list of hex-encoded sha256 of DER encoded SubjectPublicKeyInfo"} + } + if len(ra) != 0 { + wl := PinnedCertSet{} + re := regexp.MustCompile("^[A-Fa-f0-9]{64}$") + for _, r := range ra { + tk, r := unwrapValue(r, <) + entry := strings.ToLower(r.(string)) + if !re.MatchString(entry) { + return nil, &configErr{tk, fmt.Sprintf("error parsing tls config, 'pinned_certs' key %s does not look like hex-encoded sha256 of DER encoded SubjectPublicKeyInfo", entry)} + } + wl[entry] = struct{}{} + } + tc.PinnedCerts = wl + } default: return nil, &configErr{tk, fmt.Sprintf("error parsing tls config, unknown field [%q]", mk)} } @@ -3573,6 +3607,7 @@ func parseWebsocket(v interface{}, o *Options, errors *[]error, warnings *[]erro continue } o.Websocket.TLSMap = tc.Map + o.Websocket.TLSPinnedCerts = tc.PinnedCerts case "same_origin": o.Websocket.SameOrigin = mv.(bool) case "allowed_origins", "allowed_origin", "allow_origins", "allow_origin", "origins", "origin": @@ -3662,6 +3697,7 @@ func parseMQTT(v interface{}, o *Options, errors *[]error, warnings *[]error) er } o.MQTT.TLSTimeout = tc.Timeout o.MQTT.TLSMap = tc.Map + o.MQTT.TLSPinnedCerts = tc.PinnedCerts case "authorization", "authentication": auth := parseSimpleAuth(tk, errors, warnings) o.MQTT.Username = auth.user diff --git a/server/opts_test.go b/server/opts_test.go index 3714c21f..09cbd0c6 100644 --- a/server/opts_test.go +++ b/server/opts_test.go @@ -890,6 +890,94 @@ func TestNkeyUsersConfig(t *testing.T) { } } +// Test pinned certificates +func TestTlsPinnedCertificates(t *testing.T) { + confFileName := createConfFile(t, []byte(` + tls { + cert_file: "./configs/certs/server.pem" + key_file: "./configs/certs/key.pem" + # Require a client certificate and map user id from certificate + verify: true + pinned_certs: ["7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069", + "a8f407340dcc719864214b85ed96f98d16cbffa8f509d9fa4ca237b7bb3f9c32"] + } + cluster { + port -1 + name cluster-hub + tls { + cert_file: "./configs/certs/server.pem" + key_file: "./configs/certs/key.pem" + # Require a client certificate and map user id from certificate + verify: true + pinned_certs: ["7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069", + "a8f407340dcc719864214b85ed96f98d16cbffa8f509d9fa4ca237b7bb3f9c32"] + } + } + leafnodes { + port -1 + tls { + cert_file: "./configs/certs/server.pem" + key_file: "./configs/certs/key.pem" + # Require a client certificate and map user id from certificate + verify: true + pinned_certs: ["7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069", + "a8f407340dcc719864214b85ed96f98d16cbffa8f509d9fa4ca237b7bb3f9c32"] + } + } + gateway { + name: "A" + port -1 + tls { + cert_file: "./configs/certs/server.pem" + key_file: "./configs/certs/key.pem" + # Require a client certificate and map user id from certificate + verify: true + pinned_certs: ["7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069", + "a8f407340dcc719864214b85ed96f98d16cbffa8f509d9fa4ca237b7bb3f9c32"] + } + } + websocket { + port -1 + tls { + cert_file: "./configs/certs/server.pem" + key_file: "./configs/certs/key.pem" + # Require a client certificate and map user id from certificate + verify: true + pinned_certs: ["7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069", + "a8f407340dcc719864214b85ed96f98d16cbffa8f509d9fa4ca237b7bb3f9c32"] + } + } + mqtt { + port -1 + tls { + cert_file: "./configs/certs/server.pem" + key_file: "./configs/certs/key.pem" + # Require a client certificate and map user id from certificate + verify: true + pinned_certs: ["7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069", + "a8f407340dcc719864214b85ed96f98d16cbffa8f509d9fa4ca237b7bb3f9c32"] + } + }`)) + defer removeFile(t, confFileName) + opts, err := ProcessConfigFile(confFileName) + if err != nil { + t.Fatalf("Received an error reading config file: %v", err) + } + check := func(set PinnedCertSet) { + t.Helper() + if l := len(set); l != 2 { + t.Fatalf("Expected 2 pinned certificates, got got %d", l) + } + } + + check(opts.TLSPinnedCerts) + check(opts.LeafNode.TLSPinnedCerts) + check(opts.Cluster.TLSPinnedCerts) + check(opts.MQTT.TLSPinnedCerts) + check(opts.Gateway.TLSPinnedCerts) + check(opts.Websocket.TLSPinnedCerts) +} + func TestNkeyUsersDefaultPermissionsConfig(t *testing.T) { confFileName := createConfFile(t, []byte(` authorization { diff --git a/server/reload.go b/server/reload.go index 189dfa03..424cb4f1 100644 --- a/server/reload.go +++ b/server/reload.go @@ -215,6 +215,17 @@ func (t *tlsTimeoutOption) Apply(server *Server) { server.Noticef("Reloaded: tls timeout = %v", t.newValue) } +// tlsPinnedCertOption implements the option interface for the tls `pinned_certs` setting. +type tlsPinnedCertOption struct { + noopOption + newValue PinnedCertSet +} + +// Apply is a no-op because the pinned certs will be reloaded after options are applied. +func (t *tlsPinnedCertOption) Apply(server *Server) { + server.Noticef("Reloaded: %d pinned_certs", len(t.newValue)) +} + // authOption is a base struct that provides default option behaviors. type authOption struct { noopOption @@ -790,7 +801,7 @@ func imposeOrder(value interface{}) error { }) case WebsocketOpts: sort.Strings(value.AllowedOrigins) - case string, bool, int, int32, int64, time.Duration, float64, nil, LeafNodeOpts, ClusterOpts, *tls.Config, + case string, bool, int, int32, int64, time.Duration, float64, nil, LeafNodeOpts, ClusterOpts, *tls.Config, PinnedCertSet, *URLAccResolver, *MemAccResolver, *DirAccResolver, *CacheDirAccResolver, Authentication, MQTTOpts, jwt.TagList: // explicitly skipped types default: @@ -864,6 +875,8 @@ func (s *Server) diffOptions(newOpts *Options) ([]option, error) { diffOpts = append(diffOpts, &tlsOption{newValue: newValue.(*tls.Config)}) case "tlstimeout": diffOpts = append(diffOpts, &tlsTimeoutOption{newValue: newValue.(float64)}) + case "tlspinnedcerts": + diffOpts = append(diffOpts, &tlsPinnedCertOption{newValue: newValue.(PinnedCertSet)}) case "username": diffOpts = append(diffOpts, &usernameOption{}) case "password": diff --git a/server/server.go b/server/server.go index 0214efcf..6fde16c0 100644 --- a/server/server.go +++ b/server/server.go @@ -26,6 +26,7 @@ import ( "math/rand" "net" "net/http" + "regexp" // Allow dynamic profiling. _ "net/http/pprof" @@ -549,7 +550,10 @@ func (s *Server) ClientURL() string { return fmt.Sprintf("%s%s:%d", scheme, opts.Host, opts.Port) } -func validateClusterName(o *Options) error { +func validateCluster(o *Options) error { + if err := validatePinnedCerts(o.Cluster.TLSPinnedCerts); err != nil { + return fmt.Errorf("cluster: %v", err) + } // Check that cluster name if defined matches any gateway name. if o.Gateway.Name != "" && o.Gateway.Name != o.Cluster.Name { if o.Cluster.Name != "" { @@ -561,6 +565,17 @@ func validateClusterName(o *Options) error { return nil } +func validatePinnedCerts(pinned PinnedCertSet) error { + re := regexp.MustCompile("^[a-f0-9]{64}$") + for certId := range pinned { + entry := strings.ToLower(certId) + if !re.MatchString(entry) { + return fmt.Errorf("error parsing 'pinned_certs' key %s does not look like lower case hex-encoded sha256 of DER encoded SubjectPublicKeyInfo", entry) + } + } + return nil +} + func validateOptions(o *Options) error { if o.LameDuckDuration > 0 && o.LameDuckGracePeriod >= o.LameDuckDuration { return fmt.Errorf("lame duck grace period (%v) should be strictly lower than lame duck duration (%v)", @@ -585,7 +600,7 @@ func validateOptions(o *Options) error { return err } // Check that cluster name if defined matches any gateway name. - if err := validateClusterName(o); err != nil { + if err := validateCluster(o); err != nil { return err } if err := validateMQTTOptions(o); err != nil { diff --git a/server/websocket.go b/server/websocket.go index 940ac627..b699631d 100644 --- a/server/websocket.go +++ b/server/websocket.go @@ -886,6 +886,9 @@ func validateWebsocketOptions(o *Options) error { return fmt.Errorf("trusted operators or trusted keys configuration is required for JWT authentication via cookie %q", wo.JWTCookie) } } + if err := validatePinnedCerts(wo.TLSPinnedCerts); err != nil { + return fmt.Errorf("websocket: %v", err) + } return nil } diff --git a/test/tls_test.go b/test/tls_test.go index 87c77dbf..dce01490 100644 --- a/test/tls_test.go +++ b/test/tls_test.go @@ -1935,3 +1935,48 @@ func TestTLSClientSVIDAuth(t *testing.T) { }) } } + +func TestTLSPinnedCerts(t *testing.T) { + tmpl := ` + host: localhost + port: -1 + tls { + ca_file: "configs/certs/ca.pem" + cert_file: "configs/certs/server-cert.pem" + key_file: "configs/certs/server-key.pem" + # Require a client certificate and map user id from certificate + verify: true + pinned_certs: ["%s"] + }` + + confFileName := createConfFile(t, []byte(fmt.Sprintf(tmpl, "aaaaaaaa09fde09451411ba3b42c0f74727d61a974c69fd3cf5257f39c75f0e9"))) + defer removeFile(t, confFileName) + srv, o := RunServerWithConfig(confFileName) + defer srv.Shutdown() + + if len(o.TLSPinnedCerts) != 1 { + t.Fatal("expected one pinned cert") + } + + opts := []nats.Option{ + nats.RootCAs("configs/certs/ca.pem"), + nats.ClientCert("./configs/certs/client-cert.pem", "./configs/certs/client-key.pem"), + } + + nc, err := nats.Connect(srv.ClientURL(), opts...) + if err == nil { + nc.Close() + t.Fatalf("Expected error trying to connect without a certificate in pinned_certs") + } + + ioutil.WriteFile(confFileName, []byte(fmt.Sprintf(tmpl, "bf6f821f09fde09451411ba3b42c0f74727d61a974c69fd3cf5257f39c75f0e9")), 0660) + if err := srv.Reload(); err != nil { + t.Fatalf("on Reload got %v", err) + } + // reload pinned to the certs used + nc, err = nats.Connect(srv.ClientURL(), opts...) + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + nc.Close() +}