-
Notifications
You must be signed in to change notification settings - Fork 5.6k
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
Changes from all commits
54abdc2
79c28d6
bd12da1
7001319
b4a4908
7d93a69
146d616
e53e1e7
d0e540a
4d28028
8649314
0e8cfe6
35b29c4
8a1f0e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
|
||
## 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 | ||
``` |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have removed the |
||
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()) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aren't these already ints? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it's |
||
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}, | ||
} | ||
}) | ||
} |
There was a problem hiding this comment.
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