mirror of
https://github.com/ribbybibby/ssl_exporter.git
synced 2024-11-24 08:22:17 +02:00
83f01274fc
* Move to yaml.v3 everywhere * Switch to github.com/prometheus/common/promlog for logging
483 lines
13 KiB
Go
483 lines
13 KiB
Go
package prober
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-kit/log"
|
|
"github.com/go-kit/log/level"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"golang.org/x/crypto/ocsp"
|
|
v1 "k8s.io/api/core/v1"
|
|
)
|
|
|
|
const (
|
|
namespace = "ssl"
|
|
)
|
|
|
|
func collectConnectionStateMetrics(state tls.ConnectionState, registry *prometheus.Registry) error {
|
|
if err := collectTLSVersionMetrics(state.Version, registry); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := collectCertificateMetrics(state.PeerCertificates, registry); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := collectVerifiedChainMetrics(state.VerifiedChains, registry); err != nil {
|
|
return err
|
|
}
|
|
|
|
return collectOCSPMetrics(state.OCSPResponse, registry)
|
|
}
|
|
|
|
func collectTLSVersionMetrics(version uint16, registry *prometheus.Registry) error {
|
|
var (
|
|
tlsVersion = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "", "tls_version_info"),
|
|
Help: "The TLS version used",
|
|
},
|
|
[]string{"version"},
|
|
)
|
|
)
|
|
registry.MustRegister(tlsVersion)
|
|
|
|
var v string
|
|
switch version {
|
|
case tls.VersionTLS10:
|
|
v = "TLS 1.0"
|
|
case tls.VersionTLS11:
|
|
v = "TLS 1.1"
|
|
case tls.VersionTLS12:
|
|
v = "TLS 1.2"
|
|
case tls.VersionTLS13:
|
|
v = "TLS 1.3"
|
|
default:
|
|
v = "unknown"
|
|
}
|
|
|
|
tlsVersion.WithLabelValues(v).Set(1)
|
|
|
|
return nil
|
|
}
|
|
|
|
func collectCertificateMetrics(certs []*x509.Certificate, registry *prometheus.Registry) error {
|
|
var (
|
|
notAfter = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "", "cert_not_after"),
|
|
Help: "NotAfter expressed as a Unix Epoch Time",
|
|
},
|
|
[]string{"serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
|
|
)
|
|
notBefore = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "", "cert_not_before"),
|
|
Help: "NotBefore expressed as a Unix Epoch Time",
|
|
},
|
|
[]string{"serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
|
|
)
|
|
)
|
|
registry.MustRegister(notAfter, notBefore)
|
|
|
|
certs = uniq(certs)
|
|
|
|
if len(certs) == 0 {
|
|
return fmt.Errorf("No certificates found")
|
|
}
|
|
|
|
for _, cert := range certs {
|
|
labels := labelValues(cert)
|
|
|
|
if !cert.NotAfter.IsZero() {
|
|
notAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix()))
|
|
}
|
|
|
|
if !cert.NotBefore.IsZero() {
|
|
notBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix()))
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func collectVerifiedChainMetrics(verifiedChains [][]*x509.Certificate, registry *prometheus.Registry) error {
|
|
var (
|
|
verifiedNotAfter = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "", "verified_cert_not_after"),
|
|
Help: "NotAfter expressed as a Unix Epoch Time",
|
|
},
|
|
[]string{"chain_no", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
|
|
)
|
|
verifiedNotBefore = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "", "verified_cert_not_before"),
|
|
Help: "NotBefore expressed as a Unix Epoch Time",
|
|
},
|
|
[]string{"chain_no", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
|
|
)
|
|
)
|
|
registry.MustRegister(verifiedNotAfter, verifiedNotBefore)
|
|
|
|
sort.Slice(verifiedChains, func(i, j int) bool {
|
|
iExpiry := time.Time{}
|
|
for _, cert := range verifiedChains[i] {
|
|
if (iExpiry.IsZero() || cert.NotAfter.Before(iExpiry)) && !cert.NotAfter.IsZero() {
|
|
iExpiry = cert.NotAfter
|
|
}
|
|
}
|
|
jExpiry := time.Time{}
|
|
for _, cert := range verifiedChains[j] {
|
|
if (jExpiry.IsZero() || cert.NotAfter.Before(jExpiry)) && !cert.NotAfter.IsZero() {
|
|
jExpiry = cert.NotAfter
|
|
}
|
|
}
|
|
|
|
return iExpiry.After(jExpiry)
|
|
})
|
|
|
|
for i, chain := range verifiedChains {
|
|
chain = uniq(chain)
|
|
for _, cert := range chain {
|
|
chainNo := strconv.Itoa(i)
|
|
labels := append([]string{chainNo}, labelValues(cert)...)
|
|
|
|
if !cert.NotAfter.IsZero() {
|
|
verifiedNotAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix()))
|
|
}
|
|
|
|
if !cert.NotBefore.IsZero() {
|
|
verifiedNotBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix()))
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func collectOCSPMetrics(ocspResponse []byte, registry *prometheus.Registry) error {
|
|
var (
|
|
ocspStapled = prometheus.NewGauge(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "", "ocsp_response_stapled"),
|
|
Help: "If the connection state contains a stapled OCSP response",
|
|
},
|
|
)
|
|
ocspStatus = prometheus.NewGauge(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "", "ocsp_response_status"),
|
|
Help: "The status in the OCSP response 0=Good 1=Revoked 2=Unknown",
|
|
},
|
|
)
|
|
ocspProducedAt = prometheus.NewGauge(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "", "ocsp_response_produced_at"),
|
|
Help: "The producedAt value in the OCSP response, expressed as a Unix Epoch Time",
|
|
},
|
|
)
|
|
ocspThisUpdate = prometheus.NewGauge(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "", "ocsp_response_this_update"),
|
|
Help: "The thisUpdate value in the OCSP response, expressed as a Unix Epoch Time",
|
|
},
|
|
)
|
|
ocspNextUpdate = prometheus.NewGauge(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "", "ocsp_response_next_update"),
|
|
Help: "The nextUpdate value in the OCSP response, expressed as a Unix Epoch Time",
|
|
},
|
|
)
|
|
ocspRevokedAt = prometheus.NewGauge(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "", "ocsp_response_revoked_at"),
|
|
Help: "The revocationTime value in the OCSP response, expressed as a Unix Epoch Time",
|
|
},
|
|
)
|
|
)
|
|
registry.MustRegister(
|
|
ocspStapled,
|
|
ocspStatus,
|
|
ocspProducedAt,
|
|
ocspThisUpdate,
|
|
ocspNextUpdate,
|
|
ocspRevokedAt,
|
|
)
|
|
|
|
if len(ocspResponse) == 0 {
|
|
return nil
|
|
}
|
|
|
|
resp, err := ocsp.ParseResponse(ocspResponse, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ocspStapled.Set(1)
|
|
ocspStatus.Set(float64(resp.Status))
|
|
ocspProducedAt.Set(float64(resp.ProducedAt.Unix()))
|
|
ocspThisUpdate.Set(float64(resp.ThisUpdate.Unix()))
|
|
ocspNextUpdate.Set(float64(resp.NextUpdate.Unix()))
|
|
ocspRevokedAt.Set(float64(resp.RevokedAt.Unix()))
|
|
|
|
return nil
|
|
}
|
|
|
|
func collectFileMetrics(logger log.Logger, files []string, registry *prometheus.Registry) error {
|
|
var (
|
|
totalCerts []*x509.Certificate
|
|
fileNotAfter = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "", "file_cert_not_after"),
|
|
Help: "NotAfter expressed as a Unix Epoch Time for a certificate found in a file",
|
|
},
|
|
[]string{"file", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
|
|
)
|
|
fileNotBefore = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "", "file_cert_not_before"),
|
|
Help: "NotBefore expressed as a Unix Epoch Time for a certificate found in a file",
|
|
},
|
|
[]string{"file", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
|
|
)
|
|
)
|
|
registry.MustRegister(fileNotAfter, fileNotBefore)
|
|
|
|
for _, f := range files {
|
|
data, err := ioutil.ReadFile(f)
|
|
if err != nil {
|
|
level.Debug(logger).Log("msg", fmt.Sprintf("Error reading file %s: %s", f, err))
|
|
continue
|
|
}
|
|
certs, err := decodeCertificates(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
totalCerts = append(totalCerts, certs...)
|
|
for _, cert := range certs {
|
|
labels := append([]string{f}, labelValues(cert)...)
|
|
|
|
if !cert.NotAfter.IsZero() {
|
|
fileNotAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix()))
|
|
}
|
|
|
|
if !cert.NotBefore.IsZero() {
|
|
fileNotBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix()))
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(totalCerts) == 0 {
|
|
return fmt.Errorf("No certificates found")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func collectKubernetesSecretMetrics(secrets []v1.Secret, registry *prometheus.Registry) error {
|
|
var (
|
|
totalCerts []*x509.Certificate
|
|
kubernetesNotAfter = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "", "kubernetes_cert_not_after"),
|
|
Help: "NotAfter expressed as a Unix Epoch Time for a certificate found in a kubernetes secret",
|
|
},
|
|
[]string{"namespace", "secret", "key", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
|
|
)
|
|
kubernetesNotBefore = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "", "kubernetes_cert_not_before"),
|
|
Help: "NotBefore expressed as a Unix Epoch Time for a certificate found in a kubernetes secret",
|
|
},
|
|
[]string{"namespace", "secret", "key", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
|
|
)
|
|
)
|
|
registry.MustRegister(kubernetesNotAfter, kubernetesNotBefore)
|
|
|
|
for _, secret := range secrets {
|
|
for _, key := range []string{"tls.crt", "ca.crt"} {
|
|
data := secret.Data[key]
|
|
if len(data) == 0 {
|
|
continue
|
|
}
|
|
certs, err := decodeCertificates(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
totalCerts = append(totalCerts, certs...)
|
|
for _, cert := range certs {
|
|
labels := append([]string{secret.Namespace, secret.Name, key}, labelValues(cert)...)
|
|
|
|
if !cert.NotAfter.IsZero() {
|
|
kubernetesNotAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix()))
|
|
}
|
|
|
|
if !cert.NotBefore.IsZero() {
|
|
kubernetesNotBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix()))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(totalCerts) == 0 {
|
|
return fmt.Errorf("No certificates found")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func collectKubeconfigMetrics(logger log.Logger, kubeconfig KubeConfig, registry *prometheus.Registry) error {
|
|
var (
|
|
totalCerts []*x509.Certificate
|
|
kubeconfigNotAfter = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "kubeconfig", "cert_not_after"),
|
|
Help: "NotAfter expressed as a Unix Epoch Time for a certificate found in a kubeconfig",
|
|
},
|
|
[]string{"kubeconfig", "name", "type", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
|
|
)
|
|
kubeconfigNotBefore = prometheus.NewGaugeVec(
|
|
prometheus.GaugeOpts{
|
|
Name: prometheus.BuildFQName(namespace, "kubeconfig", "cert_not_before"),
|
|
Help: "NotBefore expressed as a Unix Epoch Time for a certificate found in a kubeconfig",
|
|
},
|
|
[]string{"kubeconfig", "name", "type", "serial_no", "issuer_cn", "cn", "dnsnames", "ips", "emails", "ou"},
|
|
)
|
|
)
|
|
registry.MustRegister(kubeconfigNotAfter, kubeconfigNotBefore)
|
|
|
|
for _, c := range kubeconfig.Clusters {
|
|
var data []byte
|
|
var err error
|
|
if c.Cluster.CertificateAuthorityData != "" {
|
|
data, err = base64.StdEncoding.DecodeString(c.Cluster.CertificateAuthorityData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if c.Cluster.CertificateAuthority != "" {
|
|
data, err = ioutil.ReadFile(c.Cluster.CertificateAuthority)
|
|
if err != nil {
|
|
level.Debug(logger).Log("msg", fmt.Sprintf("Error reading file %s: %s", c.Cluster.CertificateAuthority, err))
|
|
return err
|
|
}
|
|
}
|
|
if data == nil {
|
|
continue
|
|
}
|
|
certs, err := decodeCertificates(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
totalCerts = append(totalCerts, certs...)
|
|
for _, cert := range certs {
|
|
labels := append([]string{kubeconfig.Path, c.Name, "cluster"}, labelValues(cert)...)
|
|
|
|
if !cert.NotAfter.IsZero() {
|
|
kubeconfigNotAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix()))
|
|
}
|
|
|
|
if !cert.NotBefore.IsZero() {
|
|
kubeconfigNotBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix()))
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, u := range kubeconfig.Users {
|
|
var data []byte
|
|
var err error
|
|
if u.User.ClientCertificateData != "" {
|
|
data, err = base64.StdEncoding.DecodeString(u.User.ClientCertificateData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if u.User.ClientCertificate != "" {
|
|
data, err = ioutil.ReadFile(u.User.ClientCertificate)
|
|
if err != nil {
|
|
level.Debug(logger).Log("msg", fmt.Sprintf("Error reading file %s: %s", u.User.ClientCertificate, err))
|
|
return err
|
|
}
|
|
}
|
|
if data == nil {
|
|
continue
|
|
}
|
|
certs, err := decodeCertificates(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
totalCerts = append(totalCerts, certs...)
|
|
for _, cert := range certs {
|
|
labels := append([]string{kubeconfig.Path, u.Name, "user"}, labelValues(cert)...)
|
|
|
|
if !cert.NotAfter.IsZero() {
|
|
kubeconfigNotAfter.WithLabelValues(labels...).Set(float64(cert.NotAfter.Unix()))
|
|
}
|
|
|
|
if !cert.NotBefore.IsZero() {
|
|
kubeconfigNotBefore.WithLabelValues(labels...).Set(float64(cert.NotBefore.Unix()))
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(totalCerts) == 0 {
|
|
return fmt.Errorf("No certificates found")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func labelValues(cert *x509.Certificate) []string {
|
|
return []string{
|
|
cert.SerialNumber.String(),
|
|
cert.Issuer.CommonName,
|
|
cert.Subject.CommonName,
|
|
dnsNames(cert),
|
|
ipAddresses(cert),
|
|
emailAddresses(cert),
|
|
organizationalUnits(cert),
|
|
}
|
|
}
|
|
|
|
func dnsNames(cert *x509.Certificate) string {
|
|
if len(cert.DNSNames) > 0 {
|
|
return "," + strings.Join(cert.DNSNames, ",") + ","
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func emailAddresses(cert *x509.Certificate) string {
|
|
if len(cert.EmailAddresses) > 0 {
|
|
return "," + strings.Join(cert.EmailAddresses, ",") + ","
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func ipAddresses(cert *x509.Certificate) string {
|
|
if len(cert.IPAddresses) > 0 {
|
|
ips := ","
|
|
for _, ip := range cert.IPAddresses {
|
|
ips = ips + ip.String() + ","
|
|
}
|
|
return ips
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func organizationalUnits(cert *x509.Certificate) string {
|
|
if len(cert.Subject.OrganizationalUnit) > 0 {
|
|
return "," + strings.Join(cert.Subject.OrganizationalUnit, ",") + ","
|
|
}
|
|
|
|
return ""
|
|
}
|