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
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
Exports metrics for certificates collected from various sources:
- [TCP probes](#tcp)
- [HTTPS probes](#https)
- [OCSP probes](#ocsp)
- [PEM files](#file)
- [Remote PEM files](#http_file)
- [Kubernetes secrets](#kubernetes)
Expand Down Expand Up @@ -136,6 +137,39 @@ configuration.

The latter takes precedence.

### OCSP

The exporter will make a HTTP connection to the target, sending an
[OCSP](https://en.wikipedia.org/wiki/Online_Certificate_Status_Protocol) request
to the specified path, verifying the supplied certificate with the supplied
issuing certificate. By providing both certificates (vs. downloading the
issuing certificate via the Issuing Certificate URL specified in the client
certificate) we limit our testing to just the OCSP validation service instead of
having a path depend on being able to retrieve the issuing certificate.

This will return just `ssl_ocsp...` metrics, as OCSP responders communicate over
plain HTTP.

```yml
scrape_configs:
- job_name: "ocsp"
metrics_path: /probe
params:
module: ["ocsp"]
static_configs:
- targets:
- ocspresponder1.example.com
- ocspresponder2.example.com
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
```


### File

The `file` prober exports `ssl_file_cert_not_after` and
Expand Down Expand Up @@ -365,6 +399,19 @@ target: <string>
[ proxy_url: <string> ]
```

### <ocsp_probe>

```
# The client certificate to check vs. the OCSP responder
[ client_cert: <filename> ]

# The issuing certificate which signed the client_cert
[ issuing_cert: <filename> ]

# path on the target to the OCSP responder (e.g., '/ocsp')
[ path: <string> ]
```

### <tcp_probe>

```
Expand Down
11 changes: 11 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ var (
"tcp": {
Prober: "tcp",
},
"ocsp": {
Prober: "ocsp",
},
"http": {
Prober: "https",
},
Expand Down Expand Up @@ -75,6 +78,7 @@ type Module struct {
TLSConfig TLSConfig `yaml:"tls_config,omitempty"`
HTTPS HTTPSProbe `yaml:"https,omitempty"`
TCP TCPProbe `yaml:"tcp,omitempty"`
OCSP OCSPProbe `yaml:"ocsp,omitempty"`
Kubernetes KubernetesProbe `yaml:"kubernetes,omitempty"`
HTTPFile HTTPFileProbe `yaml:"http_file,omitempty"`
}
Expand Down Expand Up @@ -136,6 +140,13 @@ type TCPProbe struct {
StartTLS string `yaml:"starttls,omitempty"`
}

// OCSPProbe configures a ocsp probe
type OCSPProbe struct {
ClientCert string `yaml:"client_cert"`
IssuingCert string `yaml:"issuing_cert,omitempty"`
Path string `yaml:"path,omitempty"`
}

// HTTPSProbe configures a https probe
type HTTPSProbe struct {
ProxyURL URL `yaml:"proxy_url,omitempty"`
Expand Down
6 changes: 6 additions & 0 deletions examples/ssl_exporter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,9 @@ modules:
kubeconfig: /root/.kube/config
kubeconfig:
prober: kubeconfig
ocsp:
prober: ocsp
ocsp:
client_cert: /etc/ssl/certs/CompanyServerCA.pem
issuing_cert: /etc/ssl/certs/CompanyRoot.pem
path: /ocsp
99 changes: 99 additions & 0 deletions prober/ocsp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package prober

import (
"bytes"
"context"
"crypto"
"crypto/x509"
"encoding/pem"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"os"

"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/ribbybibby/ssl_exporter/v2/config"
"golang.org/x/crypto/ocsp"
)

// ProbeOCSP performs a OCSP probe
func ProbeOCSP(ctx context.Context, logger log.Logger, target string, module config.Module, registry *prometheus.Registry) error {
cert, err := readCert(module.OCSP.ClientCert)
if err != nil {
return err
}

issuerCert, err := readCert(module.OCSP.IssuingCert)
if err != nil {
return err
}

ocspURL := "http://" + target + module.OCSP.Path

buffer, err := ocsp.CreateRequest(cert, issuerCert, &ocsp.RequestOptions{
Hash: crypto.SHA1,
})
if err != nil {
return fmt.Errorf("creating ocsp request body: %w", err)
}

req, err := http.NewRequest(http.MethodPost, ocspURL, bytes.NewBuffer(buffer))
if err != nil {
return fmt.Errorf("creating http request: %w", err)
}

ocspUrl, err := url.Parse(ocspURL)
if err != nil {
return fmt.Errorf("parsing ocsp url: %w", err)
}

req.Header.Add("Content-Type", "application/ocsp-request")
req.Header.Add("Accept", "application/ocsp-response")
req.Header.Add("host", ocspUrl.Host)
req = req.WithContext(ctx)

// Make OCSP request
httpResponse, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("making ocsp request: %w", err)
}

defer httpResponse.Body.Close()

output, err := ioutil.ReadAll(httpResponse.Body)
if err != nil {
return fmt.Errorf("reading response body: %w", err)
}

return collectOCSPMetrics(output, registry)
}

func readCert(path string) (*x509.Certificate, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("opening %s: %w", path, err)
}

defer file.Close()

b, err := io.ReadAll(file)
if err != nil {
return nil, fmt.Errorf("reading %s: %w", path, err)
}

// PEM to DER
block, _ := pem.Decode([]byte(b))
if block == nil {
panic("failed to parse certificate PEM")
}

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parsing certificate %s: %w", path, err)
}

return cert, nil
}
1 change: 1 addition & 0 deletions prober/prober.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ var (
"https": ProbeHTTPS,
"http": ProbeHTTPS,
"tcp": ProbeTCP,
"ocsp": ProbeOCSP,
"file": ProbeFile,
"http_file": ProbeHTTPFile,
"kubernetes": ProbeKubernetes,
Expand Down