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
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Exports metrics for certificates collected from various sources:
- [TCP probes](#tcp)
- [HTTPS probes](#https)
- [PEM files](#file)
- [Java KeyStore files](#jks)
- [Kubernetes secrets](#kubernetes)
- [Kubeconfig files](#kubeconfig)

Expand Down Expand Up @@ -62,6 +63,8 @@ Flags:
| 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_jks_cert_not_after | The date after which a certificate found by the jks prober expires. Expressed as a Unix Epoch Time. | hostname, file, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | jks |
| ssl_jks_cert_not_before | The date before which a certificate found by the jks prober is not valid. Expressed as a Unix Epoch Time. | hostname, file, serial_no, issuer_cn, cn, dnsnames, ips, emails, ou | jks |
| 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 |
Expand Down Expand Up @@ -175,6 +178,46 @@ scrape_configs:
replacement: ${1}:9219
```

### JKS

The `jks` prober exports `ssl_jks_cert_not_after` and
`ssl_jks_cert_not_before` for PEM encoded certificates found in local java keystore files.

Java KeyStore files local to the exporter can be scraped by providing them as the target
parameter:

```
curl "localhost:9219/probe?module=jks&target=/usr/java/jdkXXX/jre/lib/security/cacerts"
```

The target parameter supports globbing (as provided by the
[doublestar](https://github.com/bmatcuk/doublestar) package),
which allows you to capture multiple files at once:

```
curl "localhost:9219/probe?module=file&target=/usr/java/jdkXXX/jre/lib/security/*.keystore"
```

One specific usage of this prober could be to run the exporter as a Systemd service
in virtual machine that runs JVM and then scrape Java related keystore to check the
expiry of certificates on each node:

```yml
scrape_configs:
- job_name: "java-cacerts-keystore"
metrics_path: /probe
params:
module: ["jks"]
target: ["/usr/java/jdkXXX/jre/lib/security/cacerts"]
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 # SSL exporter.
```

### Kubernetes

The `kubernetes` prober exports `ssl_kubernetes_cert_not_after` and
Expand Down
9 changes: 9 additions & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ var (
"file": Module{
Prober: "file",
},
"jks": Module{
Prober: "jks",
},
"kubernetes": Module{
Prober: "kubernetes",
},
Expand Down Expand Up @@ -70,6 +73,7 @@ type Module struct {
Target string `yaml:"target,omitempty"`
Timeout time.Duration `yaml:"timeout,omitempty"`
TLSConfig TLSConfig `yaml:"tls_config,omitempty"`
JKS JKSProbe `yaml:"jks,omitempty"`
HTTPS HTTPSProbe `yaml:"https,omitempty"`
TCP TCPProbe `yaml:"tcp,omitempty"`
Kubernetes KubernetesProbe `yaml:"kubernetes,omitempty"`
Expand Down Expand Up @@ -132,6 +136,11 @@ type TCPProbe struct {
StartTLS string `yaml:"starttls,omitempty"`
}

// JKSProbe configures a java keystore probe
type JKSProbe struct {
Password string `yaml:"password,omitempty"`
}

// HTTPSProbe configures a https probe
type HTTPSProbe struct {
ProxyURL URL `yaml:"proxy_url,omitempty"`
Expand Down
8 changes: 8 additions & 0 deletions examples/example.prometheus.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ scrape_configs:
static_configs:
- targets:
- 127.0.0.1:9219
- job_name: 'jks-files'
metrics_path: /probe
params:
module: ["jks"]
target: ["/usr/java/jdk1.8.0_74/jre/lib/security/cacerts"]
static_configs:
- targets:
- 127.0.0.1:9219
- job_name: 'ssl-kubernetes-secrets'
metrics_path: /probe
params:
Expand Down
4 changes: 4 additions & 0 deletions examples/ssl_exporter.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ modules:
file_ca_certificates:
prober: file
target: /etc/ssl/certs/ca-certificates.crt
jks:
prober: jks
jks:
password: changeit
kubernetes:
prober: kubernetes
kubernetes_kubeconfig:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/ribbybibby/ssl_exporter/v2
require (
github.com/bmatcuk/doublestar/v2 v2.0.4
github.com/go-kit/log v0.2.1
github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.1
github.com/prometheus/client_golang v1.13.0
github.com/prometheus/client_model v0.3.0
github.com/prometheus/common v0.37.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,8 @@ github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGV
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.1 h1:FyBdsRqqHH4LctMLL+BL2oGO+ONcIPwn96ctofCVtNE=
github.com/pavlo-v-chernykh/keystore-go/v4 v4.4.1/go.mod h1:lAVhWwbNaveeJmxrxuSTxMgKpF6DjnuVpn6T8WiBwYQ=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand Down
41 changes: 41 additions & 0 deletions prober/jks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package prober

import (
"context"
"fmt"

"github.com/bmatcuk/doublestar/v2"
"github.com/go-kit/log"
"github.com/prometheus/client_golang/prometheus"
"github.com/ribbybibby/ssl_exporter/v2/config"
)

// ProbeJKS collects certificate metrics from local java keystore files
func ProbeJKS(ctx context.Context, logger log.Logger, target string, module config.Module, registry *prometheus.Registry) error {
errCh := make(chan error, 1)

if module.JKS.Password == "" {
return fmt.Errorf("No password for jks configured")
}

go func() {
files, err := doublestar.Glob(target)
if err != nil {
errCh <- err
return
}

if len(files) == 0 {
errCh <- fmt.Errorf("No java keystore files found")
} else {
errCh <- collectJKSMetrics(logger, files, registry, module.JKS.Password)
}
}()

select {
case <-ctx.Done():
return fmt.Errorf("context timeout, ran out of time")
case err := <-errCh:
return err
}
}
219 changes: 219 additions & 0 deletions prober/jks_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package prober

import (
"context"
"crypto/x509"
"encoding/pem"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
"time"

"github.com/ribbybibby/ssl_exporter/v2/config"
"github.com/ribbybibby/ssl_exporter/v2/test"

"github.com/prometheus/client_golang/prometheus"
)

// TestProbeFile tests a java keystore file
func TestProbeJKSFile(t *testing.T) {
cert, certFile, err := createTestJKSFile("", "tls*.keystore")
if err != nil {
t.Fatalf(err.Error())
}
defer os.Remove(certFile)

module := config.Module{
JKS: config.JKSProbe{
Password: "changeit",
},
}

registry := prometheus.NewRegistry()

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

if err := ProbeJKS(ctx, newTestLogger(), certFile, module, registry); err != nil {
t.Fatalf("error: %s", err)
}

checkJKSFileMetrics(cert, certFile, registry, t)
}

// TestProbeFileGlob tests matching a java keystore file with a glob
func TestProbeJKSFileGlob(t *testing.T) {
cert, certFile, err := createTestJKSFile("", "tls*.keystore")
if err != nil {
t.Fatalf(err.Error())
}
defer os.Remove(certFile)

module := config.Module{
JKS: config.JKSProbe{
Password: "changeit",
},
}

registry := prometheus.NewRegistry()

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

glob := filepath.Dir(certFile) + "/*.keystore"

if err := ProbeJKS(ctx, newTestLogger(), glob, module, registry); err != nil {
t.Fatalf("error: %s", err)
}

checkJKSFileMetrics(cert, certFile, registry, t)
}

// TestProbeFileGlobDoubleStar tests matching a java keystore file with a ** glob
func TestProbeJKSFileGlobDoubleStar(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "testdir")
if err != nil {
t.Fatalf(err.Error())
}
cert, certFile, err := createTestJKSFile(tmpDir, "tls*.keystore")
if err != nil {
t.Fatalf(err.Error())
}
defer os.Remove(certFile)

module := config.Module{
JKS: config.JKSProbe{
Password: "changeit",
},
}

registry := prometheus.NewRegistry()

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

glob := filepath.Dir(filepath.Dir(certFile)) + "/**/*.keystore"

if err := ProbeJKS(ctx, newTestLogger(), glob, module, registry); err != nil {
t.Fatalf("error: %s", err)
}

checkJKSFileMetrics(cert, certFile, registry, t)
}

// TestProbeFileGlobDoubleStarMultiple tests matching multiple java keystore files with a ** glob
func TestProbeJKSFileGlobDoubleStarMultiple(t *testing.T) {
tmpDir, err := ioutil.TempDir("", "testdir")
if err != nil {
t.Fatalf(err.Error())
}
defer os.RemoveAll(tmpDir)

tmpDir1, err := ioutil.TempDir(tmpDir, "testdir")
if err != nil {
t.Fatalf(err.Error())
}
cert1, certFile1, err := createTestJKSFile(tmpDir1, "1*.keystore")
if err != nil {
t.Fatalf(err.Error())
}

tmpDir2, err := ioutil.TempDir(tmpDir, "testdir")
if err != nil {
t.Fatalf(err.Error())
}
cert2, certFile2, err := createTestJKSFile(tmpDir2, "2*.keystore")
if err != nil {
t.Fatalf(err.Error())
}

module := config.Module{
JKS: config.JKSProbe{
Password: "changeit",
},
}

registry := prometheus.NewRegistry()

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

glob := tmpDir + "/**/*.keystore"

if err := ProbeJKS(ctx, newTestLogger(), glob, module, registry); err != nil {
t.Fatalf("error: %s", err)
}

checkJKSFileMetrics(cert1, certFile1, registry, t)
checkJKSFileMetrics(cert2, certFile2, registry, t)
}

// Create a java keystore contains certificate and write it to a file
func createTestJKSFile(dir, filename string) (*x509.Certificate, string, error) {
certPEM, _ := test.GenerateTestCertificate(time.Now().Add(time.Hour * 1))
block, _ := pem.Decode([]byte(certPEM))
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, "", err
}
tmpFile, err := ioutil.TempFile(dir, filename)
if err != nil {
return nil, tmpFile.Name(), err
}
jks := test.GenerateTestJKSWithCertificate([]*x509.Certificate{cert})
if err := jks.Store(tmpFile, []byte("changeit")); err != nil {
return nil, "", err
}
if err := tmpFile.Close(); err != nil {
return nil, tmpFile.Name(), err
}

return cert, tmpFile.Name(), nil
}

// Check metrics
func checkJKSFileMetrics(cert *x509.Certificate, certFile string, registry *prometheus.Registry, t *testing.T) {
mfs, err := registry.Gather()
if err != nil {
t.Fatal(err)
}
ips := ","
for _, ip := range cert.IPAddresses {
ips = ips + ip.String() + ","
}
expectedResults := []*registryResult{
&registryResult{
Name: "ssl_jks_cert_not_after",
LabelValues: map[string]string{
"hostname": hostname(),
"file": certFile,
"serial_no": cert.SerialNumber.String(),
"issuer_cn": cert.Issuer.CommonName,
"cn": cert.Subject.CommonName,
"dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",",
"ips": ips,
"emails": "," + strings.Join(cert.EmailAddresses, ",") + ",",
"ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",",
},
Value: float64(cert.NotAfter.Unix()),
},
&registryResult{
Name: "ssl_jks_cert_not_before",
LabelValues: map[string]string{
"hostname": hostname(),
"file": certFile,
"serial_no": cert.SerialNumber.String(),
"issuer_cn": cert.Issuer.CommonName,
"cn": cert.Subject.CommonName,
"dnsnames": "," + strings.Join(cert.DNSNames, ",") + ",",
"ips": ips,
"emails": "," + strings.Join(cert.EmailAddresses, ",") + ",",
"ou": "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ",",
},
Value: float64(cert.NotBefore.Unix()),
},
}
checkRegistryResults(expectedResults, mfs, t)
}
Loading