Skip to content

Commit

Permalink
feat: add http_file prober
Browse files Browse the repository at this point in the history
  • Loading branch information
ribbybibby committed Jan 5, 2024
1 parent 3a594bc commit dc001f0
Show file tree
Hide file tree
Showing 6 changed files with 220 additions and 29 deletions.
73 changes: 44 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -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)

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ var (
"https": Module{
Prober: "https",
},
"http_file": Module{
Prober: "http_file",
},
"file": Module{
Prober: "file",
},
Expand Down Expand Up @@ -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"`
}
Expand Down Expand Up @@ -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"`
Expand Down
2 changes: 2 additions & 0 deletions examples/ssl_exporter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ modules:
prober: tcp
tcp:
starttls: smtp
http_file:
prober: http_file
file:
prober: file
file_ca_certificates:
Expand Down
83 changes: 83 additions & 0 deletions prober/http_file.go
Original file line number Diff line number Diff line change
@@ -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)
}
81 changes: 81 additions & 0 deletions prober/http_file_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
1 change: 1 addition & 0 deletions prober/prober.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ var (
Probers = map[string]ProbeFn{
"https": ProbeHTTPS,
"http": ProbeHTTPS,
"http_file": ProbeHTTPFile,
"tcp": ProbeTCP,
"file": ProbeFile,
"kubernetes": ProbeKubernetes,
Expand Down

0 comments on commit dc001f0

Please sign in to comment.