From 951b7c38f6d2965b1c3829e001789d076f1c6043 Mon Sep 17 00:00:00 2001 From: Ivan Kozlovic Date: Mon, 22 Aug 2022 11:48:49 -0600 Subject: [PATCH] [ADDED] Monitoring: TLS Peer Certificates in Connz when auth is on Add basic peer certificates information in /connz endpoint when the "auth" option is provided. Resolves #3317 Signed-off-by: Ivan Kozlovic --- server/monitor.go | 101 ++++++++++++++++++++++++++--------------- server/monitor_test.go | 53 +++++++++++++++++++++ server/server.go | 2 +- 3 files changed, 119 insertions(+), 37 deletions(-) diff --git a/server/monitor.go b/server/monitor.go index 78e88ced..6fbd92d6 100644 --- a/server/monitor.go +++ b/server/monitor.go @@ -14,7 +14,10 @@ package server import ( + "bytes" + "crypto/sha256" "crypto/tls" + "crypto/x509" "encoding/json" "fmt" "net" @@ -108,38 +111,45 @@ const ( // ConnInfo has detailed information on a per connection basis. type ConnInfo struct { - Cid uint64 `json:"cid"` - Kind string `json:"kind,omitempty"` - Type string `json:"type,omitempty"` - IP string `json:"ip"` - Port int `json:"port"` - Start time.Time `json:"start"` - LastActivity time.Time `json:"last_activity"` - Stop *time.Time `json:"stop,omitempty"` - Reason string `json:"reason,omitempty"` - RTT string `json:"rtt,omitempty"` - Uptime string `json:"uptime"` - Idle string `json:"idle"` - Pending int `json:"pending_bytes"` - InMsgs int64 `json:"in_msgs"` - OutMsgs int64 `json:"out_msgs"` - InBytes int64 `json:"in_bytes"` - OutBytes int64 `json:"out_bytes"` - NumSubs uint32 `json:"subscriptions"` - Name string `json:"name,omitempty"` - Lang string `json:"lang,omitempty"` - Version string `json:"version,omitempty"` - TLSVersion string `json:"tls_version,omitempty"` - TLSCipher string `json:"tls_cipher_suite,omitempty"` - AuthorizedUser string `json:"authorized_user,omitempty"` - Account string `json:"account,omitempty"` - Subs []string `json:"subscriptions_list,omitempty"` - SubsDetail []SubDetail `json:"subscriptions_list_detail,omitempty"` - JWT string `json:"jwt,omitempty"` - IssuerKey string `json:"issuer_key,omitempty"` - NameTag string `json:"name_tag,omitempty"` - Tags jwt.TagList `json:"tags,omitempty"` - MQTTClient string `json:"mqtt_client,omitempty"` // This is the MQTT client id + Cid uint64 `json:"cid"` + Kind string `json:"kind,omitempty"` + Type string `json:"type,omitempty"` + IP string `json:"ip"` + Port int `json:"port"` + Start time.Time `json:"start"` + LastActivity time.Time `json:"last_activity"` + Stop *time.Time `json:"stop,omitempty"` + Reason string `json:"reason,omitempty"` + RTT string `json:"rtt,omitempty"` + Uptime string `json:"uptime"` + Idle string `json:"idle"` + Pending int `json:"pending_bytes"` + InMsgs int64 `json:"in_msgs"` + OutMsgs int64 `json:"out_msgs"` + InBytes int64 `json:"in_bytes"` + OutBytes int64 `json:"out_bytes"` + NumSubs uint32 `json:"subscriptions"` + Name string `json:"name,omitempty"` + Lang string `json:"lang,omitempty"` + Version string `json:"version,omitempty"` + TLSVersion string `json:"tls_version,omitempty"` + TLSCipher string `json:"tls_cipher_suite,omitempty"` + TLSPeerCerts []*TLSPeerCert `json:"tls_peer_certs,omitempty"` + AuthorizedUser string `json:"authorized_user,omitempty"` + Account string `json:"account,omitempty"` + Subs []string `json:"subscriptions_list,omitempty"` + SubsDetail []SubDetail `json:"subscriptions_list_detail,omitempty"` + JWT string `json:"jwt,omitempty"` + IssuerKey string `json:"issuer_key,omitempty"` + NameTag string `json:"name_tag,omitempty"` + Tags jwt.TagList `json:"tags,omitempty"` + MQTTClient string `json:"mqtt_client,omitempty"` // This is the MQTT client id +} + +// TLSPeerCert contains basic information about a TLS peer certificate +type TLSPeerCert struct { + Subject string `json:"subject,omitempty"` + Fingerprints string `json:"fprints,omitempty"` } // DefaultConnListSize is the default size of the connection list. @@ -394,7 +404,7 @@ func (s *Server) Connz(opts *ConnzOptions) (*Connz, error) { for _, client := range openClients { client.mu.Lock() ci := &conns[i] - ci.fill(client, client.nc, c.Now) + ci.fill(client, client.nc, c.Now, auth) // Fill in subscription data if requested. if len(client.subs) > 0 { if subsDet { @@ -524,7 +534,7 @@ func (s *Server) Connz(opts *ConnzOptions) (*Connz, error) { // Fills in the ConnInfo from the client. // client should be locked. -func (ci *ConnInfo) fill(client *client, nc net.Conn, now time.Time) { +func (ci *ConnInfo) fill(client *client, nc net.Conn, now time.Time, auth bool) { ci.Cid = client.cid ci.MQTTClient = client.getMQTTClientID() ci.Kind = client.kindString() @@ -554,6 +564,9 @@ func (ci *ConnInfo) fill(client *client, nc net.Conn, now time.Time) { cs := conn.ConnectionState() ci.TLSVersion = tlsVersion(cs.Version) ci.TLSCipher = tlsCipher(cs.CipherSuite) + if auth && len(cs.PeerCertificates) > 0 { + ci.TLSPeerCerts = makePeerCerts(cs.PeerCertificates) + } } if client.port != 0 { @@ -562,6 +575,22 @@ func (ci *ConnInfo) fill(client *client, nc net.Conn, now time.Time) { } } +func makePeerCerts(pc []*x509.Certificate) []*TLSPeerCert { + res := make([]*TLSPeerCert, len(pc)) + for i, c := range pc { + fp := sha256.Sum256(c.Raw) + var buf bytes.Buffer + for i, f := range fp { + if i > 0 { + fmt.Fprintf(&buf, ":") + } + fmt.Fprintf(&buf, "%02X", f) + } + res[i] = &TLSPeerCert{Subject: c.Subject.String(), Fingerprints: buf.String()} + } + return res +} + // Assume lock is held func (c *client) getRTT() time.Duration { if c.rtt == 0 { @@ -1869,7 +1898,7 @@ func createOutboundRemoteGatewayz(c *client, opts *GatewayzOptions, now time.Tim rgw.IsConfigured = !c.gw.cfg.isImplicit() } rgw.Connection = &ConnInfo{} - rgw.Connection.fill(c, c.nc, now) + rgw.Connection.fill(c, c.nc, now, false) name = c.gw.name } c.mu.Unlock() @@ -1953,7 +1982,7 @@ func (s *Server) createInboundsRemoteGatewayz(opts *GatewayzOptions, now time.Ti rgw.Accounts = createInboundAccountsGatewayz(opts, c.gw) } rgw.Connection = &ConnInfo{} - rgw.Connection.fill(c, c.nc, now) + rgw.Connection.fill(c, c.nc, now, false) igws = append(igws, rgw) m[c.gw.name] = igws } diff --git a/server/monitor_test.go b/server/monitor_test.go index d6f756f0..5b9f4595 100644 --- a/server/monitor_test.go +++ b/server/monitor_test.go @@ -2186,6 +2186,59 @@ func TestConnzTLSCfg(t *testing.T) { } } +func TestConnzTLSPeerCerts(t *testing.T) { + resetPreviousHTTPConnections() + + tc := &TLSConfigOpts{} + tc.CertFile = "../test/configs/certs/server-cert.pem" + tc.KeyFile = "../test/configs/certs/server-key.pem" + tc.CaFile = "../test/configs/certs/ca.pem" + tc.Verify = true + tc.Timeout = 2.0 + + var err error + opts := DefaultMonitorOptions() + opts.TLSConfig, err = GenTLSConfig(tc) + require_NoError(t, err) + + s := RunServer(opts) + defer s.Shutdown() + + nc := natsConnect(t, s.ClientURL(), + nats.ClientCert("../test/configs/certs/client-cert.pem", "../test/configs/certs/client-key.pem"), + nats.RootCAs("../test/configs/certs/ca.pem")) + defer nc.Close() + + endpoint := fmt.Sprintf("http://%s:%d/connz", opts.HTTPHost, s.MonitorAddr().Port) + for mode := 0; mode < 2; mode++ { + // Without "auth" option, we should not get the details + connz := pollConz(t, s, mode, endpoint, nil) + require_True(t, len(connz.Conns) == 1) + c := connz.Conns[0] + if c.TLSPeerCerts != nil { + t.Fatalf("Did not expect TLSPeerCerts when auth is not specified: %+v", c.TLSPeerCerts) + } + // Now specify "auth" option + connz = pollConz(t, s, mode, endpoint+"?auth=1", &ConnzOptions{Username: true}) + require_True(t, len(connz.Conns) == 1) + c = connz.Conns[0] + if c.TLSPeerCerts == nil { + t.Fatal("Expected TLSPeerCerts to be set, was not") + } else if len(c.TLSPeerCerts) != 1 { + t.Fatalf("Unexpected peer certificates: %+v", c.TLSPeerCerts) + } else { + for _, d := range c.TLSPeerCerts { + if d.Subject != "CN=localhost,OU=nats.io,O=Synadia,ST=California,C=US" { + t.Fatalf("Unexpected subject: %s", d.Subject) + } + if n := strings.Count(d.Fingerprints, ":"); n != 31 { + t.Fatalf("Unexpected fingerprints: %s", d.Fingerprints) + } + } + } + } +} + func TestServerIDs(t *testing.T) { s := runMonitorServer() defer s.Shutdown() diff --git a/server/server.go b/server/server.go index 3515daf4..e7beb92d 100644 --- a/server/server.go +++ b/server/server.go @@ -2655,7 +2655,7 @@ func (s *Server) saveClosedClient(c *client, nc net.Conn, reason ClosedState) { c.mu.Lock() cc := &closedClient{} - cc.fill(c, nc, now) + cc.fill(c, nc, now, false) cc.Stop = &now cc.Reason = reason.String()