From 19530dc2f71e8ec3d862ee39112ff28d422fa9dc Mon Sep 17 00:00:00 2001 From: greg linton Date: Fri, 19 Jul 2019 18:34:08 -0600 Subject: [PATCH 1/3] Add certificate verification status to x509_cert input --- plugins/inputs/x509_cert/README.md | 12 ++-- plugins/inputs/x509_cert/dev/telegraf.conf | 3 +- plugins/inputs/x509_cert/x509_cert.go | 71 +++++++++++++++------- 3 files changed, 56 insertions(+), 30 deletions(-) diff --git a/plugins/inputs/x509_cert/README.md b/plugins/inputs/x509_cert/README.md index a85d05463568e..ecfe5dbbffaac 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 + - validation - fields: + - validation (int) + - validation_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,validation=success age=7693222i,enddate=1871249033i,expiry=307666777i,startdate=1555889033i,validation=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,validation=fail age=20219055i,enddate=1606910400i,expiry=43328144i,startdate=1543363200i,validation=1i,validation_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,validation=success age=200838255i,enddate=1678276800i,expiry=114694544i,startdate=1362744000i,validation=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,validation=success age=400465455i,enddate=1952035200i,expiry=388452944i,startdate=1163116800i,validation=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..d0168fef88938 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" @@ -53,14 +50,26 @@ 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, *tls.Config, error) { + tlsCfg, err := c.ClientConfig.TLSConfig() + if err != nil { + return nil, nil, err + } + if tlsCfg == nil { + tlsCfg = &tls.Config{} } switch u.Scheme { @@ -70,51 +79,44 @@ 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 + return nil, nil, err } defer ipConn.Close() - if tlsCfg == nil { - tlsCfg = &tls.Config{} - } tlsCfg.ServerName = u.Hostname() + tlsCfg.InsecureSkipVerify = true conn := tls.Client(ipConn, tlsCfg) defer conn.Close() hsErr := conn.Handshake() if hsErr != nil { - return nil, hsErr + return nil, nil, hsErr } certs := conn.ConnectionState().PeerCertificates - return certs, nil + return certs, tlsCfg, nil case "file": content, err := ioutil.ReadFile(u.Path) if err != nil { - return nil, err + return nil, nil, err } block, _ := pem.Decode(content) if block == nil { - return nil, fmt.Errorf("failed to parse certificate PEM") + return nil, nil, fmt.Errorf("failed to parse certificate PEM") } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { - return nil, err + return nil, nil, err } - return []*x509.Certificate{cert}, nil + return []*x509.Certificate{cert}, tlsCfg, nil default: - return nil, fmt.Errorf("unsuported scheme '%s' in location %s\n", u.Scheme, location) + return nil, nil, fmt.Errorf("unsuported scheme '%s' in location %s", u.Scheme, u.String()) } } @@ -164,15 +166,38 @@ 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) + } + + certs, tlsCfg, 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) + opts := x509.VerifyOptions{} + if i == 0 { + opts.DNSName = u.Hostname() + } + if tlsCfg.RootCAs != nil { + opts.Roots = tlsCfg.RootCAs + } + + _, err = cert.Verify(opts) + if err == nil { + tags["validation"] = "success" + fields["validation"] = 0 + } else { + tags["validation"] = "fail" + fields["validation"] = 1 + fields["validation_error"] = err.Error() + } + acc.AddFields("x509_cert", fields, tags) } } From e6493043573e565e8175b2efef5dfb7d0b819672 Mon Sep 17 00:00:00 2001 From: greg linton Date: Mon, 22 Jul 2019 16:24:41 -0600 Subject: [PATCH 2/3] Address feedback --- plugins/inputs/x509_cert/x509_cert.go | 64 +++++++++++++--------- plugins/inputs/x509_cert/x509_cert_test.go | 5 ++ 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/plugins/inputs/x509_cert/x509_cert.go b/plugins/inputs/x509_cert/x509_cert.go index d0168fef88938..1cddc23fcc284 100644 --- a/plugins/inputs/x509_cert/x509_cert.go +++ b/plugins/inputs/x509_cert/x509_cert.go @@ -37,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 } @@ -63,15 +64,7 @@ func (c *X509Cert) locationToURL(location string) (*url.URL, error) { return u, nil } -func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certificate, *tls.Config, error) { - tlsCfg, err := c.ClientConfig.TLSConfig() - if err != nil { - return nil, nil, err - } - if tlsCfg == nil { - tlsCfg = &tls.Config{} - } - +func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certificate, error) { switch u.Scheme { case "https": u.Scheme = "tcp" @@ -81,42 +74,42 @@ func (c *X509Cert) getCert(u *url.URL, timeout time.Duration) ([]*x509.Certifica case "tcp", "tcp4", "tcp6": ipConn, err := net.DialTimeout(u.Scheme, u.Host, timeout) if err != nil { - return nil, nil, err + return nil, err } defer ipConn.Close() - tlsCfg.ServerName = u.Hostname() - tlsCfg.InsecureSkipVerify = true - 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() if hsErr != nil { - return nil, nil, hsErr + return nil, hsErr } certs := conn.ConnectionState().PeerCertificates - return certs, tlsCfg, nil + return certs, nil case "file": content, err := ioutil.ReadFile(u.Path) if err != nil { - return nil, nil, err + return nil, err } block, _ := pem.Decode(content) if block == nil { - return nil, nil, fmt.Errorf("failed to parse certificate PEM") + return nil, fmt.Errorf("failed to parse certificate PEM") } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { - return nil, nil, err + return nil, err } - return []*x509.Certificate{cert}, tlsCfg, nil + return []*x509.Certificate{cert}, nil default: - return nil, nil, fmt.Errorf("unsuported scheme '%s' in location %s", u.Scheme, u.String()) + return nil, fmt.Errorf("unsuported scheme '%s' in location %s", u.Scheme, u.String()) } } @@ -169,9 +162,10 @@ func (c *X509Cert) Gather(acc telegraf.Accumulator) error { u, err := c.locationToURL(location) if err != nil { acc.AddError(err) + return nil } - certs, tlsCfg, err := c.getCert(u, c.Timeout.Duration*time.Second) + 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())) } @@ -180,21 +174,23 @@ func (c *X509Cert) Gather(acc telegraf.Accumulator) error { 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 tlsCfg.RootCAs != nil { - opts.Roots = tlsCfg.RootCAs + if c.tlsCfg.RootCAs != nil { + opts.Roots = c.tlsCfg.RootCAs } _, err = cert.Verify(opts) if err == nil { - tags["validation"] = "success" - fields["validation"] = 0 + tags["verification"] = "valid" + fields["verification"] = 0 } else { - tags["validation"] = "fail" - fields["validation"] = 1 + tags["verification"] = "invalid" + fields["verification"] = 1 fields["validation_error"] = err.Error() } @@ -205,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) From 651ce0d09a34d80d6dc564672c08593e89856e92 Mon Sep 17 00:00:00 2001 From: greg linton Date: Mon, 22 Jul 2019 16:46:25 -0600 Subject: [PATCH 3/3] Document renamed tag/fields --- plugins/inputs/x509_cert/README.md | 14 +++++++------- plugins/inputs/x509_cert/x509_cert.go | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/plugins/inputs/x509_cert/README.md b/plugins/inputs/x509_cert/README.md index ecfe5dbbffaac..450dd3d1039e0 100644 --- a/plugins/inputs/x509_cert/README.md +++ b/plugins/inputs/x509_cert/README.md @@ -32,10 +32,10 @@ file or network connection. - country - province - locality - - validation + - verification - fields: - - validation (int) - - validation_error (string) + - verification_code (int) + - verification_error (string) - expiry (int, seconds) - age (int, seconds) - startdate (int, seconds) @@ -45,8 +45,8 @@ file or network connection. ### Example output ``` -x509_cert,common_name=ubuntu,source=/etc/ssl/certs/ssl-cert-snakeoil.pem,validation=success age=7693222i,enddate=1871249033i,expiry=307666777i,startdate=1555889033i,validation=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,validation=fail age=20219055i,enddate=1606910400i,expiry=43328144i,startdate=1543363200i,validation=1i,validation_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,validation=success age=200838255i,enddate=1678276800i,expiry=114694544i,startdate=1362744000i,validation=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,validation=success age=400465455i,enddate=1952035200i,expiry=388452944i,startdate=1163116800i,validation=0i 1563582256000000000 +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/x509_cert.go b/plugins/inputs/x509_cert/x509_cert.go index 1cddc23fcc284..8558378d14a83 100644 --- a/plugins/inputs/x509_cert/x509_cert.go +++ b/plugins/inputs/x509_cert/x509_cert.go @@ -187,11 +187,11 @@ func (c *X509Cert) Gather(acc telegraf.Accumulator) error { _, err = cert.Verify(opts) if err == nil { tags["verification"] = "valid" - fields["verification"] = 0 + fields["verification_code"] = 0 } else { tags["verification"] = "invalid" - fields["verification"] = 1 - fields["validation_error"] = err.Error() + fields["verification_code"] = 1 + fields["verification_error"] = err.Error() } acc.AddFields("x509_cert", fields, tags)