--- /dev/null
+package mtls
+
+// NOTE: this is not part of the mtls package because mbclient depends on mtls
+import (
+ "context"
+ "crypto/x509"
+ "os"
+ "path"
+ "time"
+
+ "go.fuhry.dev/runtime/metrics/metricbus/mbclient"
+ "go.fuhry.dev/runtime/mtls"
+ "go.fuhry.dev/runtime/mtls/certutil"
+ "go.fuhry.dev/runtime/mtls/fsnotify"
+ "go.fuhry.dev/runtime/utils/hashset"
+ "go.fuhry.dev/runtime/utils/log"
+)
+
+type mtlsMetrics struct {
+ log *log.Logger
+
+ svc *mbclient.MetricBusService
+
+ secondsUntilExpiration mbclient.GaugeMetric
+ rotationsObserved mbclient.CounterMetric
+ certificatesOnHost mbclient.GaugeMetric
+
+ certs map[string]*mtlsMetricCertificate
+}
+
+type mtlsMetricCertificate struct {
+ baseDir string
+}
+
+func NewMtlsMetricsService(ctx context.Context) *mtlsMetrics {
+ svc := mbclient.NewService(ctx)
+
+ return &mtlsMetrics{
+ log: log.WithPrefix("mtls_monitor"),
+ svc: svc,
+
+ secondsUntilExpiration: svc.DefineGauge(
+ "mtls_certificate_seconds_until_expire",
+ "Time until an mTLS certificate expires",
+ "cert_name"),
+ rotationsObserved: svc.DefineCounter(
+ "mtls_certificate_rotations",
+ "Number of times the rotation of a given certificate has been observed",
+ "cert_name"),
+ certificatesOnHost: svc.DefineGauge(
+ "mtls_host_certificates_count",
+ "Total number of certificates present",
+ "status"),
+
+ certs: make(map[string]*mtlsMetricCertificate),
+ }
+}
+
+func (m *mtlsMetrics) Tick() {
+ allCerts := m.discoverMtlsCertificates()
+
+ for _, baseDir := range allCerts {
+ if _, ok := m.certs[baseDir]; !ok {
+ m.log.Noticef("found a new certificate at %s", baseDir)
+ certWatcher := &mtlsMetricCertificate{
+ baseDir: baseDir,
+ }
+
+ err := fsnotify.NotifyPath(certWatcher.LeafPath(), m.fsNotifyEvent)
+ if err != nil {
+ m.log.Warnf("failed to add watcher on %s, not watching this cert: %v",
+ certWatcher.LeafPath(), err)
+ continue
+ }
+
+ m.certs[baseDir] = certWatcher
+ }
+ }
+
+ removedCerts := make([]string, 0)
+ certsHash := hashset.FromSlice(allCerts)
+ for baseDir, _ := range m.certs {
+ if !certsHash.Contains(baseDir) {
+ m.log.Noticef("certificate removed: %s", baseDir)
+ removedCerts = append(removedCerts, baseDir)
+ }
+ }
+
+ for _, removedCert := range removedCerts {
+ delete(m.certs, removedCert)
+ }
+
+ var countActive, countExpired float64
+
+ for _, cert := range m.certs {
+ expiresIn := cert.TimeUntilExpiration()
+ if expiresIn > 0 {
+ m.log.V(2).Infof("cert %s: counted as active", cert.Name())
+ countActive += 1
+ } else {
+ m.log.V(2).Infof("cert %s: counted as expired", cert.Name())
+ countExpired += 1
+ }
+
+ m.log.V(2).Infof("cert %s: %.0f seconds until expiration", cert.Name(), expiresIn.Seconds())
+ m.secondsUntilExpiration.WithLabelValues(mbclient.KV{"cert_name": cert.Name()}).Set(expiresIn.Seconds())
+ }
+
+ m.log.V(1).Infof("total %.0f active/%.0f expired certificates", countActive, countExpired)
+ m.certificatesOnHost.WithLabelValues(mbclient.KV{"status": "active"}).Set(countActive)
+ m.certificatesOnHost.WithLabelValues(mbclient.KV{"status": "expired"}).Set(countExpired)
+}
+
+func (m *mtlsMetrics) FlushAndWait() {
+ m.svc.FlushAndWait()
+}
+
+func (m *mtlsMetrics) fsNotifyEvent(eventPath string, op fsnotify.Op) {
+ if op != fsnotify.Create && op != fsnotify.Close {
+ return
+ }
+
+ dir := path.Dir(eventPath)
+ cert, ok := m.certs[eventPath]
+ if !ok {
+ cert, ok = m.certs[dir]
+ }
+ if ok {
+ m.log.V(1).Infof("certificate %s was just rotated", cert.Name())
+ m.rotationsObserved.WithLabelValues(mbclient.KV{"cert_name": cert.Name()}).Add(1)
+ } else {
+ m.log.V(1).Warnf("observed rotation of an unaccounted cert at path: %s", eventPath)
+ }
+}
+
+func (m *mtlsMetrics) discoverMtlsCertificates() []string {
+ certDirs := make([]string, 0)
+
+ for _, dir := range mtls.RootPaths() {
+ if entries, err := os.ReadDir(dir); err == nil {
+ for _, mtlsEntry := range entries {
+ if !mtlsEntry.IsDir() {
+ continue
+ }
+
+ leafPath := path.Join(dir, mtlsEntry.Name(), "cert.pem")
+ leafStat, err := os.Stat(leafPath)
+ if err != nil {
+ continue
+ }
+ if !leafStat.Mode().IsRegular() {
+ continue
+ }
+
+ certDirs = append(certDirs, path.Join(dir, mtlsEntry.Name()))
+ }
+ }
+ }
+
+ return certDirs
+}
+
+func (c *mtlsMetricCertificate) LeafPath() string {
+ return path.Join(c.baseDir, "cert.pem")
+}
+
+func (c *mtlsMetricCertificate) Leaf() *x509.Certificate {
+ leafPath := c.LeafPath()
+ certsInLeaf, err := certutil.LoadCertificatesFromPEM(leafPath)
+ if err != nil {
+ return nil
+ }
+
+ if len(certsInLeaf) != 1 {
+ return nil
+ }
+
+ return certsInLeaf[0]
+}
+
+func (c mtlsMetricCertificate) Name() string {
+ return path.Base(c.baseDir)
+}
+
+func (c *mtlsMetricCertificate) TimeUntilExpiration() time.Duration {
+ cert := c.Leaf()
+ if cert == nil {
+ return 0
+ }
+
+ now := time.Now()
+
+ if now.After(cert.NotAfter) {
+ return 0
+ }
+
+ return cert.NotAfter.Sub(now)
+}