diff --git a/plugins/inputs/x509_cert/README.md b/plugins/inputs/x509_cert/README.md index a85d05463568e..450dd3d1039e0 100644 --- a/plugins/inputs/x509_cert/README.md +++ b/plugins/inputs/x509_cert/README.md @@ -19,9 +19,6 @@ file or network connection. # tls_ca = "/etc/telegraf/ca.pem" # tls_cert = "/etc/telegraf/cert.pem" # tls_key = "/etc/telegraf/key.pem" - - ## Use TLS but skip chain & host verification - # insecure_skip_verify = false ``` @@ -35,7 +32,10 @@ file or network connection. - country - province - locality + - verification - fields: + - verification_code (int) + - verification_error (string) - expiry (int, seconds) - age (int, seconds) - startdate (int, seconds) @@ -45,6 +45,8 @@ file or network connection. ### Example output ``` -x509_cert,host=myhost,source=https://example.org age=1753627i,expiry=5503972i,startdate=1516092060i,enddate=1523349660i 1517845687000000000 -x509_cert,host=myhost,source=/etc/ssl/certs/ssl-cert-snakeoil.pem age=7522207i,expiry=308002732i,startdate=1510323480i,enddate=1825848420i 1517845687000000000 +x509_cert,common_name=ubuntu,source=/etc/ssl/certs/ssl-cert-snakeoil.pem,verification=valid age=7693222i,enddate=1871249033i,expiry=307666777i,startdate=1555889033i,verification_code=0i 1563582256000000000 +x509_cert,common_name=www.example.org,country=US,locality=Los\ Angeles,organization=Internet\ Corporation\ for\ Assigned\ Names\ and\ Numbers,organizational_unit=Technology,province=California,source=https://example.org:443,verification=invalid age=20219055i,enddate=1606910400i,expiry=43328144i,startdate=1543363200i,verification_code=1i,verification_error="x509: certificate signed by unknown authority" 1563582256000000000 +x509_cert,common_name=DigiCert\ SHA2\ Secure\ Server\ CA,country=US,organization=DigiCert\ Inc,source=https://example.org:443,verification=valid age=200838255i,enddate=1678276800i,expiry=114694544i,startdate=1362744000i,verification_code=0i 1563582256000000000 +x509_cert,common_name=DigiCert\ Global\ Root\ CA,country=US,organization=DigiCert\ Inc,organizational_unit=www.digicert.com,source=https://example.org:443,verification=valid age=400465455i,enddate=1952035200i,expiry=388452944i,startdate=1163116800i,verification_code=0i 1563582256000000000 ``` diff --git a/plugins/inputs/x509_cert/dev/telegraf.conf b/plugins/inputs/x509_cert/dev/telegraf.conf index 1eda94f02b325..7545997a4d394 100644 --- a/plugins/inputs/x509_cert/dev/telegraf.conf +++ b/plugins/inputs/x509_cert/dev/telegraf.conf @@ -1,5 +1,4 @@ [[inputs.x509_cert]] - sources = ["https://www.influxdata.com:443"] + sources = ["https://expired.badssl.com:443", "https://wrong.host.badssl.com:443"] [[outputs.file]] - files = ["stdout"] diff --git a/plugins/inputs/x509_cert/x509_cert.go b/plugins/inputs/x509_cert/x509_cert.go index 81bcb0d2c15f2..8558378d14a83 100644 --- a/plugins/inputs/x509_cert/x509_cert.go +++ b/plugins/inputs/x509_cert/x509_cert.go @@ -30,9 +30,6 @@ const sampleConfig = ` # tls_ca = "/etc/telegraf/ca.pem" # tls_cert = "/etc/telegraf/cert.pem" # tls_key = "/etc/telegraf/key.pem" - - ## Use TLS but skip chain & host verification - # insecure_skip_verify = false ` const description = "Reads metrics from a SSL certificate" @@ -40,6 +37,7 @@ const description = "Reads metrics from a SSL certificate" type X509Cert struct { Sources []string `toml:"sources"` Timeout internal.Duration `toml:"timeout"` + tlsCfg *tls.Config _tls.ClientConfig } @@ -53,16 +51,20 @@ func (c *X509Cert) SampleConfig() string { return sampleConfig } -func (c *X509Cert) getCert(location string, timeout time.Duration) ([]*x509.Certificate, error) { +func (c *X509Cert) locationToURL(location string) (*url.URL, error) { if strings.HasPrefix(location, "/") { location = "file://" + location } u, err := url.Parse(location) if err != nil { - return nil, fmt.Errorf("failed to parse cert location - %s\n", err.Error()) + return nil, fmt.Errorf("failed to parse cert location - %s", err.Error()) } + return u, nil +} + +func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certificate, error) { switch u.Scheme { case "https": u.Scheme = "tcp" @@ -70,22 +72,15 @@ func (c *X509Cert) getCert(location string, timeout time.Duration) ([]*x509.Cert case "udp", "udp4", "udp6": fallthrough case "tcp", "tcp4", "tcp6": - tlsCfg, err := c.ClientConfig.TLSConfig() - if err != nil { - return nil, err - } - ipConn, err := net.DialTimeout(u.Scheme, u.Host, timeout) if err != nil { return nil, err } defer ipConn.Close() - if tlsCfg == nil { - tlsCfg = &tls.Config{} - } - tlsCfg.ServerName = u.Hostname() - conn := tls.Client(ipConn, tlsCfg) + c.tlsCfg.ServerName = u.Hostname() + c.tlsCfg.InsecureSkipVerify = true + conn := tls.Client(ipConn, c.tlsCfg) defer conn.Close() hsErr := conn.Handshake() @@ -114,7 +109,7 @@ func (c *X509Cert) getCert(location string, timeout time.Duration) ([]*x509.Cert return []*x509.Certificate{cert}, nil default: - return nil, fmt.Errorf("unsuported scheme '%s' in location %s\n", u.Scheme, location) + return nil, fmt.Errorf("unsuported scheme '%s' in location %s", u.Scheme, u.String()) } } @@ -164,15 +159,41 @@ func (c *X509Cert) Gather(acc telegraf.Accumulator) error { now := time.Now() for _, location := range c.Sources { - certs, err := c.getCert(location, c.Timeout.Duration*time.Second) + u, err := c.locationToURL(location) + if err != nil { + acc.AddError(err) + return nil + } + + certs, err := c.getCert(u, c.Timeout.Duration*time.Second) if err != nil { acc.AddError(fmt.Errorf("cannot get SSL cert '%s': %s", location, err.Error())) } - for _, cert := range certs { + for i, cert := range certs { fields := getFields(cert, now) tags := getTags(cert.Subject, location) + // The first certificate is the leaf/end-entity certificate which needs DNS + // name validation against the URL hostname. + opts := x509.VerifyOptions{} + if i == 0 { + opts.DNSName = u.Hostname() + } + if c.tlsCfg.RootCAs != nil { + opts.Roots = c.tlsCfg.RootCAs + } + + _, err = cert.Verify(opts) + if err == nil { + tags["verification"] = "valid" + fields["verification_code"] = 0 + } else { + tags["verification"] = "invalid" + fields["verification_code"] = 1 + fields["verification_error"] = err.Error() + } + acc.AddFields("x509_cert", fields, tags) } } @@ -180,6 +201,20 @@ func (c *X509Cert) Gather(acc telegraf.Accumulator) error { return nil } +func (c *X509Cert) Init() error { + tlsCfg, err := c.ClientConfig.TLSConfig() + if err != nil { + return err + } + if tlsCfg == nil { + tlsCfg = &tls.Config{} + } + + c.tlsCfg = tlsCfg + + return nil +} + func init() { inputs.Add("x509_cert", func() telegraf.Input { return &X509Cert{ diff --git a/plugins/inputs/x509_cert/x509_cert_test.go b/plugins/inputs/x509_cert/x509_cert_test.go index 933676417cf80..188b510d263d9 100644 --- a/plugins/inputs/x509_cert/x509_cert_test.go +++ b/plugins/inputs/x509_cert/x509_cert_test.go @@ -110,6 +110,7 @@ func TestGatherRemote(t *testing.T) { Sources: []string{test.server}, Timeout: internal.Duration{Duration: test.timeout}, } + sc.Init() sc.InsecureSkipVerify = true testErr := false @@ -169,6 +170,7 @@ func TestGatherLocal(t *testing.T) { sc := X509Cert{ Sources: []string{f.Name()}, } + sc.Init() error := false @@ -218,6 +220,7 @@ func TestGatherChain(t *testing.T) { sc := X509Cert{ Sources: []string{f.Name()}, } + sc.Init() error := false @@ -237,6 +240,7 @@ func TestGatherChain(t *testing.T) { func TestStrings(t *testing.T) { sc := X509Cert{} + sc.Init() tests := []struct { name string @@ -265,6 +269,7 @@ func TestGatherCert(t *testing.T) { m := &X509Cert{ Sources: []string{"https://www.influxdata.com:443"}, } + m.Init() var acc testutil.Accumulator err := m.Gather(&acc)