Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding x509_cert input plugin #3768

Merged
merged 14 commits into from
Jul 30, 2018
1 change: 1 addition & 0 deletions plugins/inputs/all/all.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ import (
_ "github.com/influxdata/telegraf/plugins/inputs/webhooks"
_ "github.com/influxdata/telegraf/plugins/inputs/win_perf_counters"
_ "github.com/influxdata/telegraf/plugins/inputs/win_services"
_ "github.com/influxdata/telegraf/plugins/inputs/x509_cert"
_ "github.com/influxdata/telegraf/plugins/inputs/zfs"
_ "github.com/influxdata/telegraf/plugins/inputs/zipkin"
_ "github.com/influxdata/telegraf/plugins/inputs/zookeeper"
Expand Down
45 changes: 45 additions & 0 deletions plugins/inputs/x509_cert/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# X509 Cert Input Plugin

This plugin provides information about X509 certificate accessible via local
file or network connection.


### Configuration

```toml
# Reads metrics from a SSL certificate
[[inputs.x509_cert]]
## List certificate sources
sources = ["/etc/ssl/certs/ssl-cert-snakeoil.pem", "https://example.org"]

## Timeout for SSL connection
# timeout = 5s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add quotes to be valid toml


## Optional TLS Config
# 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
```


### Metrics

- `x509_cert`
- tags:
- `source` - source of the certificate
- fields:
- `expiry` (int, seconds)
- `age` (int, seconds)
- `startdate` (int, seconds)
- `enddate` (int, seconds)


### 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
```
163 changes: 163 additions & 0 deletions plugins/inputs/x509_cert/x509_cert.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
// Package x509_cert reports metrics from an SSL certificate.
package x509_cert

import (
"crypto/tls"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"net"
"net/url"
"strings"
"time"

"github.com/influxdata/telegraf"
"github.com/influxdata/telegraf/internal"
_tls "github.com/influxdata/telegraf/internal/tls"
"github.com/influxdata/telegraf/plugins/inputs"
)

const sampleConfig = `
## List certificate sources
sources = ["/etc/ssl/certs/ssl-cert-snakeoil.pem", "tcp://example.org:443"]

## Timeout for SSL connection
# timeout = 5s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add quotes to be valid toml


## Optional TLS Config
# 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"

// X509Cert holds the configuration of the plugin.
type X509Cert struct {
Sources []string `toml:"sources"`
Timeout internal.Duration `toml:"timeout"`
_tls.ClientConfig
}

// Description returns description of the plugin.
func (c *X509Cert) Description() string {
return description
}

// SampleConfig returns configuration sample for the plugin.
func (c *X509Cert) SampleConfig() string {
return sampleConfig
}

func (c *X509Cert) getCert(location string, timeout time.Duration) ([]*x509.Certificate, 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())
}

switch u.Scheme {
case "https":
u.Scheme = "tcp"
fallthrough
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()

conn := tls.Client(ipConn, tlsCfg)
defer conn.Close()

hsErr := conn.Handshake()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious about what happens when a certificate has an invalid hostname or the cert chain is invalid? What I think would be ideal is if we still called acc.AddFields( but with valid=false and potentially with more detailed information. If this is possible, Gather would only return an error if the network connection fails or we can't get any certificates.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have removed the valid metric as per your request bellow (line 116).

if hsErr != nil {
return nil, hsErr
}

certs := conn.ConnectionState().PeerCertificates

return certs, nil
case "file":
content, err := ioutil.ReadFile(u.Path)
if err != nil {
return nil, err
}

block, _ := pem.Decode(content)
if block == nil {
return nil, fmt.Errorf("failed to parse certificate PEM")
}

cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, err
}

return []*x509.Certificate{cert}, nil
default:
return nil, fmt.Errorf("unsuported scheme '%s' in location %s\n", u.Scheme, location)
}
}

func getFields(cert *x509.Certificate, now time.Time) map[string]interface{} {
age := int(now.Sub(cert.NotBefore).Seconds())
expiry := int(cert.NotAfter.Sub(now).Seconds())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't these already ints?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

startdate := cert.NotBefore.Unix()
enddate := cert.NotAfter.Unix()

fields := map[string]interface{}{
"age": age,
"expiry": expiry,
"startdate": startdate,
"enddate": enddate,
}

return fields
}

// Gather adds metrics into the accumulator.
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)
if err != nil {
return fmt.Errorf("cannot get SSL cert '%s': %s", location, err.Error())
}

tags := map[string]string{
"source": location,
}

for _, cert := range certs {
fields := getFields(cert, now)

acc.AddFields("x509_cert", fields, tags)
}
}

return nil
}

func init() {
inputs.Add("x509_cert", func() telegraf.Input {
return &X509Cert{
Sources: []string{},
Timeout: internal.Duration{Duration: 5},
}
})
}
Loading