1
0
mirror of https://github.com/ribbybibby/ssl_exporter.git synced 2024-11-27 08:31:02 +02:00

Refactor prober function and metrics collection

The existing implementation consists of a collector that exports
information from a tls.ConnectionState returned by the prober function.
This won't necessarily integrate well with additional probers that
retrieve certs from sources other than a tls handshake (from file, for
instance).

I've made the probing more generically expandable by removing the
collector and instead registering and collecting metrics inside the
prober. This makes it possible to collect the same metrics in a
different way, or collect different metrics depending on the prober.
This commit is contained in:
Rob Best 2020-11-07 17:17:06 +00:00
parent e05745b959
commit c74c0de901
8 changed files with 439 additions and 440 deletions

View File

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

View File

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

268
prober/metrics.go Normal file
View File

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

View File

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

View File

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

View File

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

54
prober/tls.go Normal file
View File

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

View File

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