diff --git a/prober/https.go b/prober/https.go index fec051d..29df8c5 100644 --- a/prober/https.go +++ b/prober/https.go @@ -1,7 +1,6 @@ package prober import ( - "crypto/tls" "fmt" "io" "io/ioutil" @@ -10,16 +9,20 @@ import ( "strings" "time" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/log" "github.com/ribbybibby/ssl_exporter/config" - - pconfig "github.com/prometheus/common/config" ) // ProbeHTTPS performs a https probe -func ProbeHTTPS(target string, module config.Module, timeout time.Duration) (*tls.ConnectionState, error) { +func ProbeHTTPS(target string, module config.Module, timeout time.Duration, registry *prometheus.Registry) error { + tlsConfig, err := newTLSConfig("", registry, &module.TLSConfig) + if err != nil { + return err + } + if strings.HasPrefix(target, "http://") { - return nil, fmt.Errorf("Target is using http scheme: %s", target) + return fmt.Errorf("Target is using http scheme: %s", target) } if !strings.HasPrefix(target, "https://") { @@ -28,12 +31,7 @@ func ProbeHTTPS(target string, module config.Module, timeout time.Duration) (*tl targetURL, err := url.Parse(target) if err != nil { - return nil, err - } - - tlsConfig, err := pconfig.NewTLSConfig(&module.TLSConfig) - if err != nil { - return nil, err + return err } proxy := http.ProxyFromEnvironment @@ -56,7 +54,7 @@ func ProbeHTTPS(target string, module config.Module, timeout time.Duration) (*tl // Issue a GET request to the target resp, err := client.Get(targetURL.String()) if err != nil { - return nil, err + return err } defer func() { _, err := io.Copy(ioutil.Discard, resp.Body) @@ -68,8 +66,8 @@ func ProbeHTTPS(target string, module config.Module, timeout time.Duration) (*tl // Check if the response from the target is encrypted if resp.TLS == nil { - return nil, fmt.Errorf("The response from %s is unencrypted", targetURL.String()) + return fmt.Errorf("The response from %s is unencrypted", targetURL.String()) } - return resp.TLS, nil + return nil } diff --git a/prober/https_test.go b/prober/https_test.go index 21abffe..c20b75e 100644 --- a/prober/https_test.go +++ b/prober/https_test.go @@ -11,6 +11,7 @@ import ( "testing" "time" + "github.com/prometheus/client_golang/prometheus" pconfig "github.com/prometheus/common/config" "github.com/ribbybibby/ssl_exporter/config" "github.com/ribbybibby/ssl_exporter/test" @@ -34,13 +35,12 @@ func TestProbeHTTPS(t *testing.T) { }, } - state, err := ProbeHTTPS(server.URL, module, 5*time.Second) - if err != nil { + registry := prometheus.NewRegistry() + + if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err != nil { t.Fatalf("error: %s", err) } - if state == nil { - t.Fatalf("expected state but got nil") - } + } // TestProbeHTTPSInvalidName tests hitting the server on an address which isn't @@ -67,7 +67,9 @@ func TestProbeHTTPSInvalidName(t *testing.T) { t.Fatalf(err.Error()) } - if _, err := ProbeHTTPS("https://localhost:"+u.Port(), module, 5*time.Second); err == nil { + registry := prometheus.NewRegistry() + + if err := ProbeHTTPS("https://localhost:"+u.Port(), module, 5*time.Second, registry); err == nil { t.Fatalf("expected error, but err was nil") } } @@ -96,7 +98,9 @@ func TestProbeHTTPSNoScheme(t *testing.T) { t.Fatalf(err.Error()) } - if _, err := ProbeHTTPS(u.Host, module, 5*time.Second); err != nil { + registry := prometheus.NewRegistry() + + if err := ProbeHTTPS(u.Host, module, 5*time.Second, registry); err != nil { t.Fatalf("error: %s", err) } } @@ -126,7 +130,9 @@ func TestProbeHTTPSServerName(t *testing.T) { }, } - if _, err := ProbeHTTPS("https://localhost:"+u.Port(), module, 5*time.Second); err != nil { + registry := prometheus.NewRegistry() + + if err := ProbeHTTPS("https://localhost:"+u.Port(), module, 5*time.Second, registry); err != nil { t.Fatalf("error: %s", err) } } @@ -139,7 +145,9 @@ func TestProbeHTTPSHTTP(t *testing.T) { server.Start() defer server.Close() - if _, err := ProbeHTTPS(server.URL, config.Module{}, 5*time.Second); err == nil { + registry := prometheus.NewRegistry() + + if err := ProbeHTTPS(server.URL, config.Module{}, 5*time.Second, registry); err == nil { t.Fatalf("expected error, but err was nil") } } @@ -186,13 +194,11 @@ func TestProbeHTTPSClientAuth(t *testing.T) { }, } - state, err := ProbeHTTPS(server.URL, module, 5*time.Second) - if err != nil { + registry := prometheus.NewRegistry() + + if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err != nil { t.Fatalf("error: %s", err) } - if state == nil { - t.Fatalf("expected state but got nil") - } } // TestProbeHTTPSClientAuthWrongClientCert tests that the probe fails with a bad @@ -241,7 +247,9 @@ func TestProbeHTTPSClientAuthWrongClientCert(t *testing.T) { }, } - if _, err := ProbeHTTPS(server.URL, module, 5*time.Second); err == nil { + registry := prometheus.NewRegistry() + + if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err == nil { t.Fatalf("expected error but err is nil") } } @@ -272,7 +280,9 @@ func TestProbeHTTPSExpired(t *testing.T) { }, } - if _, err := ProbeHTTPS(server.URL, module, 5*time.Second); err == nil { + registry := prometheus.NewRegistry() + + if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err == nil { t.Fatalf("expected error but err is nil") } } @@ -304,13 +314,11 @@ func TestProbeHTTPSExpiredInsecure(t *testing.T) { }, } - state, err := ProbeHTTPS(server.URL, module, 5*time.Second) - if err != nil { + registry := prometheus.NewRegistry() + + if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err != nil { t.Fatalf("error: %s", err) } - if state == nil { - t.Fatalf("expected state but got nil") - } } // TestProbeHTTPSProxy tests the proxy_url field in the configuration @@ -352,19 +360,17 @@ func TestProbeHTTPSProxy(t *testing.T) { }, } - _, err = ProbeHTTPS(server.URL, module, 5*time.Second) - if err == nil { + registry := prometheus.NewRegistry() + + if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err == nil { t.Fatalf("expected error but err was nil") } // Test with the proxy url, this shouldn't return an error module.HTTPS.ProxyURL = config.URL{URL: proxyURL} - state, err := ProbeHTTPS(server.URL, module, 5*time.Second) - if err != nil { + if err := ProbeHTTPS(server.URL, module, 5*time.Second, registry); err != nil { t.Fatalf("error: %s", err) } - if state == nil { - t.Fatalf("expected state but got nil") - } + } diff --git a/prober/metrics.go b/prober/metrics.go new file mode 100644 index 0000000..c430f66 --- /dev/null +++ b/prober/metrics.go @@ -0,0 +1,268 @@ +package prober + +import ( + "crypto/tls" + "crypto/x509" + "sort" + "strconv" + "strings" + "time" + + "github.com/prometheus/client_golang/prometheus" + "golang.org/x/crypto/ocsp" +) + +const ( + namespace = "ssl" +) + +func collectConnectionStateMetrics(state tls.ConnectionState, registry *prometheus.Registry) error { + if err := collectTLSVersionMetrics(state.Version, registry); err != nil { + return err + } + + if err := collectCertificateMetrics(state.PeerCertificates, registry); err != nil { + return err + } + + if err := collectVerifiedChainMetrics(state.VerifiedChains, registry); err != nil { + return err + } + + return collectOCSPMetrics(state.OCSPResponse, registry) +} + +func collectTLSVersionMetrics(version uint16, registry *prometheus.Registry) error { + var ( + tlsVersion = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "", "tls_version_info"), + Help: "The TLS version used", + }, + []string{"version"}, + ) + ) + registry.MustRegister(tlsVersion) + + var v string + switch version { + case tls.VersionTLS10: + v = "TLS 1.0" + case tls.VersionTLS11: + v = "TLS 1.1" + case tls.VersionTLS12: + v = "TLS 1.2" + case tls.VersionTLS13: + v = "TLS 1.3" + default: + v = "unknown" + } + + tlsVersion.WithLabelValues(v).Set(1) + + return nil +} + +func collectCertificateMetrics(certs []*x509.Certificate, registry *prometheus.Registry) error { + var ( + notAfter = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "", "cert_not_after"), + Help: "NotAfter expressed as a Unix Epoch Time", + }, + []string{"serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"}, + ) + notBefore = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "", "cert_not_before"), + Help: "NotBefore expressed as a Unix Epoch Time", + }, + []string{"serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"}, + ) + ) + registry.MustRegister(notAfter, notBefore) + + for _, cert := range uniq(certs) { + labels := labelValues(cert) + + if !cert.NotAfter.IsZero() { + notAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix())) + } + + if !cert.NotBefore.IsZero() { + notBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix())) + } + } + + return nil +} + +func collectVerifiedChainMetrics(verifiedChains [][]*x509.Certificate, registry *prometheus.Registry) error { + var ( + verifiedNotAfter = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "", "verified_cert_not_after"), + Help: "NotAfter expressed as a Unix Epoch Time", + }, + []string{"chain_no", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"}, + ) + verifiedNotBefore = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "", "verified_cert_not_before"), + Help: "NotBefore expressed as a Unix Epoch Time", + }, + []string{"chain_no", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"}, + ) + ) + registry.MustRegister(verifiedNotAfter, verifiedNotBefore) + + sort.Slice(verifiedChains, func(i, j int) bool { + iExpiry := time.Time{} + for _, cert := range verifiedChains[i] { + if (iExpiry.IsZero() || cert.NotAfter.Before(iExpiry)) && !cert.NotAfter.IsZero() { + iExpiry = cert.NotAfter + } + } + jExpiry := time.Time{} + for _, cert := range verifiedChains[j] { + if (jExpiry.IsZero() || cert.NotAfter.Before(jExpiry)) && !cert.NotAfter.IsZero() { + jExpiry = cert.NotAfter + } + } + + return iExpiry.After(jExpiry) + }) + + for i, chain := range verifiedChains { + chain = uniq(chain) + for _, cert := range chain { + chainNo := strconv.Itoa(i) + labels := append([]string{chainNo}, labelValues(cert)...) + + if !cert.NotAfter.IsZero() { + verifiedNotAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix())) + } + + if !cert.NotBefore.IsZero() { + verifiedNotBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix())) + } + } + } + + return nil +} + +func collectOCSPMetrics(ocspResponse []byte, registry *prometheus.Registry) error { + var ( + ocspStapled = prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "", "ocsp_response_stapled"), + Help: "If the connection state contains a stapled OCSP response", + }, + ) + ocspStatus = prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "", "ocsp_response_status"), + Help: "The status in the OCSP response 0=Good 1=Revoked 2=Unknown", + }, + ) + ocspProducedAt = prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "", "ocsp_response_produced_at"), + Help: "The producedAt value in the OCSP response, expressed as a Unix Epoch Time", + }, + ) + ocspThisUpdate = prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "", "ocsp_response_this_update"), + Help: "The thisUpdate value in the OCSP response, expressed as a Unix Epoch Time", + }, + ) + ocspNextUpdate = prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "", "ocsp_response_next_update"), + Help: "The nextUpdate value in the OCSP response, expressed as a Unix Epoch Time", + }, + ) + ocspRevokedAt = prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "", "ocsp_response_revoked_at"), + Help: "The revocationTime value in the OCSP response, expressed as a Unix Epoch Time", + }, + ) + ) + registry.MustRegister( + ocspStapled, + ocspStatus, + ocspProducedAt, + ocspThisUpdate, + ocspNextUpdate, + ocspRevokedAt, + ) + + if len(ocspResponse) == 0 { + return nil + } + + resp, err := ocsp.ParseResponse(ocspResponse, nil) + if err != nil { + return err + } + + ocspStapled.Set(1) + ocspStatus.Set(float64(resp.Status)) + ocspProducedAt.Set(float64(resp.ProducedAt.Unix())) + ocspThisUpdate.Set(float64(resp.ThisUpdate.Unix())) + ocspNextUpdate.Set(float64(resp.NextUpdate.Unix())) + ocspRevokedAt.Set(float64(resp.RevokedAt.Unix())) + + return nil +} + +func labelValues(cert *x509.Certificate) []string { + return []string{ + cert.SerialNumber.String(), + cert.Issuer.CommonName, + cert.Subject.CommonName, + dnsNames(cert), + ipAddresses(cert), + emailAddresses(cert), + organizationalUnits(cert), + } +} + +func dnsNames(cert *x509.Certificate) string { + if len(cert.DNSNames) > 0 { + return "," + strings.Join(cert.DNSNames, ",") + "," + } + + return "" +} + +func emailAddresses(cert *x509.Certificate) string { + if len(cert.EmailAddresses) > 0 { + return "," + strings.Join(cert.EmailAddresses, ",") + "," + } + + return "" +} + +func ipAddresses(cert *x509.Certificate) string { + if len(cert.IPAddresses) > 0 { + ips := "," + for _, ip := range cert.IPAddresses { + ips = ips + ip.String() + "," + } + return ips + } + + return "" +} + +func organizationalUnits(cert *x509.Certificate) string { + if len(cert.Subject.OrganizationalUnit) > 0 { + return "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + "," + } + + return "" +} diff --git a/prober/prober.go b/prober/prober.go index 4595e70..179b828 100644 --- a/prober/prober.go +++ b/prober/prober.go @@ -1,9 +1,9 @@ package prober import ( - "crypto/tls" "time" + "github.com/prometheus/client_golang/prometheus" "github.com/ribbybibby/ssl_exporter/config" ) @@ -17,4 +17,4 @@ var ( ) // ProbeFn probes -type ProbeFn func(target string, module config.Module, timeout time.Duration) (*tls.ConnectionState, error) +type ProbeFn func(target string, module config.Module, timeout time.Duration, registry *prometheus.Registry) error diff --git a/prober/tcp.go b/prober/tcp.go index ff29fc9..e669d3f 100644 --- a/prober/tcp.go +++ b/prober/tcp.go @@ -8,56 +8,41 @@ import ( "regexp" "time" - "github.com/ribbybibby/ssl_exporter/config" - - pconfig "github.com/prometheus/common/config" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/common/log" + "github.com/ribbybibby/ssl_exporter/config" ) // ProbeTCP performs a tcp probe -func ProbeTCP(target string, module config.Module, timeout time.Duration) (*tls.ConnectionState, error) { +func ProbeTCP(target string, module config.Module, timeout time.Duration, registry *prometheus.Registry) error { + tlsConfig, err := newTLSConfig(target, registry, &module.TLSConfig) + if err != nil { + return err + } + dialer := &net.Dialer{Timeout: timeout} conn, err := dialer.Dial("tcp", target) if err != nil { - return nil, err + return err } defer conn.Close() if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil { - return nil, fmt.Errorf("Error setting deadline") + return fmt.Errorf("Error setting deadline") } if module.TCP.StartTLS != "" { err = startTLS(conn, module.TCP.StartTLS) if err != nil { - return nil, err + return err } } - tlsConfig, err := pconfig.NewTLSConfig(&module.TLSConfig) - if err != nil { - return nil, err - } - - if tlsConfig.ServerName == "" { - targetAddress, _, err := net.SplitHostPort(target) - if err != nil { - return nil, err - } - tlsConfig.ServerName = targetAddress - } - tlsConn := tls.Client(conn, tlsConfig) defer tlsConn.Close() - if err := tlsConn.Handshake(); err != nil { - return nil, err - } - - state := tlsConn.ConnectionState() - - return &state, nil + return tlsConn.Handshake() } type queryResponse struct { diff --git a/prober/tcp_test.go b/prober/tcp_test.go index a37337b..a64f6b7 100644 --- a/prober/tcp_test.go +++ b/prober/tcp_test.go @@ -9,6 +9,7 @@ import ( "github.com/ribbybibby/ssl_exporter/config" "github.com/ribbybibby/ssl_exporter/test" + "github.com/prometheus/client_golang/prometheus" pconfig "github.com/prometheus/common/config" ) @@ -30,7 +31,9 @@ func TestProbeTCP(t *testing.T) { }, } - if _, err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second); err != nil { + registry := prometheus.NewRegistry() + + if err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second, registry); err != nil { t.Fatalf("error: %s", err) } } @@ -56,7 +59,9 @@ func TestProbeTCPInvalidName(t *testing.T) { _, listenPort, _ := net.SplitHostPort(server.Listener.Addr().String()) - if _, err := ProbeTCP("localhost:"+listenPort, module, 10*time.Second); err == nil { + registry := prometheus.NewRegistry() + + if err := ProbeTCP("localhost:"+listenPort, module, 10*time.Second, registry); err == nil { t.Fatalf("expected error but err was nil") } } @@ -83,7 +88,9 @@ func TestProbeTCPServerName(t *testing.T) { }, } - if _, err := ProbeTCP("localhost:"+listenPort, module, 10*time.Second); err != nil { + registry := prometheus.NewRegistry() + + if err := ProbeTCP("localhost:"+listenPort, module, 10*time.Second, registry); err != nil { t.Fatalf("error: %s", err) } } @@ -114,7 +121,9 @@ func TestProbeTCPExpired(t *testing.T) { }, } - if _, err := ProbeTCP(server.Listener.Addr().String(), module, 5*time.Second); err == nil { + registry := prometheus.NewRegistry() + + if err := ProbeTCP(server.Listener.Addr().String(), module, 5*time.Second, registry); err == nil { t.Fatalf("expected error but err is nil") } } @@ -146,13 +155,12 @@ func TestProbeTCPExpiredInsecure(t *testing.T) { }, } - state, err := ProbeTCP(server.Listener.Addr().String(), module, 5*time.Second) - if err != nil { + registry := prometheus.NewRegistry() + + if err := ProbeTCP(server.Listener.Addr().String(), module, 5*time.Second, registry); err != nil { t.Fatalf("error: %s", err) } - if state == nil { - t.Fatalf("expected state but got nil") - } + } // TestProbeTCPStartTLSSMTP tests STARTTLS against a mock SMTP server @@ -176,7 +184,9 @@ func TestProbeTCPStartTLSSMTP(t *testing.T) { }, } - if _, err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second); err != nil { + registry := prometheus.NewRegistry() + + if err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second, registry); err != nil { t.Fatalf("error: %s", err) } } @@ -202,7 +212,9 @@ func TestProbeTCPStartTLSFTP(t *testing.T) { }, } - if _, err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second); err != nil { + registry := prometheus.NewRegistry() + + if err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second, registry); err != nil { t.Fatalf("error: %s", err) } } @@ -228,7 +240,9 @@ func TestProbeTCPStartTLSIMAP(t *testing.T) { }, } - if _, err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second); err != nil { + registry := prometheus.NewRegistry() + + if err := ProbeTCP(server.Listener.Addr().String(), module, 10*time.Second, registry); err != nil { t.Fatalf("error: %s", err) } } diff --git a/prober/tls.go b/prober/tls.go new file mode 100644 index 0000000..1a6c201 --- /dev/null +++ b/prober/tls.go @@ -0,0 +1,54 @@ +package prober + +import ( + "crypto/tls" + "crypto/x509" + "net" + + "github.com/prometheus/client_golang/prometheus" + pconfig "github.com/prometheus/common/config" +) + +// newTLSConfig sets up TLS config and instruments it with a function that +// collects metrics for the verified chain +func newTLSConfig(target string, registry *prometheus.Registry, pTLSConfig *pconfig.TLSConfig) (*tls.Config, error) { + tlsConfig, err := pconfig.NewTLSConfig(pTLSConfig) + if err != nil { + return nil, err + } + + if tlsConfig.ServerName == "" && target != "" { + targetAddress, _, err := net.SplitHostPort(target) + if err != nil { + return nil, err + } + tlsConfig.ServerName = targetAddress + } + + tlsConfig.VerifyConnection = func(state tls.ConnectionState) error { + return collectConnectionStateMetrics(state, registry) + } + + return tlsConfig, nil +} + +func uniq(certs []*x509.Certificate) []*x509.Certificate { + r := []*x509.Certificate{} + + for _, c := range certs { + if !contains(r, c) { + r = append(r, c) + } + } + + return r +} + +func contains(certs []*x509.Certificate, cert *x509.Certificate) bool { + for _, c := range certs { + if (c.SerialNumber.String() == cert.SerialNumber.String()) && (c.Issuer.CommonName == cert.Issuer.CommonName) { + return true + } + } + return false +} diff --git a/ssl_exporter.go b/ssl_exporter.go index b3c62d4..1314091 100644 --- a/ssl_exporter.go +++ b/ssl_exporter.go @@ -1,297 +1,24 @@ package main import ( - "crypto/tls" - "crypto/x509" "fmt" + "net/http" + "strconv" + "time" + "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/common/log" "github.com/prometheus/common/version" "github.com/ribbybibby/ssl_exporter/config" "github.com/ribbybibby/ssl_exporter/prober" - "golang.org/x/crypto/ocsp" "gopkg.in/alecthomas/kingpin.v2" - "net/http" - "sort" - "strconv" - "strings" - "time" ) const ( namespace = "ssl" ) -var ( - tlsConnectSuccess = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "tls_connect_success"), - "If the TLS connection was a success", - nil, nil, - ) - tlsVersion = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "tls_version_info"), - "The TLS version used", - []string{"version"}, nil, - ) - proberType = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "prober"), - "The prober used by the exporter to connect to the target", - []string{"prober"}, nil, - ) - notBefore = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "cert_not_before"), - "NotBefore expressed as a Unix Epoch Time", - []string{"serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"}, nil, - ) - notAfter = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "cert_not_after"), - "NotAfter expressed as a Unix Epoch Time", - []string{"serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"}, nil, - ) - verifiedNotBefore = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "verified_cert_not_before"), - "NotBefore expressed as a Unix Epoch Time for a certificate in the list of verified chains", - []string{"chain_no", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"}, nil, - ) - verifiedNotAfter = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "verified_cert_not_after"), - "NotAfter expressed as a Unix Epoch Time for a certificate in the list of verified chains", - []string{"chain_no", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"}, nil, - ) - ocspStapled = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "ocsp_response_stapled"), - "If the connection state contains a stapled OCSP response", - nil, nil, - ) - ocspStatus = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "ocsp_response_status"), - "The status in the OCSP response 0=Good 1=Revoked 2=Unknown", - nil, nil, - ) - ocspProducedAt = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "ocsp_response_produced_at"), - "The producedAt value in the OCSP response, expressed as a Unix Epoch Time", - nil, nil, - ) - ocspThisUpdate = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "ocsp_response_this_update"), - "The thisUpdate value in the OCSP response, expressed as a Unix Epoch Time", - nil, nil, - ) - ocspNextUpdate = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "ocsp_response_next_update"), - "The nextUpdate value in the OCSP response, expressed as a Unix Epoch Time", - nil, nil, - ) - ocspRevokedAt = prometheus.NewDesc( - prometheus.BuildFQName(namespace, "", "ocsp_response_revoked_at"), - "The revocationTime value in the OCSP response, expressed as a Unix Epoch Time", - nil, nil, - ) -) - -// Exporter is the exporter type... -type Exporter struct { - target string - prober prober.ProbeFn - timeout time.Duration - module config.Module -} - -// Describe metrics -func (e *Exporter) Describe(ch chan<- *prometheus.Desc) { - ch <- tlsConnectSuccess - ch <- tlsVersion - ch <- proberType - ch <- notAfter - ch <- notBefore - ch <- verifiedNotAfter - ch <- verifiedNotBefore - ch <- ocspStapled - ch <- ocspStatus - ch <- ocspProducedAt - ch <- ocspThisUpdate - ch <- ocspNextUpdate - ch <- ocspRevokedAt -} - -// Collect metrics -func (e *Exporter) Collect(ch chan<- prometheus.Metric) { - ch <- prometheus.MustNewConstMetric( - proberType, prometheus.GaugeValue, 1, e.module.Prober, - ) - - state, err := e.prober(e.target, e.module, e.timeout) - if err != nil { - log.Errorf("error=%s target=%s prober=%s timeout=%s", err, e.target, e.module.Prober, e.timeout) - ch <- prometheus.MustNewConstMetric( - tlsConnectSuccess, prometheus.GaugeValue, 0, - ) - return - } - - // Get the TLS version from the connection state and export it as a metric - ch <- prometheus.MustNewConstMetric( - tlsVersion, prometheus.GaugeValue, 1, getTLSVersion(state), - ) - - // Retrieve certificates from the connection state - peerCertificates := state.PeerCertificates - if len(peerCertificates) < 1 { - log.Errorf("error=No certificates found in connection state. target=%s prober=%s", e.target, e.module.Prober) - ch <- prometheus.MustNewConstMetric( - tlsConnectSuccess, prometheus.GaugeValue, 0, - ) - return - } - - // If there are peer certificates in the connection state then consider - // the tls connection a success - ch <- prometheus.MustNewConstMetric( - tlsConnectSuccess, prometheus.GaugeValue, 1, - ) - - // Remove duplicate certificates from the response - peerCertificates = uniq(peerCertificates) - - // Loop through peer certificates and create metrics - for _, cert := range peerCertificates { - if !cert.NotAfter.IsZero() { - ch <- prometheus.MustNewConstMetric( - notAfter, - prometheus.GaugeValue, - float64(cert.NotAfter.UnixNano()/1e9), - cert.SerialNumber.String(), - cert.Issuer.CommonName, - cert.Subject.CommonName, - getDNSNames(cert), - getIPAddresses(cert), - getEmailAddresses(cert), - getOrganizationalUnits(cert), - ) - } - - if !cert.NotBefore.IsZero() { - ch <- prometheus.MustNewConstMetric( - notBefore, - prometheus.GaugeValue, - float64(cert.NotBefore.UnixNano()/1e9), - cert.SerialNumber.String(), - cert.Issuer.CommonName, - cert.Subject.CommonName, - getDNSNames(cert), - getIPAddresses(cert), - getEmailAddresses(cert), - getOrganizationalUnits(cert), - ) - } - } - - // Retrieve the list of verified chains from the connection state - verifiedChains := state.VerifiedChains - - // Sort the verified chains from the chain that is valid for longest to the chain - // that expires the soonest - sort.Slice(verifiedChains, func(i, j int) bool { - iExpiry := time.Time{} - for _, cert := range verifiedChains[i] { - if (iExpiry.IsZero() || cert.NotAfter.Before(iExpiry)) && !cert.NotAfter.IsZero() { - iExpiry = cert.NotAfter - } - } - jExpiry := time.Time{} - for _, cert := range verifiedChains[j] { - if (jExpiry.IsZero() || cert.NotAfter.Before(jExpiry)) && !cert.NotAfter.IsZero() { - jExpiry = cert.NotAfter - } - } - - return iExpiry.After(jExpiry) - }) - - // Loop through the verified chains creating metrics. Label the metrics - // with the index of the chain. - for i, chain := range verifiedChains { - chain = uniq(chain) - for _, cert := range chain { - chainNo := strconv.Itoa(i) - - if !cert.NotAfter.IsZero() { - ch <- prometheus.MustNewConstMetric( - verifiedNotAfter, - prometheus.GaugeValue, - float64(cert.NotAfter.UnixNano()/1e9), - chainNo, - cert.SerialNumber.String(), - cert.Issuer.CommonName, - cert.Subject.CommonName, - getDNSNames(cert), - getIPAddresses(cert), - getEmailAddresses(cert), - getOrganizationalUnits(cert), - ) - } - - if !cert.NotBefore.IsZero() { - ch <- prometheus.MustNewConstMetric( - verifiedNotBefore, - prometheus.GaugeValue, - float64(cert.NotBefore.UnixNano()/1e9), - chainNo, - cert.SerialNumber.String(), - cert.Issuer.CommonName, - cert.Subject.CommonName, - getDNSNames(cert), - getIPAddresses(cert), - getEmailAddresses(cert), - getOrganizationalUnits(cert), - ) - } - } - } - - if err := collectOCSPMetrics(ch, state); err != nil { - log.Errorf("error=%s target=%s prober=%s timeout=%s", err, e.target, e.module.Prober, e.timeout) - } -} - -func collectOCSPMetrics(ch chan<- prometheus.Metric, state *tls.ConnectionState) error { - if len(state.OCSPResponse) > 0 { - resp, err := ocsp.ParseResponse(state.OCSPResponse, nil) - if err != nil { - ch <- prometheus.MustNewConstMetric( - ocspStapled, prometheus.GaugeValue, 0, - ) - return err - } - ch <- prometheus.MustNewConstMetric( - ocspStapled, prometheus.GaugeValue, 1, - ) - ch <- prometheus.MustNewConstMetric( - ocspStatus, prometheus.GaugeValue, float64(resp.Status), - ) - ch <- prometheus.MustNewConstMetric( - ocspProducedAt, prometheus.GaugeValue, float64(resp.ProducedAt.Unix()), - ) - ch <- prometheus.MustNewConstMetric( - ocspThisUpdate, prometheus.GaugeValue, float64(resp.ThisUpdate.Unix()), - ) - ch <- prometheus.MustNewConstMetric( - ocspNextUpdate, prometheus.GaugeValue, float64(resp.NextUpdate.Unix()), - ) - ch <- prometheus.MustNewConstMetric( - ocspRevokedAt, prometheus.GaugeValue, float64(resp.RevokedAt.Unix()), - ) - } else { - ch <- prometheus.MustNewConstMetric( - ocspStapled, prometheus.GaugeValue, 0, - ) - } - - return nil -} - func probeHandler(w http.ResponseWriter, r *http.Request, conf *config.Config) { moduleName := r.URL.Query().Get("module") if moduleName == "" { @@ -331,99 +58,46 @@ func probeHandler(w http.ResponseWriter, r *http.Request, conf *config.Config) { return } - prober, ok := prober.Probers[module.Prober] + probeFunc, ok := prober.Probers[module.Prober] if !ok { http.Error(w, fmt.Sprintf("Unknown prober %q", module.Prober), http.StatusBadRequest) return } - exporter := &Exporter{ - target: target, - prober: prober, - timeout: timeout, - module: module, - } + var ( + tlsConnectSuccess = prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "", "tls_connect_success"), + Help: "If the TLS connection was a success", + }, + ) + proberType = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: prometheus.BuildFQName(namespace, "", "prober"), + Help: "The prober used by the exporter to connect to the target", + }, + []string{"prober"}, + ) + ) registry := prometheus.NewRegistry() - registry.MustRegister(exporter) + registry.MustRegister(tlsConnectSuccess, proberType) + proberType.WithLabelValues(module.Prober).Set(1) + + err := probeFunc(target, module, timeout, registry) + if err != nil { + log.Errorf("error=%s target=%s prober=%s timeout=%s", err, target, module.Prober, timeout) + tlsConnectSuccess.Set(0) + + } else { + tlsConnectSuccess.Set(1) + } // Serve h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{}) h.ServeHTTP(w, r) } -func uniq(certs []*x509.Certificate) []*x509.Certificate { - r := []*x509.Certificate{} - - for _, c := range certs { - if !contains(r, c) { - r = append(r, c) - } - } - - return r -} - -func contains(certs []*x509.Certificate, cert *x509.Certificate) bool { - for _, c := range certs { - if (c.SerialNumber.String() == cert.SerialNumber.String()) && (c.Issuer.CommonName == cert.Issuer.CommonName) { - return true - } - } - return false -} - -func getTLSVersion(state *tls.ConnectionState) string { - switch state.Version { - case tls.VersionTLS10: - return "TLS 1.0" - case tls.VersionTLS11: - return "TLS 1.1" - case tls.VersionTLS12: - return "TLS 1.2" - case tls.VersionTLS13: - return "TLS 1.3" - default: - return "unknown" - } -} - -func getDNSNames(cert *x509.Certificate) string { - if len(cert.DNSNames) > 0 { - return "," + strings.Join(cert.DNSNames, ",") + "," - } - - return "" -} - -func getEmailAddresses(cert *x509.Certificate) string { - if len(cert.EmailAddresses) > 0 { - return "," + strings.Join(cert.EmailAddresses, ",") + "," - } - - return "" -} - -func getIPAddresses(cert *x509.Certificate) string { - if len(cert.IPAddresses) > 0 { - ips := "," - for _, ip := range cert.IPAddresses { - ips = ips + ip.String() + "," - } - return ips - } - - return "" -} - -func getOrganizationalUnits(cert *x509.Certificate) string { - if len(cert.Subject.OrganizationalUnit) > 0 { - return "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + "," - } - - return "" -} - func init() { prometheus.MustRegister(version.NewCollector(namespace + "_exporter")) }