Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,12 @@ Flags:
| ssl_ocsp_response_status | The status in the OCSP response. 0=Good 1=Revoked 2=Unknown | | tcp, https |
| ssl_ocsp_response_stapled | Does the connection state contain a stapled OCSP response? Boolean. | | tcp, https |
| ssl_ocsp_response_this_update | The thisUpdate value in the OCSP response. Expressed as a Unix Epoch Time | | tcp, https |
| ssl_crl_status | The status of the CRL check 0=Good 1=Revoked 2=Unknown | | tcp, https |
| ssl_crl_revoke_reason | The reason code for revocation in the CRL as specified in RFC 5280 Section 5.3.1 | | tcp, https |
| ssl_crl_revoked_at | The revocationTime value in the CRL, expressed as a Unix Epoch Time | | tcp, https |
| ssl_crl_number | The value of the X.509 v2 cRLNumber extension in the CRL | | tcp, https |
| ssl_crl_this_update | The thisUpdate value in the CRL, expressed as a Unix Epoch Time | | tcp, https |
| ssl_crl_next_update | The nextUpdate value in the CRL, expressed as a Unix Epoch Time | | tcp, https |
| ssl_probe_success | Was the probe successful? Boolean. | | all |
| ssl_prober | The prober used by the exporter to connect to the target. Boolean. | prober | all |
| ssl_tls_version_info | The TLS version used. Always 1. | version | tcp, https |
Expand Down
8 changes: 8 additions & 0 deletions prober/https_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ func TestProbeHTTPS(t *testing.T) {
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

Expand Down Expand Up @@ -164,6 +165,7 @@ func TestProbeHTTPSNoScheme(t *testing.T) {
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

Expand Down Expand Up @@ -207,6 +209,7 @@ func TestProbeHTTPSServerName(t *testing.T) {
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

Expand Down Expand Up @@ -285,6 +288,7 @@ func TestProbeHTTPSClientAuth(t *testing.T) {
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

Expand Down Expand Up @@ -422,6 +426,7 @@ func TestProbeHTTPSExpiredInsecure(t *testing.T) {
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

Expand Down Expand Up @@ -486,6 +491,7 @@ func TestProbeHTTPSProxy(t *testing.T) {
}
checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

Expand Down Expand Up @@ -532,6 +538,7 @@ func TestProbeHTTPSOCSP(t *testing.T) {

checkCertificateMetrics(cert, registry, t)
checkOCSPMetrics(resp, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}

Expand Down Expand Up @@ -613,6 +620,7 @@ func TestProbeHTTPSVerifiedChains(t *testing.T) {

checkCertificateMetrics(serverCert, registry, t)
checkOCSPMetrics([]byte{}, registry, t)
checkCRLMetrics([]byte{}, registry, t)
checkVerifiedChainMetrics(verifiedChains, registry, t)
checkTLSVersionMetrics("TLS 1.3", registry, t)
}
160 changes: 156 additions & 4 deletions prober/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import (
"crypto/x509"
"encoding/base64"
"fmt"
"io/ioutil"
"io"
"math/big"
"net/http"
"os"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -231,6 +234,96 @@ func collectOCSPMetrics(ocspResponse []byte, registry *prometheus.Registry) erro
return nil
}

func collectCRLMetrics(verifiedChains [][]*x509.Certificate, registry *prometheus.Registry) error {
var (
crlStatus = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "crl_status"),
Help: "The status of the CRL check 0=Good 1=Revoked 2=Unknown",
},
)
crlRevokeReason = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "crl_revoke_reason"),
Help: "The reason code for revocation in the CRL as specified in RFC 5280 Section 5.3.1",
},
)
crlRevokedAt = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "crl_revoked_at"),
Help: "The revocationTime value in the CRL, expressed as a Unix Epoch Time",
},
)
crlNumber = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "crl_number"),
Help: "The value of the X.509 v2 cRLNumber extension in the CRL",
},
)
crlThisUpdate = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "crl_this_update"),
Help: "The thisUpdate value in the CRL, expressed as a Unix Epoch Time",
},
)
crlNextUpdate = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: prometheus.BuildFQName(namespace, "", "crl_next_update"),
Help: "The nextUpdate value in the CRL, expressed as a Unix Epoch Time",
},
)
)
registry.MustRegister(
crlStatus,
crlRevokeReason,
crlRevokedAt,
crlNumber,
crlThisUpdate,
crlNextUpdate,
)

if len(verifiedChains) == 0 {
crlStatus.Set(2)
return nil
}

var cert *x509.Certificate
var crl *x509.RevocationList
var err error
for _, chain := range verifiedChains {
if len(chain) == 0 {
continue
}
cert = chain[0]
crl, err = fetchCRL(chain)
if crl != nil {
break
}
}

if err != nil {
crlStatus.Set(2)
return err
}
if crl == nil {
crlStatus.Set(2)
return nil
}
num, _ := new(big.Float).SetInt(crl.Number).Float64()
crlNumber.Set(num)
crlThisUpdate.Set(float64(crl.ThisUpdate.Unix()))
crlNextUpdate.Set(float64(crl.NextUpdate.Unix()))
for _, revokedCert := range crl.RevokedCertificateEntries {
if revokedCert.SerialNumber.Cmp(cert.SerialNumber) == 0 {
crlStatus.Set(1)
crlRevokeReason.Set(float64(revokedCert.ReasonCode))
crlRevokedAt.Set(float64(revokedCert.RevocationTime.Unix()))
break
}
}
return nil
}

