Skip to content
Merged
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
31 changes: 24 additions & 7 deletions strongswan/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ type Collector struct {
saRekeySecs *prometheus.Desc
saLifetimeSecs *prometheus.Desc

crtCnt *prometheus.Desc
crtValid *prometheus.Desc
crtCnt *prometheus.Desc
crtValid *prometheus.Desc
crtExpireSecs *prometheus.Desc
}

func NewCollector(viciClientFn viciClientFn) *Collector {
Expand Down Expand Up @@ -214,6 +215,11 @@ func NewCollector(viciClientFn viciClientFn) *Collector {
"X509 certificate validity",
[]string{"serial_number", "subject", "alternate_names", "not_before", "not_after"}, nil,
),
crtExpireSecs: prometheus.NewDesc(
prefix+"crt_expire_secs",
"Seconds until the X509 certificate expires",
[]string{"serial_number", "subject", "alternate_names", "not_before", "not_after"}, nil,
),
}
}

Expand Down Expand Up @@ -249,6 +255,7 @@ func (c *Collector) Describe(ch chan<- *prometheus.Desc) {

ch <- c.crtCnt
ch <- c.crtValid
ch <- c.crtExpireSecs
}

func (c *Collector) Collect(ch chan<- prometheus.Metric) {
Expand Down Expand Up @@ -558,6 +565,7 @@ func alternateNames(cert *x509.Certificate) string {
func (c *Collector) collectCrtMetrics(crts []Crt, ch chan<- prometheus.Metric) {
var x509Crts uint

now := time.Now()
for _, crt := range crts {
if crt.Type != "X509" {
continue
Expand All @@ -569,21 +577,30 @@ func (c *Collector) collectCrtMetrics(crts []Crt, ch chan<- prometheus.Metric) {
continue
}

now := time.Now()
valid := 0
if now.After(cert.NotBefore) && now.Before(cert.NotAfter) {
valid = 1
}
expire := cert.NotAfter.Sub(now).Seconds()

ch <- prometheus.MustNewConstMetric(
c.crtValid,
prometheus.GaugeValue,
float64(valid),
labels := []string{
cert.SerialNumber.String(),
cert.Subject.String(),
alternateNames(cert),
cert.NotBefore.Format(time.RFC3339),
cert.NotAfter.Format(time.RFC3339),
}
ch <- prometheus.MustNewConstMetric(
c.crtValid,
prometheus.GaugeValue,
float64(valid),
labels...,
)
ch <- prometheus.MustNewConstMetric(
c.crtExpireSecs,
prometheus.GaugeValue,
float64(expire),
labels...,
)

x509Crts++
Expand Down
186 changes: 146 additions & 40 deletions strongswan/collector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,17 @@ import (
"crypto/x509/pkix"
"errors"
"fmt"
"math"
"math/big"
"net"
"net/url"
"strconv"
"strings"
"testing"
"time"

"github.com/prometheus/client_golang/prometheus/testutil"
"github.com/prometheus/common/expfmt"
"github.com/stretchr/testify/require"
"github.com/strongswan/govici/vici"
)
Expand All @@ -28,6 +31,11 @@ type fakeViciClient struct {
closeTriggered int
}

const (
crtExpireTimeMetricName = "strongswan_crt_expire_secs"
crtExpireAllowedDiffFromExpected = 60.0
)

func (fvc *fakeViciClient) StreamedCommandRequest(cmd string, event string, _ *vici.Message) ([]*vici.Message, error) {
if cmd == "list-sas" && event == "list-sa" {
return fvc.sasMsgs, fvc.sasErr
Expand Down Expand Up @@ -410,27 +418,16 @@ func TestCollector_Metrics(t *testing.T) {
wantMetricsHelp: "Number of X509 certificates",
wantMetricsType: "gauge",
wantMetricsValue: 1,
wantMetricsCount: 3,
wantMetricsCount: 4,
},
{
name: "one cert count",
crtsMsgsModifierFn: func() []*vici.Message {
crtMsg := vici.NewMessage()
crtMsg.Set("type", "X509")

crt, err := createSingleX509Crt()
if err != nil {
return []*vici.Message{}
}

crtMsg.Set("data", string(crt))
return []*vici.Message{crtMsg}
},
metricName: "strongswan_crt_count",
wantMetricsHelp: "Number of X509 certificates",
wantMetricsType: "gauge",
wantMetricsValue: 1,
wantMetricsCount: 3,
name: "one cert count",
crtsMsgsModifierFn: singleCertViciMessages(createSingleX509Crt),
metricName: "strongswan_crt_count",
wantMetricsHelp: "Number of X509 certificates",
wantMetricsType: "gauge",
wantMetricsValue: 1,
wantMetricsCount: 4,
},
{
name: "two cert count",
Expand All @@ -454,28 +451,27 @@ func TestCollector_Metrics(t *testing.T) {
wantMetricsHelp: "Number of X509 certificates",
wantMetricsType: "gauge",
wantMetricsValue: 2,
wantMetricsCount: 4,
wantMetricsCount: 6,
},
{
name: "cert valid",
crtsMsgsModifierFn: func() []*vici.Message {
crtMsg := vici.NewMessage()
crtMsg.Set("type", "X509")

crt, err := createSingleX509Crt()
if err != nil {
return []*vici.Message{}
}

crtMsg.Set("data", string(crt))
return []*vici.Message{crtMsg}
},
metricName: "strongswan_crt_valid",
wantMetricsHelp: "X509 certificate validity",
wantMetricsType: "gauge",
wantMetricsLabels: `alternate_names="",not_after="2124-01-01T12:00:00Z",not_before="2024-01-01T12:00:00Z",serial_number="1",subject="CN=Test,O=Org1"`,
wantMetricsValue: 1,
wantMetricsCount: 3,
name: "cert valid",
crtsMsgsModifierFn: singleCertViciMessages(createSingleX509Crt),
metricName: "strongswan_crt_valid",
wantMetricsHelp: "X509 certificate validity",
wantMetricsType: "gauge",
wantMetricsLabels: `alternate_names="",not_after="2124-01-01T12:00:00Z",not_before="2024-01-01T12:00:00Z",serial_number="1",subject="CN=Test,O=Org1"`,
wantMetricsValue: 1,
wantMetricsCount: 4,
},
{
name: "cert expired",
crtsMsgsModifierFn: singleCertViciMessages(createExpiredX509Crt),
metricName: "strongswan_crt_valid",
wantMetricsHelp: "X509 certificate validity",
wantMetricsType: "gauge",
wantMetricsLabels: `alternate_names="",not_after="2024-12-01T12:00:00Z",not_before="2024-01-01T12:00:00Z",serial_number="1",subject="CN=Test,O=Org1"`,
wantMetricsValue: 0,
wantMetricsCount: 4,
},
{
name: "cert alternate names",
Expand Down Expand Up @@ -510,7 +506,7 @@ func TestCollector_Metrics(t *testing.T) {
wantMetricsType: "gauge",
wantMetricsLabels: `alternate_names="DNS=test.org+DNS=test2.org,EM=Name1+EM=Name2,IP=192.168.0.1+IP=192.168.1.1,URI=https://test.org/foobar+URI=https://test2.org/foobar2",not_after="2124-01-01T12:00:00Z",not_before="2024-01-01T12:00:00Z",serial_number="1",subject="CN=Test,O=Org1"`,
wantMetricsValue: 1,
wantMetricsCount: 3,
wantMetricsCount: 4,
},
}
for _, tt := range tests {
Expand Down Expand Up @@ -750,6 +746,100 @@ func TestCollector_MetricsSaChild(t *testing.T) {
}
}

func TestCollector_MetricsCrtExpireTime(t *testing.T) {
tz, err := time.LoadLocation("UTC")
if err != nil {
t.Errorf("failed to load timezone: %s", err)
}

tests := []struct {
name string
crtsMsgsModifierFn func() []*vici.Message
wantMetricsLabels string
wantMetricsValue float64
}{
{
name: "cert expire time for valid cert",
crtsMsgsModifierFn: singleCertViciMessages(createSingleX509Crt),
wantMetricsLabels: `alternate_names="",not_after="2124-01-01T12:00:00Z",not_before="2024-01-01T12:00:00Z",serial_number="1",subject="CN=Test,O=Org1"`,
wantMetricsValue: time.Until(time.Date(2124, 1, 1, 12, 0, 0, 0, tz)).Seconds(),
},
{
name: "cert expire time for expired cert",
crtsMsgsModifierFn: singleCertViciMessages(createExpiredX509Crt),
wantMetricsLabels: `alternate_names="",not_after="2024-12-01T12:00:00Z",not_before="2024-01-01T12:00:00Z",serial_number="1",subject="CN=Test,O=Org1"`,
wantMetricsValue: time.Until(time.Date(2024, 12, 1, 12, 0, 0, 0, tz)).Seconds(),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
crtsMmsgs := []*vici.Message{}
if tt.crtsMsgsModifierFn != nil {
crtsMmsgs = tt.crtsMsgsModifierFn()
}
c := NewCollector(func() (ViciClient, error) {
return &fakeViciClient{
crtsMsgs: crtsMmsgs,
},
nil
})

cnt := testutil.CollectAndCount(c)
const wantMetricsCount = 4
require.Equal(t, wantMetricsCount, cnt, "metrics count")

metricBytes, err := testutil.CollectAndFormat(c, expfmt.TypeTextPlain, crtExpireTimeMetricName)
if err != nil {
t.Fatalf("unexpected collecting result of '%s':\n%s", crtExpireTimeMetricName, err)
}

metricStr := strings.TrimSpace(string(metricBytes))
validateMetricsCrtExpireTimeValue(t, metricStr, tt.wantMetricsValue)
validateMetricsCrtExpireTimeLabels(t, metricStr, tt.wantMetricsLabels)
})
}
}

func validateMetricsCrtExpireTimeValue(t *testing.T, metricStr string, wantMetricsValue float64) {
metricFields := strings.Split(metricStr, " ")
if len(metricFields) == 0 {
t.Fatalf("unexpected format of metric '%s': %s", crtExpireTimeMetricName, metricStr)
}

metricValStr := metricFields[len(metricFields)-1]
metricVal, err := strconv.ParseFloat(metricValStr, 64)
if err != nil {
t.Fatalf("failure in parsing of metric's value '%s': %s\n%s", crtExpireTimeMetricName, metricValStr, err)
}

require.GreaterOrEqual(t, crtExpireAllowedDiffFromExpected, math.Abs(metricVal-wantMetricsValue), "seconds till cert expires value")
}

func validateMetricsCrtExpireTimeLabels(t *testing.T, metricStr, wantMetricsLabels string) {
labels := strings.Split(metricStr, "{")
if len(labels) < 2 {
t.Fatalf("unexpected format of metric '%s': %s", crtExpireTimeMetricName, metricStr)
}

labels = strings.Split(labels[1], "}")
require.Equal(t, labels[0], wantMetricsLabels, "seconds till cert expires labels")
}

func singleCertViciMessages(createX509CrtFn func() ([]byte, error)) func() []*vici.Message {
return func() []*vici.Message {
crtMsg := vici.NewMessage()
crtMsg.Set("type", "X509")

crt, err := createX509CrtFn()
if err != nil {
return []*vici.Message{}
}

crtMsg.Set("data", string(crt))
return []*vici.Message{crtMsg}
}
}

func createSingleX509Crt() ([]byte, error) {
return createSingleX509CrtWithAlternateNames([]string{}, []string{}, []net.IP{}, []*url.URL{})
}
Expand Down Expand Up @@ -808,6 +898,22 @@ func createDoubleX509Crt() ([]byte, []byte, error) {
return crt1, crt2, nil
}

func createExpiredX509Crt() ([]byte, error) {
tz, err := time.LoadLocation("UTC")
if err != nil {
return nil, err
}

return createX509Crt(
pkix.Name{
Organization: []string{"Org1"},
CommonName: "Test",
},
time.Date(2024, 1, 1, 12, 0, 0, 0, tz),
time.Date(2024, 12, 1, 12, 0, 0, 0, tz),
)
}

func createX509Crt(subject pkix.Name, notBefore, notAfter time.Time) ([]byte, error) {
return createX509CrtWithAlternateNames(subject, notBefore, notAfter, []string{}, []string{}, []net.IP{}, []*url.URL{})
}
Expand Down