Adding x509_cert input plugin (#3768)
This commit is contained in:
parent
019d265167
commit
a897b84049
|
@ -124,6 +124,7 @@ import (
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/webhooks"
|
_ "github.com/influxdata/telegraf/plugins/inputs/webhooks"
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/win_perf_counters"
|
_ "github.com/influxdata/telegraf/plugins/inputs/win_perf_counters"
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/win_services"
|
_ "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/zfs"
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/zipkin"
|
_ "github.com/influxdata/telegraf/plugins/inputs/zipkin"
|
||||||
_ "github.com/influxdata/telegraf/plugins/inputs/zookeeper"
|
_ "github.com/influxdata/telegraf/plugins/inputs/zookeeper"
|
||||||
|
|
|
@ -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
|
||||||
|
```
|
|
@ -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
|
||||||
|
|
||||||
|
## 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()
|
||||||
|
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())
|
||||||
|
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},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,205 @@
|
||||||
|
package x509_cert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/influxdata/telegraf"
|
||||||
|
"github.com/influxdata/telegraf/internal"
|
||||||
|
"github.com/influxdata/telegraf/testutil"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pki = testutil.NewPKI("../../../testutil/pki")
|
||||||
|
|
||||||
|
// Make sure X509Cert implements telegraf.Input
|
||||||
|
var _ telegraf.Input = &X509Cert{}
|
||||||
|
|
||||||
|
func TestGatherRemote(t *testing.T) {
|
||||||
|
if testing.Short() {
|
||||||
|
t.Skip("Skipping network-dependent test in short mode.")
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpfile, err := ioutil.TempFile("", "example")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer os.Remove(tmpfile.Name())
|
||||||
|
|
||||||
|
if _, err := tmpfile.Write([]byte(pki.ReadServerCert())); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
server string
|
||||||
|
timeout time.Duration
|
||||||
|
close bool
|
||||||
|
unset bool
|
||||||
|
noshake bool
|
||||||
|
error bool
|
||||||
|
}{
|
||||||
|
{name: "wrong port", server: ":99999", error: true},
|
||||||
|
{name: "no server", timeout: 5},
|
||||||
|
{name: "successful https", server: "https://example.org:443", timeout: 5},
|
||||||
|
{name: "successful file", server: "file://" + tmpfile.Name(), timeout: 5},
|
||||||
|
{name: "unsupported scheme", server: "foo://", timeout: 5, error: true},
|
||||||
|
{name: "no certificate", timeout: 5, unset: true, error: true},
|
||||||
|
{name: "closed connection", close: true, error: true},
|
||||||
|
{name: "no handshake", timeout: 5, noshake: true, error: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
pair, err := tls.X509KeyPair([]byte(pki.ReadServerCert()), []byte(pki.ReadServerKey()))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
Certificates: []tls.Certificate{pair},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if test.unset {
|
||||||
|
config.Certificates = nil
|
||||||
|
config.GetCertificate = func(i *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ln, err := tls.Listen("tcp", ":0", config)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
sconn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if test.close {
|
||||||
|
sconn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
serverConfig := config.Clone()
|
||||||
|
|
||||||
|
srv := tls.Server(sconn, serverConfig)
|
||||||
|
if test.noshake {
|
||||||
|
srv.Close()
|
||||||
|
}
|
||||||
|
if err := srv.Handshake(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if test.server == "" {
|
||||||
|
test.server = "tcp://" + ln.Addr().String()
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := X509Cert{
|
||||||
|
Sources: []string{test.server},
|
||||||
|
Timeout: internal.Duration{Duration: test.timeout},
|
||||||
|
}
|
||||||
|
|
||||||
|
sc.InsecureSkipVerify = true
|
||||||
|
testErr := false
|
||||||
|
|
||||||
|
acc := testutil.Accumulator{}
|
||||||
|
err = sc.Gather(&acc)
|
||||||
|
if err != nil {
|
||||||
|
testErr = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if testErr != test.error {
|
||||||
|
t.Errorf("%s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGatherLocal(t *testing.T) {
|
||||||
|
wrongCert := fmt.Sprintf("-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n", base64.StdEncoding.EncodeToString([]byte("test")))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mode os.FileMode
|
||||||
|
content string
|
||||||
|
error bool
|
||||||
|
}{
|
||||||
|
{name: "permission denied", mode: 0001, error: true},
|
||||||
|
{name: "not a certificate", mode: 0640, content: "test", error: true},
|
||||||
|
{name: "wrong certificate", mode: 0640, content: wrongCert, error: true},
|
||||||
|
{name: "correct certificate", mode: 0640, content: pki.ReadServerCert()},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
f, err := ioutil.TempFile("", "x509_cert")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = f.Write([]byte(test.content))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = f.Chmod(test.mode)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = f.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
|
||||||
|
sc := X509Cert{
|
||||||
|
Sources: []string{f.Name()},
|
||||||
|
}
|
||||||
|
|
||||||
|
error := false
|
||||||
|
|
||||||
|
acc := testutil.Accumulator{}
|
||||||
|
err = sc.Gather(&acc)
|
||||||
|
if err != nil {
|
||||||
|
error = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if error != test.error {
|
||||||
|
t.Errorf("%s", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStrings(t *testing.T) {
|
||||||
|
sc := X509Cert{}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
method string
|
||||||
|
returned string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{name: "description", method: "Description", returned: sc.Description(), expected: description},
|
||||||
|
{name: "sample config", method: "SampleConfig", returned: sc.SampleConfig(), expected: sampleConfig},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if test.returned != test.expected {
|
||||||
|
t.Errorf("Expected method %s to return '%s', found '%s'.", test.method, test.expected, test.returned)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue