diff --git a/README.md b/README.md index d2148dd..36f62b9 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,11 @@ # SSL Certificate Exporter Exports metrics for certificates collected from various sources: + - [TCP probes](#tcp) - [HTTPS probes](#https) - [PEM files](#file) +- [PEM files hosted via HTTP](#http_file) - [Kubernetes secrets](#kubernetes) - [Kubeconfig files](#kubeconfig) @@ -56,27 +58,27 @@ Flags: ## Metrics -| Metric | Meaning | Labels | Probers | -| ------------------------------ | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | ---------- | -| ssl_cert_not_after | The date after which a peer certificate expires. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | tcp, https | -| ssl_cert_not_before | The date before which a peer certificate is not valid. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | tcp, https | -| ssl_file_cert_not_after | The date after which a certificate found by the file prober expires. Expressed as a Unix Epoch Time. | file, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | file | -| ssl_file_cert_not_before | The date before which a certificate found by the file prober is not valid. Expressed as a Unix Epoch Time. | file, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | file | -| ssl_kubernetes_cert_not_after | The date after which a certificate found by the kubernetes prober expires. Expressed as a Unix Epoch Time. | namespace, secret, key, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | kubernetes | -| ssl_kubernetes_cert_not_before | The date before which a certificate found by the kubernetes prober is not valid. Expressed as a Unix Epoch Time. | namespace, secret, key, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | kubernetes | -| ssl_kubeconfig_cert_not_after | The date after which a certificate found by the kubeconfig prober expires. Expressed as a Unix Epoch Time. | kubeconfig, name, type, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | kubeconfig | -| ssl_kubeconfig_cert_not_before | The date before which a certificate found by the kubeconfig prober is not valid. Expressed as a Unix Epoch Time. | kubeconfig, name, type, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | kubeconfig | -| ssl_ocsp_response_next_update | The nextUpdate value in the OCSP response. Expressed as a Unix Epoch Time | | tcp, https | -| ssl_ocsp_response_produced_at | The producedAt value in the OCSP response. Expressed as a Unix Epoch Time | | tcp, https | -| ssl_ocsp_response_revoked_at | The revocationTime value in the OCSP response. Expressed as a Unix Epoch Time | | tcp, https | -| 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_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 | -| ssl_verified_cert_not_after | The date after which a certificate in the verified chain expires. Expressed as a Unix Epoch Time. | chain_no, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | tcp, https | -| ssl_verified_cert_not_before | The date before which a certificate in the verified chain is not valid. Expressed as a Unix Epoch Time. | chain_no, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | tcp, https | +| Metric | Meaning | Labels | Probers | +| ------------------------------ | ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | --------------------- | +| ssl_cert_not_after | The date after which a peer certificate expires. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | tcp, https, http_file | +| ssl_cert_not_before | The date before which a peer certificate is not valid. Expressed as a Unix Epoch Time. | serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | tcp, https, http_file | +| ssl_file_cert_not_after | The date after which a certificate found by the file prober expires. Expressed as a Unix Epoch Time. | file, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | file | +| ssl_file_cert_not_before | The date before which a certificate found by the file prober is not valid. Expressed as a Unix Epoch Time. | file, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | file | +| ssl_kubernetes_cert_not_after | The date after which a certificate found by the kubernetes prober expires. Expressed as a Unix Epoch Time. | namespace, secret, key, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | kubernetes | +| ssl_kubernetes_cert_not_before | The date before which a certificate found by the kubernetes prober is not valid. Expressed as a Unix Epoch Time. | namespace, secret, key, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | kubernetes | +| ssl_kubeconfig_cert_not_after | The date after which a certificate found by the kubeconfig prober expires. Expressed as a Unix Epoch Time. | kubeconfig, name, type, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | kubeconfig | +| ssl_kubeconfig_cert_not_before | The date before which a certificate found by the kubeconfig prober is not valid. Expressed as a Unix Epoch Time. | kubeconfig, name, type, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | kubeconfig | +| ssl_ocsp_response_next_update | The nextUpdate value in the OCSP response. Expressed as a Unix Epoch Time | | tcp, https | +| ssl_ocsp_response_produced_at | The producedAt value in the OCSP response. Expressed as a Unix Epoch Time | | tcp, https | +| ssl_ocsp_response_revoked_at | The revocationTime value in the OCSP response. Expressed as a Unix Epoch Time | | tcp, https | +| 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_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 | +| ssl_verified_cert_not_after | The date after which a certificate in the verified chain expires. Expressed as a Unix Epoch Time. | chain_no, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | tcp, https | +| ssl_verified_cert_not_before | The date before which a certificate in the verified chain is not valid. Expressed as a Unix Epoch Time. | chain_no, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | tcp, https | ## Configuration @@ -175,6 +177,19 @@ scrape_configs: replacement: ${1}:9219 ``` +### HTTP File + +The `http_file` prober exports `ssl_cert_not_after` and +`ssl_cert_not_before` metrics for PEM encoded certificates hosted via HTTP or +HTTPS. + +This is useful for monitoring PEM certificates that are served from a webserver +for the purposes of [BIMI email authentication](https://postmarkapp.com/blog/what-the-heck-is-bimi). + +``` +curl 'localhost:9219/probe?module=http_file&target=https://amplify.valimail.com/bimi/time-warner/rWgzqvey7wX-cable_news_network_inc.pem' +``` + ### Kubernetes The `kubernetes` prober exports `ssl_kubernetes_cert_not_after` and @@ -215,15 +230,15 @@ sources in the following order: params: module: ["kubernetes"] static_configs: - - targets: - - "test-namespace/nginx-cert" + - targets: + - "test-namespace/nginx-cert" relabel_configs: - - source_labels: [ __address__ ] - target_label: __param_target - - source_labels: [ __param_target ] - target_label: instance - - target_label: __address__ - replacement: 127.0.0.1:9219 + - source_labels: [__address__] + target_label: __param_target + - source_labels: [__param_target] + target_label: instance + - target_label: __address__ + replacement: 127.0.0.1:9219 ``` ### Kubeconfig diff --git a/config/config.go b/config/config.go index f7c4c53..2c5a544 100644 --- a/config/config.go +++ b/config/config.go @@ -26,6 +26,9 @@ var ( "https": Module{ Prober: "https", }, + "http_file": Module{ + Prober: "http_file", + }, "file": Module{ Prober: "file", }, @@ -71,6 +74,7 @@ type Module struct { Timeout time.Duration `yaml:"timeout,omitempty"` TLSConfig TLSConfig `yaml:"tls_config,omitempty"` HTTPS HTTPSProbe `yaml:"https,omitempty"` + HTTPFile HTTPFileProbe `yaml:"http_file,omitempty"` TCP TCPProbe `yaml:"tcp,omitempty"` Kubernetes KubernetesProbe `yaml:"kubernetes,omitempty"` } @@ -137,6 +141,11 @@ type HTTPSProbe struct { ProxyURL URL `yaml:"proxy_url,omitempty"` } +// HTTPFileProbe configures a http_file probe +type HTTPFileProbe struct { + ProxyURL URL `yaml:"proxy_url,omitempty"` +} + // KubernetesProbe configures a kubernetes probe type KubernetesProbe struct { Kubeconfig string `yaml:"kubeconfig,omitempty"` diff --git a/examples/ssl_exporter.yaml b/examples/ssl_exporter.yaml index b5f491e..d410623 100644 --- a/examples/ssl_exporter.yaml +++ b/examples/ssl_exporter.yaml @@ -33,6 +33,8 @@ modules: prober: tcp tcp: starttls: smtp + http_file: + prober: http_file file: prober: file file_ca_certificates: diff --git a/prober/http_file.go b/prober/http_file.go new file mode 100644 index 0000000..5fa51d1 --- /dev/null +++ b/prober/http_file.go @@ -0,0 +1,83 @@ +package prober + +import ( + "context" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + "github.com/prometheus/client_golang/prometheus" + "github.com/ribbybibby/ssl_exporter/v2/config" +) + +// ProbeHTTPFile performs a http_file probe +func ProbeHTTPFile(ctx context.Context, logger log.Logger, target string, module config.Module, registry *prometheus.Registry) error { + tlsConfig, err := config.NewTLSConfig(&module.TLSConfig) + if err != nil { + return err + } + + targetURL, err := url.Parse(target) + if err != nil { + return err + } + + // If server name isn't set, then use the target hostname + if tlsConfig.ServerName == "" { + tlsConfig.ServerName = targetURL.Hostname() + } + + proxy := http.ProxyFromEnvironment + if module.HTTPS.ProxyURL.URL != nil { + proxy = http.ProxyURL(module.HTTPS.ProxyURL.URL) + } + + client := &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + Proxy: proxy, + DisableKeepAlives: true, + }, + } + + // Issue a GET request to the target + request, err := http.NewRequest(http.MethodGet, targetURL.String(), nil) + if err != nil { + return err + } + request = request.WithContext(ctx) + resp, err := client.Do(request) + if err != nil { + return err + } + defer func() { + _, err := io.Copy(ioutil.Discard, resp.Body) + if err != nil { + level.Error(logger).Log("msg", err) + } + resp.Body.Close() + }() + + data, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + certs, err := decodeCertificates(data) + if err != nil { + return err + } + + if len(certs) == 0 { + return fmt.Errorf("no certificates in response body") + } + + return collectCertificateMetrics(certs, registry) +} diff --git a/prober/http_file_test.go b/prober/http_file_test.go new file mode 100644 index 0000000..cadba18 --- /dev/null +++ b/prober/http_file_test.go @@ -0,0 +1,81 @@ +package prober + +import ( + "context" + "crypto/x509" + "encoding/pem" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/ribbybibby/ssl_exporter/v2/config" + "github.com/ribbybibby/ssl_exporter/v2/test" +) + +func TestProbeHTTPFile(t *testing.T) { + certPEM, _ := test.GenerateTestCertificate(time.Now().Add(time.Hour * 1)) + block, _ := pem.Decode([]byte(certPEM)) + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatalf("parsing cert: %s", err) + } + + server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(certPEM) + })) + + server.Start() + defer server.Close() + + registry := prometheus.NewRegistry() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeHTTPFile(ctx, newTestLogger(), server.URL+"/file", config.Module{}, registry); err != nil { + t.Fatalf("error: %s", err) + } + + checkCertificateMetrics(cert, registry, t) +} + +func TestProbeHTTPFile_HTTPS(t *testing.T) { + server, certPEM, _, caFile, teardown, err := test.SetupHTTPSServer() + if err != nil { + t.Fatalf(err.Error()) + } + defer teardown() + + block, _ := pem.Decode([]byte(certPEM)) + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatalf("parsing cert: %s", err) + } + + server.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write(certPEM) + }) + + server.StartTLS() + defer server.Close() + + module := config.Module{ + TLSConfig: config.TLSConfig{ + CAFile: caFile, + InsecureSkipVerify: false, + }, + } + + registry := prometheus.NewRegistry() + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := ProbeHTTPFile(ctx, newTestLogger(), server.URL+"/file", module, registry); err != nil { + t.Fatalf("error: %s", err) + } + + checkCertificateMetrics(cert, registry, t) +} diff --git a/prober/prober.go b/prober/prober.go index d283b5e..cd28576 100644 --- a/prober/prober.go +++ b/prober/prober.go @@ -13,6 +13,7 @@ var ( Probers = map[string]ProbeFn{ "https": ProbeHTTPS, "http": ProbeHTTPS, + "http_file": ProbeHTTPFile, "tcp": ProbeTCP, "file": ProbeFile, "kubernetes": ProbeKubernetes,