2017-08-20 20:30:02 +02:00
|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"crypto/tls"
|
2017-11-06 16:48:34 +02:00
|
|
|
"crypto/x509"
|
2017-08-20 20:30:02 +02:00
|
|
|
"fmt"
|
2019-03-08 19:43:37 +02:00
|
|
|
"io/ioutil"
|
2017-08-20 20:30:02 +02:00
|
|
|
"net/http"
|
|
|
|
"strconv"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
|
|
"github.com/prometheus/common/log"
|
|
|
|
"github.com/prometheus/common/version"
|
2017-10-08 19:25:40 +02:00
|
|
|
"gopkg.in/alecthomas/kingpin.v2"
|
2017-08-20 20:30:02 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
namespace = "ssl"
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
httpsConnectSuccess = prometheus.NewDesc(
|
|
|
|
prometheus.BuildFQName(namespace, "", "https_connect_success"),
|
|
|
|
"If the TLS connection was a success",
|
|
|
|
nil, nil,
|
|
|
|
)
|
|
|
|
notBefore = prometheus.NewDesc(
|
|
|
|
prometheus.BuildFQName(namespace, "", "cert_not_before"),
|
|
|
|
"NotBefore expressed as a Unix Epoch Time",
|
|
|
|
[]string{"serial_no", "issuer_cn"}, nil,
|
|
|
|
)
|
|
|
|
notAfter = prometheus.NewDesc(
|
|
|
|
prometheus.BuildFQName(namespace, "", "cert_not_after"),
|
|
|
|
"NotAfter expressed as a Unix Epoch Time",
|
|
|
|
[]string{"serial_no", "issuer_cn"}, nil,
|
|
|
|
)
|
|
|
|
commonName = prometheus.NewDesc(
|
|
|
|
prometheus.BuildFQName(namespace, "", "cert_subject_common_name"),
|
|
|
|
"Subject Common Name",
|
|
|
|
[]string{"serial_no", "issuer_cn", "subject_cn"}, nil,
|
|
|
|
)
|
|
|
|
subjectAlernativeDNSNames = prometheus.NewDesc(
|
|
|
|
prometheus.BuildFQName(namespace, "", "cert_subject_alternative_dnsnames"),
|
|
|
|
"Subject Alternative DNS Names",
|
|
|
|
[]string{"serial_no", "issuer_cn", "dnsnames"}, nil,
|
|
|
|
)
|
|
|
|
subjectAlernativeIPs = prometheus.NewDesc(
|
|
|
|
prometheus.BuildFQName(namespace, "", "cert_subject_alternative_ips"),
|
2019-03-11 19:13:29 +02:00
|
|
|
"Subject Alternative IPs",
|
2017-08-20 20:30:02 +02:00
|
|
|
[]string{"serial_no", "issuer_cn", "ips"}, nil,
|
|
|
|
)
|
|
|
|
subjectAlernativeEmailAddresses = prometheus.NewDesc(
|
|
|
|
prometheus.BuildFQName(namespace, "", "cert_subject_alternative_emails"),
|
2019-03-11 19:13:29 +02:00
|
|
|
"Subject Alternative Email Addresses",
|
2017-08-20 20:30:02 +02:00
|
|
|
[]string{"serial_no", "issuer_cn", "emails"}, nil,
|
|
|
|
)
|
2019-01-25 08:14:56 +02:00
|
|
|
subjectOrganizationUnits = prometheus.NewDesc(
|
|
|
|
prometheus.BuildFQName(namespace, "", "cert_subject_organization_units"),
|
|
|
|
"Subject Organization Units",
|
|
|
|
[]string{"serial_no", "issuer_cn", "subject_ou"}, nil,
|
|
|
|
)
|
2017-08-20 20:30:02 +02:00
|
|
|
)
|
|
|
|
|
2019-03-11 19:13:29 +02:00
|
|
|
// Exporter is the exporter type...
|
2017-08-20 20:30:02 +02:00
|
|
|
type Exporter struct {
|
2019-03-08 19:43:37 +02:00
|
|
|
target string
|
|
|
|
timeout time.Duration
|
|
|
|
tlsConfig *tls.Config
|
2017-08-20 20:30:02 +02:00
|
|
|
}
|
|
|
|
|
2019-03-11 19:13:29 +02:00
|
|
|
// Describe metrics
|
2017-08-20 20:30:02 +02:00
|
|
|
func (e *Exporter) Describe(ch chan<- *prometheus.Desc) {
|
|
|
|
ch <- httpsConnectSuccess
|
|
|
|
ch <- notAfter
|
|
|
|
ch <- commonName
|
|
|
|
ch <- subjectAlernativeDNSNames
|
|
|
|
ch <- subjectAlernativeIPs
|
|
|
|
ch <- subjectAlernativeEmailAddresses
|
2019-01-25 08:14:56 +02:00
|
|
|
ch <- subjectOrganizationUnits
|
2017-08-20 20:30:02 +02:00
|
|
|
}
|
|
|
|
|
2019-03-11 19:13:29 +02:00
|
|
|
// Collect metrics
|
2017-08-20 20:30:02 +02:00
|
|
|
func (e *Exporter) Collect(ch chan<- prometheus.Metric) {
|
|
|
|
|
|
|
|
// Create the HTTP client and make a get request of the target
|
|
|
|
tr := &http.Transport{
|
2019-03-08 19:43:37 +02:00
|
|
|
TLSClientConfig: e.tlsConfig,
|
2017-08-20 20:30:02 +02:00
|
|
|
}
|
|
|
|
client := &http.Client{
|
2017-10-08 19:01:03 +02:00
|
|
|
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
|
|
|
return http.ErrUseLastResponse
|
|
|
|
},
|
2017-08-20 20:30:02 +02:00
|
|
|
Transport: tr,
|
2017-11-06 16:48:34 +02:00
|
|
|
Timeout: e.timeout,
|
2017-08-20 20:30:02 +02:00
|
|
|
}
|
|
|
|
resp, err := client.Get(e.target)
|
2017-11-06 16:48:34 +02:00
|
|
|
|
2017-08-20 20:30:02 +02:00
|
|
|
if err != nil {
|
|
|
|
log.Errorln(err)
|
|
|
|
ch <- prometheus.MustNewConstMetric(
|
|
|
|
httpsConnectSuccess, prometheus.GaugeValue, 0,
|
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
2017-10-08 19:01:03 +02:00
|
|
|
|
|
|
|
if resp.TLS == nil {
|
|
|
|
log.Errorln("The response from " + e.target + " is unencrypted")
|
|
|
|
ch <- prometheus.MustNewConstMetric(
|
|
|
|
httpsConnectSuccess, prometheus.GaugeValue, 0,
|
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2017-08-20 20:30:02 +02:00
|
|
|
ch <- prometheus.MustNewConstMetric(
|
|
|
|
httpsConnectSuccess, prometheus.GaugeValue, 1,
|
|
|
|
)
|
|
|
|
|
2019-03-11 19:13:29 +02:00
|
|
|
// Remove duplicate certificates from the response
|
|
|
|
peerCertificates := uniq(resp.TLS.PeerCertificates)
|
2017-11-06 16:48:34 +02:00
|
|
|
|
2017-08-20 20:30:02 +02:00
|
|
|
// Loop through returned certificates and create metrics
|
2019-03-11 19:13:29 +02:00
|
|
|
for _, cert := range peerCertificates {
|
2017-08-20 20:30:02 +02:00
|
|
|
|
2019-03-11 19:13:29 +02:00
|
|
|
subjectCN := cert.Subject.CommonName
|
|
|
|
issuerCN := cert.Issuer.CommonName
|
|
|
|
subjectDNSNames := cert.DNSNames
|
|
|
|
subjectEmails := cert.EmailAddresses
|
|
|
|
subjectIPs := cert.IPAddresses
|
|
|
|
serialNum := cert.SerialNumber.String()
|
|
|
|
subjectOUs := cert.Subject.OrganizationalUnit
|
2017-08-20 20:30:02 +02:00
|
|
|
|
|
|
|
if !cert.NotAfter.IsZero() {
|
|
|
|
ch <- prometheus.MustNewConstMetric(
|
2019-03-11 19:13:29 +02:00
|
|
|
notAfter, prometheus.GaugeValue, float64(cert.NotAfter.UnixNano()/1e9), serialNum, issuerCN,
|
2017-08-20 20:30:02 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
if !cert.NotBefore.IsZero() {
|
|
|
|
ch <- prometheus.MustNewConstMetric(
|
2019-03-11 19:13:29 +02:00
|
|
|
notBefore, prometheus.GaugeValue, float64(cert.NotBefore.UnixNano()/1e9), serialNum, issuerCN,
|
2017-08-20 20:30:02 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-03-11 19:13:29 +02:00
|
|
|
if subjectCN != "" {
|
2017-08-20 20:30:02 +02:00
|
|
|
ch <- prometheus.MustNewConstMetric(
|
2019-03-11 19:13:29 +02:00
|
|
|
commonName, prometheus.GaugeValue, 1, serialNum, issuerCN, subjectCN,
|
2017-08-20 20:30:02 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-03-11 19:13:29 +02:00
|
|
|
if len(subjectDNSNames) > 0 {
|
2017-08-20 20:30:02 +02:00
|
|
|
ch <- prometheus.MustNewConstMetric(
|
2019-03-11 19:13:29 +02:00
|
|
|
subjectAlernativeDNSNames, prometheus.GaugeValue, 1, serialNum, issuerCN, ","+strings.Join(subjectDNSNames, ",")+",",
|
2017-08-20 20:30:02 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-03-11 19:13:29 +02:00
|
|
|
if len(subjectEmails) > 0 {
|
2017-08-20 20:30:02 +02:00
|
|
|
ch <- prometheus.MustNewConstMetric(
|
2019-03-11 19:13:29 +02:00
|
|
|
subjectAlernativeEmailAddresses, prometheus.GaugeValue, 1, serialNum, issuerCN, ","+strings.Join(subjectEmails, ",")+",",
|
2017-08-20 20:30:02 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-03-11 19:13:29 +02:00
|
|
|
if len(subjectIPs) > 0 {
|
2017-08-20 20:30:02 +02:00
|
|
|
i := ","
|
2019-03-11 19:13:29 +02:00
|
|
|
for _, ip := range subjectIPs {
|
2017-08-20 20:30:02 +02:00
|
|
|
i = i + ip.String() + ","
|
|
|
|
}
|
|
|
|
ch <- prometheus.MustNewConstMetric(
|
2019-03-11 19:13:29 +02:00
|
|
|
subjectAlernativeIPs, prometheus.GaugeValue, 1, serialNum, issuerCN, i,
|
2017-08-20 20:30:02 +02:00
|
|
|
)
|
|
|
|
}
|
2019-01-25 08:14:56 +02:00
|
|
|
|
2019-03-11 19:13:29 +02:00
|
|
|
if len(subjectIPs) > 0 {
|
2019-01-25 08:14:56 +02:00
|
|
|
ch <- prometheus.MustNewConstMetric(
|
2019-03-11 19:13:29 +02:00
|
|
|
subjectOrganizationUnits, prometheus.GaugeValue, 1, serialNum, issuerCN, ","+strings.Join(subjectOUs, ",")+",",
|
2019-01-25 08:14:56 +02:00
|
|
|
)
|
|
|
|
}
|
2017-08-20 20:30:02 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-08 19:43:37 +02:00
|
|
|
func probeHandler(w http.ResponseWriter, r *http.Request, tlsConfig *tls.Config) {
|
2017-08-20 20:30:02 +02:00
|
|
|
|
|
|
|
target := r.URL.Query().Get("target")
|
|
|
|
|
2017-11-06 16:48:34 +02:00
|
|
|
// The following timeout block was taken wholly from the blackbox exporter
|
2017-08-20 20:30:02 +02:00
|
|
|
// https://github.com/prometheus/blackbox_exporter/blob/master/main.go
|
|
|
|
var timeoutSeconds float64
|
|
|
|
if v := r.Header.Get("X-Prometheus-Scrape-Timeout-Seconds"); v != "" {
|
|
|
|
var err error
|
|
|
|
timeoutSeconds, err = strconv.ParseFloat(v, 64)
|
|
|
|
if err != nil {
|
|
|
|
http.Error(w, fmt.Sprintf("Failed to parse timeout from Prometheus header: %s", err), http.StatusInternalServerError)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
timeoutSeconds = 10
|
|
|
|
}
|
|
|
|
if timeoutSeconds == 0 {
|
|
|
|
timeoutSeconds = 10
|
|
|
|
}
|
|
|
|
|
|
|
|
timeout := time.Duration((timeoutSeconds) * 1e9)
|
|
|
|
|
|
|
|
exporter := &Exporter{
|
2019-03-08 19:43:37 +02:00
|
|
|
target: target,
|
|
|
|
timeout: timeout,
|
|
|
|
tlsConfig: tlsConfig,
|
2017-08-20 20:30:02 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
registry := prometheus.NewRegistry()
|
|
|
|
registry.MustRegister(exporter)
|
|
|
|
|
|
|
|
// Serve
|
|
|
|
h := promhttp.HandlerFor(registry, promhttp.HandlerOpts{})
|
|
|
|
h.ServeHTTP(w, r)
|
|
|
|
}
|
|
|
|
|
2017-11-06 16:48:34 +02:00
|
|
|
func uniq(certs []*x509.Certificate) []*x509.Certificate {
|
|
|
|
r := []*x509.Certificate{}
|
|
|
|
|
|
|
|
for _, c := range certs {
|
|
|
|
if !contains(r, c) {
|
|
|
|
r = append(r, c)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return r
|
|
|
|
}
|
|
|
|
|
|
|
|
func contains(certs []*x509.Certificate, cert *x509.Certificate) bool {
|
|
|
|
for _, c := range certs {
|
|
|
|
if (c.SerialNumber.String() == cert.SerialNumber.String()) && (c.Issuer.CommonName == cert.Issuer.CommonName) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2017-08-20 20:30:02 +02:00
|
|
|
func init() {
|
|
|
|
prometheus.MustRegister(version.NewCollector(namespace + "_exporter"))
|
|
|
|
}
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
var (
|
2019-03-08 19:43:37 +02:00
|
|
|
tlsConfig *tls.Config
|
|
|
|
certificates []tls.Certificate
|
|
|
|
rootCAs *x509.CertPool
|
2017-10-08 19:25:40 +02:00
|
|
|
listenAddress = kingpin.Flag("web.listen-address", "Address to listen on for web interface and telemetry.").Default(":9219").String()
|
|
|
|
metricsPath = kingpin.Flag("web.metrics-path", "Path under which to expose metrics").Default("/metrics").String()
|
|
|
|
probePath = kingpin.Flag("web.probe-path", "Path under which to expose the probe endpoint").Default("/probe").String()
|
|
|
|
insecure = kingpin.Flag("tls.insecure", "Skip certificate verification").Default("false").Bool()
|
2019-03-08 19:43:37 +02:00
|
|
|
clientAuth = kingpin.Flag("tls.client-auth", "Enable client authentication").Default("false").Bool()
|
|
|
|
caFile = kingpin.Flag("tls.cacert", "Local path to an alternative CA cert bundle").String()
|
2019-03-11 19:51:29 +02:00
|
|
|
certFile = kingpin.Flag("tls.cert", "Local path to a client certificate file (for client authentication)").Default("cert.pem").String()
|
|
|
|
keyFile = kingpin.Flag("tls.key", "Local path to a private key file (for client authentication)").Default("key.pem").String()
|
2017-08-20 20:30:02 +02:00
|
|
|
)
|
|
|
|
|
2017-10-08 19:25:40 +02:00
|
|
|
log.AddFlags(kingpin.CommandLine)
|
|
|
|
kingpin.Version(version.Print(namespace + "_exporter"))
|
|
|
|
kingpin.HelpFlag.Short('h')
|
|
|
|
kingpin.Parse()
|
2017-08-20 20:30:02 +02:00
|
|
|
|
2019-03-08 19:43:37 +02:00
|
|
|
if *clientAuth {
|
|
|
|
|
|
|
|
cert, err := tls.LoadX509KeyPair(*certFile, *keyFile)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalln(err)
|
|
|
|
}
|
|
|
|
certificates = append(certificates, cert)
|
|
|
|
|
|
|
|
caCert, err := ioutil.ReadFile(*caFile)
|
|
|
|
if err != nil {
|
|
|
|
log.Fatalln(err)
|
|
|
|
}
|
|
|
|
|
|
|
|
rootCAs = x509.NewCertPool()
|
|
|
|
rootCAs.AppendCertsFromPEM(caCert)
|
|
|
|
}
|
|
|
|
|
|
|
|
tlsConfig = &tls.Config{
|
|
|
|
InsecureSkipVerify: *insecure,
|
|
|
|
Certificates: certificates,
|
|
|
|
RootCAs: rootCAs,
|
|
|
|
}
|
|
|
|
|
2017-11-06 16:48:34 +02:00
|
|
|
log.Infoln("Starting "+namespace+"_exporter", version.Info())
|
2017-08-20 20:30:02 +02:00
|
|
|
log.Infoln("Build context", version.BuildContext())
|
|
|
|
|
|
|
|
http.Handle(*metricsPath, prometheus.Handler())
|
|
|
|
http.HandleFunc(*probePath, func(w http.ResponseWriter, r *http.Request) {
|
2019-03-08 19:43:37 +02:00
|
|
|
probeHandler(w, r, tlsConfig)
|
2017-08-20 20:30:02 +02:00
|
|
|
})
|
|
|
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
w.Write([]byte(`<html>
|
|
|
|
<head><title>SSL Exporter</title></head>
|
|
|
|
<body>
|
|
|
|
<h1>SSL Exporter</h1>
|
2019-01-25 08:05:17 +02:00
|
|
|
<p><a href="` + *probePath + `?target=https://example.com">Probe https://example.com for SSL cert metrics</a></p>
|
2017-08-20 20:30:02 +02:00
|
|
|
<p><a href='` + *metricsPath + `'>Metrics</a></p>
|
|
|
|
</body>
|
|
|
|
</html>`))
|
|
|
|
})
|
|
|
|
|
|
|
|
log.Infoln("Listening on", *listenAddress)
|
|
|
|
log.Fatal(http.ListenAndServe(*listenAddress, nil))
|
2017-11-06 16:48:34 +02:00
|
|
|
}
|