@cryptotaxi247 / netdata-1 / commits / a2b6132a4

improvement(go.d/x509check): support checking full chain expiry time (#19001)

Ilya Mashchenko committed Nov 12, 2024 at 18:20 UTC a2b6132a470d27365301bd8e6205056964675431
9 files changed +115 -114
src/go/plugin/go.d/modules/x509check/charts.go
+42 -20
@@ -2,42 +2,64 @@
2
3 package x509check
4
5 -import "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
5 +import (
6 + "fmt"
7 + "strconv"
8
7 -var (
8 - baseCharts = module.Charts{
9 - timeUntilExpirationChart.Copy(),
10 - }
11 - withRevocationCharts = module.Charts{
12 - timeUntilExpirationChart.Copy(),
13 - revocationStatusChart.Copy(),
14 - }
9 + "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
10 +)
11
16 - timeUntilExpirationChart = module.Chart{
17 - ID: "time_until_expiration",
12 +var certChartsTmpl = module.Charts{
13 + certTimeUntilExpirationChartTmpl.Copy(),
14 + certRevocationStatusChartTmpl.Copy(),
15 +}
16 +
17 +var (
18 + certTimeUntilExpirationChartTmpl = module.Chart{
19 + ID: "cert_depth%d_time_until_expiration",
20 Title: "Time Until Certificate Expiration",
21 Units: "seconds",
22 Fam: "expiration time",
23 Ctx: "x509check.time_until_expiration",
24 Opts: module.Opts{StoreFirst: true},
25 Dims: module.Dims{
24 - {ID: "expiry"},
25 - },
26 - Vars: module.Vars{
27 - {ID: "days_until_expiration_warning"},
28 - {ID: "days_until_expiration_critical"},
26 + {ID: "cert_depth%d_expiry", Name: "expiry"},
27 },
28 }
31 - revocationStatusChart = module.Chart{
32 - ID: "revocation_status",
29 + certRevocationStatusChartTmpl = module.Chart{
30 + ID: "cert_depth%d_revocation_status",
31 Title: "Revocation Status",
32 Units: "boolean",
33 Fam: "revocation",
34 Ctx: "x509check.revocation_status",
35 Opts: module.Opts{StoreFirst: true},
36 Dims: module.Dims{
39 - {ID: "not_revoked"},
40 - {ID: "revoked"},
37 + {ID: "cert_depth%d_not_revoked", Name: "not_revoked"},
38 + {ID: "cert_depth%d_revoked", Name: "revoked"},
39 },
40 }
41 )
42 +
43 +func (x *X509Check) addCertCharts(commonName string, depth int) {
44 + charts := certChartsTmpl.Copy()
45 +
46 + if depth > 0 || !x.CheckRevocation {
47 + _ = charts.Remove(certRevocationStatusChartTmpl.ID)
48 + }
49 +
50 + for _, chart := range *charts {
51 + chart.ID = fmt.Sprintf(chart.ID, depth)
52 + chart.Labels = []module.Label{
53 + {Key: "source", Value: x.Source},
54 + {Key: "common_name", Value: commonName},
55 + {Key: "depth", Value: strconv.Itoa(depth)},
56 + }
57 + for _, dim := range chart.Dims {
58 + dim.ID = fmt.Sprintf(dim.ID, depth)
59 + }
60 + }
61 +
62 + if err := x.Charts().Add(*charts...); err != nil {
63 + x.Warningf("failed to add charts for '%s': %v", commonName, err)
64 + }
65 +}
src/go/plugin/go.d/modules/x509check/collect.go
+33 -23
@@ -7,6 +7,8 @@ import (
7 "fmt"
8 "time"
9
10 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/metrix"
11 +
12 "github.com/cloudflare/cfssl/revoke"
13 )
14
@@ -22,37 +24,45 @@ func (x *X509Check) collect() (map[string]int64, error) {
24
25 mx := make(map[string]int64)
26
25 - x.collectExpiration(mx, certs)
26 - if x.CheckRevocation {
27 - x.collectRevocation(mx, certs)
27 + if err := x.collectCertificates(mx, certs); err != nil {
28 + return nil, err
29 }
30
31 return mx, nil
32 }
33
33 -func (x *X509Check) collectExpiration(mx map[string]int64, certs []*x509.Certificate) {
34 - expiry := time.Until(certs[0].NotAfter).Seconds()
35 - mx["expiry"] = int64(expiry)
36 - mx["days_until_expiration_warning"] = x.DaysUntilWarn
37 - mx["days_until_expiration_critical"] = x.DaysUntilCritical
34 +func (x *X509Check) collectCertificates(mx map[string]int64, certs []*x509.Certificate) error {
35 + for i, cert := range certs {
36 + cn := cert.Subject.CommonName
37
39 -}
38 + if !x.seenCerts[cn] {
39 + x.seenCerts[cn] = true
40 + x.addCertCharts(cn, i)
41 + }
42
41 -func (x *X509Check) collectRevocation(mx map[string]int64, certs []*x509.Certificate) {
42 - rev, ok, err := revoke.VerifyCertificateError(certs[0])
43 - if err != nil {
44 - x.Debug(err)
45 - }
46 - if !ok {
47 - return
48 - }
43 + px := fmt.Sprintf("cert_depth%d_", i)
44 +
45 + expiry := int64(time.Until(cert.NotAfter).Seconds())
46
50 - mx["revoked"] = 0
51 - mx["not_revoked"] = 0
47 + mx[px+"expiry"] = expiry
48
53 - if rev {
54 - mx["revoked"] = 1
55 - } else {
56 - mx["not_revoked"] = 1
49 + if i == 0 && x.CheckRevocation {
50 + rev, ok, err := revoke.VerifyCertificateError(certs[0])
51 + if err != nil {
52 + x.Debug(err)
53 + continue
54 + }
55 + if !ok {
56 + continue
57 + }
58 + mx[px+"revoked"] = metrix.Bool(rev)
59 + mx[px+"not_revoked"] = metrix.Bool(!rev)
60 + }
61 +
62 + if !x.CheckFullChain {
63 + break
64 + }
65 }
66 +
67 + return nil
68 }
src/go/plugin/go.d/modules/x509check/config_schema.json
+8 -18
@@ -23,25 +23,16 @@
23 "minimum": 0.5,
24 "default": 1
25 },
26 + "check_full_chain": {
27 + "title": "Full chain",
28 + "description": "Monitor expiration time for all certificates in the SSL/TLS chain, including intermediate and root certificates.",
29 + "type": "boolean"
30 + },
31 "check_revocation_status": {
27 - "title": "Revocation status check",
32 + "title": "Revocation status",
33 "description": "Whether to check the revocation status of the certificate.",
34 "type": "boolean"
35 },
31 - "days_until_expiration_warning": {
32 - "title": "Days until warning",
33 - "description": "Number of days before the alarm status is set to warning.",
34 - "type": "integer",
35 - "minimum": 1,
36 - "default": 14
37 - },
38 - "days_until_expiration_critical": {
39 - "title": "Days until critical",
40 - "description": "Number of days before the alarm status is set to critical.",
41 - "type": "integer",
42 - "minimum": 1,
43 - "default": 7
44 - },
36 "tls_skip_verify": {
37 "title": "Skip TLS verification",
38 "description": "If set, TLS certificate verification will be skipped.",
@@ -94,9 +85,8 @@
85 "update_every",
86 "source",
87 "timeout",
97 - "check_revocation_status",
98 - "days_until_expiration_warning",
99 - "days_until_expiration_critical"
88 + "check_full_chain",
89 + "check_revocation_status"
90 ]
91 },
92 {
src/go/plugin/go.d/modules/x509check/init.go
-20
@@ -4,8 +4,6 @@ package x509check
4
5 import (
6 "errors"
7 -
8 - "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
7 )
8
9 func (x *X509Check) validateConfig() error {
@@ -18,21 +16,3 @@ func (x *X509Check) validateConfig() error {
16 func (x *X509Check) initProvider() (provider, error) {
17 return newProvider(x.Config)
18 }
21 -
22 -func (x *X509Check) initCharts() *module.Charts {
23 - var charts *module.Charts
24 - if x.CheckRevocation {
25 - charts = withRevocationCharts.Copy()
26 - } else {
27 - charts = baseCharts.Copy()
28 - }
29 -
30 - for _, chart := range *charts {
31 - chart.Labels = []module.Label{
32 - {Key: "source", Value: x.Source},
33 - }
34 - }
35 -
36 - return charts
37 -
38 -}
src/go/plugin/go.d/modules/x509check/metadata.yaml
+11 -11
@@ -62,13 +62,9 @@ modules:
62 description: "Certificate source. Allowed schemes: https, tcp, tcp4, tcp6, udp, udp4, udp6, file, smtp."
63 default_value: ""
64 required: false
65 - - name: days_until_expiration_warning
66 - description: Number of days before the alarm status is warning.
67 - default_value: 30
68 - required: false
69 - - name: days_until_expiration_critical
70 - description: Number of days before the alarm status is critical.
71 - default_value: 15
65 + - name: check_full_chain
66 + description: Monitor expiration time for all certificates in the SSL/TLS chain, including intermediate and root certificates.
67 + default_value: false
68 required: false
69 - name: check_revocation_status
70 description: Whether to check the revocation status of the certificate.
@@ -138,11 +134,11 @@ modules:
134 alerts:
135 - name: x509check_days_until_expiration
136 metric: x509check.time_until_expiration
141 - info: "Time until x509 certificate expires for ${label:source}"
137 + info: "SSL cert expiring soon (${label:source} cn:${label:common_name})"
138 link: https://github.com/netdata/netdata/blob/master/src/health/health.d/x509check.conf
139 - name: x509check_revocation_status
140 metric: x509check.revocation_status
145 - info: "x509 certificate revocation status for ${label:source}"
141 + info: "SSL cert revoked (${label:source})"
142 link: https://github.com/netdata/netdata/blob/master/src/health/health.d/x509check.conf
143 metrics:
144 folding:
@@ -152,10 +148,14 @@ modules:
148 availability: []
149 scopes:
150 - name: source
155 - description: These metrics refer to the configured source.
151 + description: These metrics refer to the SSL certificate.
152 labels:
153 - name: source
158 - description: Configured source.
154 + description: Same as the "source" configuration option.
155 + - name: common_name
156 + description: The common name (CN) extracted from the certificate.
157 + - name: depth
158 + description: The depth of the certificate within the certificate chain. The leaf certificate has a depth of 0, and subsequent certificates (intermediate certificates) have increasing depth values. The root certificate is at the highest depth.
159 metrics:
160 - name: x509check.time_until_expiration
161 description: Time Until Certificate Expiration
src/go/plugin/go.d/modules/x509check/testdata/config.json
+1 -2
@@ -6,7 +6,6 @@
6 "tls_cert": "ok",
7 "tls_key": "ok",
8 "tls_skip_verify": true,
9 - "days_until_expiration_warning": 123,
10 - "days_until_expiration_critical": 123,
9 + "check_full_chain": true,
10 "check_revocation_status": true
11 }
src/go/plugin/go.d/modules/x509check/testdata/config.yaml
+1 -2
@@ -5,6 +5,5 @@ tls_ca: "ok"
5 tls_cert: "ok"
6 tls_key: "ok"
7 tls_skip_verify: yes
8 -days_until_expiration_warning: 123
9 -days_until_expiration_critical: 123
8 +check_full_chain: yes
9 check_revocation_status: yes
src/go/plugin/go.d/modules/x509check/x509check.go
+13 -12
@@ -33,21 +33,22 @@ func init() {
33 func New() *X509Check {
34 return &X509Check{
35 Config: Config{
36 - Timeout: confopt.Duration(time.Second * 2),
37 - DaysUntilWarn: 14,
38 - DaysUntilCritical: 7,
36 + Timeout: confopt.Duration(time.Second * 2),
37 + CheckFullChain: false,
38 },
39 +
40 + charts: &module.Charts{},
41 + seenCerts: make(map[string]bool),
42 }
43 }
44
45 type Config struct {
44 - UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
45 - Source string `yaml:"source" json:"source"`
46 - Timeout confopt.Duration `yaml:"timeout,omitempty" json:"timeout"`
47 - DaysUntilWarn int64 `yaml:"days_until_expiration_warning,omitempty" json:"days_until_expiration_warning"`
48 - DaysUntilCritical int64 `yaml:"days_until_expiration_critical,omitempty" json:"days_until_expiration_critical"`
49 - CheckRevocation bool `yaml:"check_revocation_status" json:"check_revocation_status"`
50 - tlscfg.TLSConfig `yaml:",inline" json:""`
46 + UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
47 + Source string `yaml:"source" json:"source"`
48 + Timeout confopt.Duration `yaml:"timeout,omitempty" json:"timeout"`
49 + CheckFullChain bool `yaml:"check_full_chain" json:"check_full_chain"`
50 + CheckRevocation bool `yaml:"check_revocation_status" json:"check_revocation_status"`
51 + tlscfg.TLSConfig `yaml:",inline" json:""`
52 }
53
54 type X509Check struct {
@@ -57,6 +58,8 @@ type X509Check struct {
58 charts *module.Charts
59
60 prov provider
61 +
62 + seenCerts map[string]bool
63 }
64
65 func (x *X509Check) Configuration() any {
@@ -74,8 +77,6 @@ func (x *X509Check) Init() error {
77 }
78 x.prov = prov
79
77 - x.charts = x.initCharts()
78 -
80 return nil
81 }
82
src/health/health.d/x509check.conf
+6 -6
@@ -7,10 +7,10 @@ component: x509 certificates
7 calc: $expiry / 86400
8 units: days
9 every: 60s
10 - warn: $this < $days_until_expiration_warning
11 - crit: $this < $days_until_expiration_critical
12 - summary: x509 certificate expiration for ${label:source}
13 - info: Time until x509 certificate expires for ${label:source}
10 + warn: $this < 14
11 + crit: $this < 7
12 + summary: SSL cert expiring soon (${label:source} cn:${label:common_name})
13 + info: SSL cert expiring soon (${label:source} cn:${label:common_name})
14 to: webmaster
15
16 template: x509check_revocation_status
@@ -22,6 +22,6 @@ component: x509 certificates
22 units: status
23 every: 60s
24 crit: $this == 1
25 - summary: x509 certificate revocation status for ${label:source}
26 - info: x509 certificate revocation status for ${label:source}
25 + summary: SSL cert revoked (${label:source})
26 + info: SSL cert revoked (${label:source})
27 to: webmaster