func collectFileMetrics(logger log.Logger, files []string, registry *prometheus.Registry) error {
var (
totalCerts []*x509.Certificate
Expand All @@ -252,7 +345,7 @@ func collectFileMetrics(logger log.Logger, files []string, registry *prometheus.
registry.MustRegister(fileNotAfter, fileNotBefore)

for _, f := range files {
data, err := ioutil.ReadFile(f)
data, err := os.ReadFile(f)
if err != nil {
level.Debug(logger).Log("msg", fmt.Sprintf("Error reading file %s: %s", f, err))
continue
Expand Down Expand Up @@ -363,7 +456,7 @@ func collectKubeconfigMetrics(logger log.Logger, kubeconfig KubeConfig, registry
return err
}
} else if c.Cluster.CertificateAuthority != "" {
data, err = ioutil.ReadFile(c.Cluster.CertificateAuthority)
data, err = os.ReadFile(c.Cluster.CertificateAuthority)
if err != nil {
level.Debug(logger).Log("msg", fmt.Sprintf("Error reading file %s: %s", c.Cluster.CertificateAuthority, err))
return err
Expand Down Expand Up @@ -399,7 +492,7 @@ func collectKubeconfigMetrics(logger log.Logger, kubeconfig KubeConfig, registry
return err
}
} else if u.User.ClientCertificate != "" {
data, err = ioutil.ReadFile(u.User.ClientCertificate)
data, err = os.ReadFile(u.User.ClientCertificate)
if err != nil {
level.Debug(logger).Log("msg", fmt.Sprintf("Error reading file %s: %s", u.User.ClientCertificate, err))
return err
Expand Down Expand Up @@ -480,3 +573,62 @@ func organizationalUnits(cert *x509.Certificate) string {

return ""
}

func fetchCRLDistributionPointFromCert(cert *x509.Certificate) string {
for _, url := range cert.CRLDistributionPoints {
if strings.HasPrefix(url, "http://") || strings.HasPrefix(url, "https://") {
return url
}
}
return ""
}

func getCRLIssuerCert(crl *x509.RevocationList, issuers []*x509.Certificate) (*x509.Certificate, error) {
for _, issuer := range issuers {
if crl.Issuer.String() == issuer.Subject.String() {
return issuer, nil
}
}
return nil, fmt.Errorf("no issuer found for CRL")
}

func fetchCRL(issuers []*x509.Certificate ) (*x509.RevocationList, error) {
var crlURL string
for _, issuer := range issuers {
crlURL = fetchCRLDistributionPointFromCert(issuer)
if crlURL != "" {
break
}
}
if crlURL == "" {
// CA/B Forum Ballot SC-063 v4 requires a CRL distribution point, but that only applies for publicly trusted CAs
return nil, nil
}

resp, err := http.Get(crlURL)
if err != nil {
return nil, err
}
defer resp.Body.Close()

data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}

crl, err := x509.ParseRevocationList(data)
if err != nil {
return nil, err
}
issuer, err := getCRLIssuerCert(crl, issuers)
if err != nil {
return nil, err
}
if err := crl.CheckSignatureFrom(issuer); err != nil {
return nil, err
}
if crl.NextUpdate.Before(time.Now()) {
return nil, fmt.Errorf("CRL has expired")
}
return crl, nil
}
64 changes: 64 additions & 0 deletions prober/metrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,70 @@ func checkOCSPMetrics(resp []byte, registry *prometheus.Registry, t *testing.T)
checkRegistryResults(expectedResults, mfs, t)
}

func checkCRLMetrics(crlRaw []byte, registry *prometheus.Registry, t *testing.T) {
var (
status float64
reason float64
revokedAt float64
number float64
thisUpdate float64
nextUpdate float64
)
mfs, err := registry.Gather()
if err != nil {
t.Fatal(err)
}
if len(crlRaw) == 0 {
expectedResults := []*registryResult{
{
Name: "ssl_crl_status",
Value: 2,
},
}
checkRegistryResults(expectedResults, mfs, t)
return
}
crl, err := x509.ParseRevocationList(crlRaw)
if err != nil {
t.Fatal(err)
}
number = float64(crl.Number.Int64())
thisUpdate = float64(crl.ThisUpdate.Unix())
nextUpdate = float64(crl.NextUpdate.Unix())
if len(crl.RevokedCertificateEntries) > 0 {
status = 1
reason = float64(crl.RevokedCertificateEntries[0].ReasonCode)
revokedAt = float64(crl.RevokedCertificateEntries[0].RevocationTime.Unix())
}
expectedResults := []*registryResult{
{
Name: "ssl_crl_status",
Value: status,
},
{
Name: "ssl_crl_revoke_reason",
Value: reason,
},
{
Name: "ssl_crl_revoked_at",
Value: revokedAt,
},
{
Name: "ssl_crl_number",
Value: number,
},
{
Name: "ssl_crl_this_update",
Value: thisUpdate,
},
{
Name: "ssl_crl_next_update",
Value: nextUpdate,
},
}
checkRegistryResults(expectedResults, mfs, t)
}

func checkTLSVersionMetrics(version string, registry *prometheus.Registry, t *testing.T) {
mfs, err := registry.Gather()
if err != nil {
Expand Down
Loading