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