Repository: prometheus/blackbox_exporter
Pull request #678 (2020-08-18 04:26:59)

prober/tls: adding metric to expose certificate fingerprint info
this change adds a new metric `probe_ssl_fingerprint_info` to both tcp
and http probes. the metric always returns 1 similar to the tls version
metric and contains the leaf certificates sha256 fingerprint (hex) as a
label value.

this change allows users to validate in prometheus if a particular
certificate is being served.


Patch:
diff --git a/prober/http.go b/prober/http.go
index 7fa6c9c..8362c04 100644
--- a/prober/http.go
+++ b/prober/http.go
@@ -266,6 +266,14 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr
 			Help: "Returns last SSL chain expiry in timestamp seconds",
 		})
 
+		probeSSLLastInformation = prometheus.NewGaugeVec(
+			prometheus.GaugeOpts{
+				Name: "probe_ssl_last_chain_info",
+				Help: "Contains SSL leaf certificate information",
+			},
+			[]string{"fingerprint_sha256"},
+		)
+
 		probeTLSVersion = prometheus.NewGaugeVec(
 			prometheus.GaugeOpts{
 				Name: "probe_tls_version_info",
@@ -549,10 +557,11 @@ func ProbeHTTP(ctx context.Context, target string, module config.Module, registr
 
 	if resp.TLS != nil {
 		isSSLGauge.Set(float64(1))
-		registry.MustRegister(probeSSLEarliestCertExpiryGauge, probeTLSVersion, probeSSLLastChainExpiryTimestampSeconds)
+		registry.MustRegister(probeSSLEarliestCertExpiryGauge, probeTLSVersion, probeSSLLastChainExpiryTimestampSeconds, probeSSLLastInformation)
 		probeSSLEarliestCertExpiryGauge.Set(float64(getEarliestCertExpiry(resp.TLS).Unix()))
 		probeTLSVersion.WithLabelValues(getTLSVersion(resp.TLS)).Set(1)
 		probeSSLLastChainExpiryTimestampSeconds.Set(float64(getLastChainExpiry(resp.TLS).Unix()))
+		probeSSLLastInformation.WithLabelValues(getFingerprint(resp.TLS)).Set(1)
 		if httpConfig.FailIfSSL {
 			level.Error(logger).Log("msg", "Final request was over SSL")
 			success = false
diff --git a/prober/tcp.go b/prober/tcp.go
index b5f5add..3e8339b 100644
--- a/prober/tcp.go
+++ b/prober/tcp.go
@@ -98,6 +98,13 @@ func ProbeTCP(ctx context.Context, target string, module config.Module, registry
 		Name: "probe_ssl_last_chain_expiry_timestamp_seconds",
 		Help: "Returns last SSL chain expiry in unixtime",
 	})
+	probeSSLLastInformation := prometheus.NewGaugeVec(
+		prometheus.GaugeOpts{
+			Name: "probe_ssl_last_chain_info",
+			Help: "Contains SSL leaf certificate information",
+		},
+		[]string{"fingerprint_sha256"},
+	)
 	probeTLSVersion := prometheus.NewGaugeVec(
 		prometheus.GaugeOpts{
 			Name: "probe_tls_version_info",
@@ -129,10 +136,11 @@ func ProbeTCP(ctx context.Context, target string, module config.Module, registry
 	}
 	if module.TCP.TLS {
 		state := conn.(*tls.Conn).ConnectionState()
-		registry.MustRegister(probeSSLEarliestCertExpiry, probeTLSVersion, probeSSLLastChainExpiryTimestampSeconds)
+		registry.MustRegister(probeSSLEarliestCertExpiry, probeTLSVersion, probeSSLLastChainExpiryTimestampSeconds, probeSSLLastInformation)
 		probeSSLEarliestCertExpiry.Set(float64(getEarliestCertExpiry(&state).Unix()))
 		probeTLSVersion.WithLabelValues(getTLSVersion(&state)).Set(1)
 		probeSSLLastChainExpiryTimestampSeconds.Set(float64(getLastChainExpiry(&state).Unix()))
+		probeSSLLastInformation.WithLabelValues(getFingerprint(&state)).Set(1)
 	}
 	scanner := bufio.NewScanner(conn)
 	for i, qr := range module.TCP.QueryResponse {
@@ -203,6 +211,7 @@ func ProbeTCP(ctx context.Context, target string, module config.Module, registry
 			probeSSLEarliestCertExpiry.Set(float64(getEarliestCertExpiry(&state).Unix()))
 			probeTLSVersion.WithLabelValues(getTLSVersion(&state)).Set(1)
 			probeSSLLastChainExpiryTimestampSeconds.Set(float64(getLastChainExpiry(&state).Unix()))
+			probeSSLLastInformation.WithLabelValues(getFingerprint(&state)).Set(1)
 		}
 	}
 	return true
diff --git a/prober/tls.go b/prober/tls.go
index a4ef0f9..8710797 100644
--- a/prober/tls.go
+++ b/prober/tls.go
@@ -14,7 +14,9 @@
 package prober
 
 import (
+	"crypto/sha256"
 	"crypto/tls"
+	"encoding/hex"
 	"time"
 )
 
@@ -28,6 +30,12 @@ func getEarliestCertExpiry(state *tls.ConnectionState) time.Time {
 	return earliest
 }
 
+func getFingerprint(state *tls.ConnectionState) string {
+	cert := state.PeerCertificates[0]
+	fingerprint := sha256.Sum256(cert.Raw)
+	return hex.EncodeToString(fingerprint[:])
+}
+
 func getLastChainExpiry(state *tls.ConnectionState) time.Time {
 	lastChainExpiry := time.Time{}
 	for _, chain := range state.VerifiedChains {


Test patch:
diff --git a/prober/tcp_test.go b/prober/tcp_test.go
index d05f317..fd219f9 100644
--- a/prober/tcp_test.go
+++ b/prober/tcp_test.go
@@ -196,6 +196,7 @@ func TestTCPConnectionWithTLS(t *testing.T) {
 	// Check values
 	expectedResults := map[string]float64{
 		"probe_ssl_earliest_cert_expiry": float64(certExpiry.Unix()),
+		"probe_ssl_last_chain_info":      1,
 		"probe_tls_version_info":         1,
 	}
 	checkRegistryResults(expectedResults, mfs, t)
@@ -317,6 +318,7 @@ func TestTCPConnectionWithTLSAndVerifiedCertificateChain(t *testing.T) {
 	expectedResults := map[string]float64{
 		"probe_ssl_earliest_cert_expiry":                float64(serverCertExpiry.Unix()),
 		"probe_ssl_last_chain_expiry_timestamp_seconds": float64(serverCertExpiry.Unix()),
+		"probe_ssl_last_chain_info":                     1,
 		"probe_tls_version_info":                        1,
 	}
 	checkRegistryResults(expectedResults, mfs, t)