222 lines
4.7 KiB
Go
222 lines
4.7 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-co-op/gocron/v2"
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
|
|
"github.com/urfave/cli/v2"
|
|
|
|
log "github.com/sirupsen/logrus"
|
|
)
|
|
|
|
type CertificateMetrics struct {
|
|
Domain string
|
|
Port int
|
|
ALPN []string
|
|
|
|
ExpiryIn prometheus.Gauge
|
|
IsValid prometheus.Gauge
|
|
}
|
|
|
|
func NewCertificateMetrics(domain string, port int, alpn []string, name string) CertificateMetrics {
|
|
metrics := CertificateMetrics{
|
|
Domain: domain,
|
|
Port: port,
|
|
ALPN: alpn,
|
|
ExpiryIn: prometheus.NewGauge(
|
|
prometheus.GaugeOpts{
|
|
Name: "cert_status_expiry_in",
|
|
ConstLabels: prometheus.Labels{"name": name},
|
|
},
|
|
),
|
|
IsValid: prometheus.NewGauge(
|
|
prometheus.GaugeOpts{
|
|
Name: "cert_status_is_valid",
|
|
ConstLabels: prometheus.Labels{"name": name},
|
|
},
|
|
),
|
|
}
|
|
|
|
// Default values
|
|
metrics.ExpiryIn.Set(0)
|
|
metrics.IsValid.Set(0)
|
|
return metrics
|
|
}
|
|
|
|
func (c *CertificateMetrics) register(registry *prometheus.Registry) {
|
|
registry.MustRegister(
|
|
c.ExpiryIn,
|
|
c.IsValid,
|
|
)
|
|
}
|
|
|
|
func (c *CertificateMetrics) checkTls() {
|
|
conn, err := tls.Dial("tcp", c.Domain+":"+fmt.Sprint(c.Port), &tls.Config{
|
|
NextProtos: c.ALPN,
|
|
})
|
|
if err != nil {
|
|
log.Debugf("Failed to dial %s:%d@%s using ALPN %v: %v", c.Domain, c.Port, "tcp", c.ALPN, err)
|
|
c.IsValid.Set(0)
|
|
return
|
|
}
|
|
|
|
now := time.Now()
|
|
leafCert := conn.ConnectionState().PeerCertificates[0]
|
|
invalidIn := leafCert.NotAfter.Sub(now)
|
|
c.ExpiryIn.Set(invalidIn.Hours() / 24)
|
|
|
|
if leafCert.NotAfter.Compare(now) == -1 {
|
|
log.Debugf("leafCert.NotAfter (%v) < now (%v)!!", leafCert.NotAfter, now)
|
|
c.IsValid.Set(0)
|
|
} else {
|
|
if err := leafCert.VerifyHostname(c.Domain); err != nil {
|
|
log.Debugf("Peer cert does not verify domain '%s'", c.Domain)
|
|
c.IsValid.Set(0)
|
|
} else {
|
|
c.IsValid.Set(1)
|
|
}
|
|
}
|
|
}
|
|
|
|
func updateMetrics(metrics *[]CertificateMetrics, lastUpdated prometheus.Gauge) {
|
|
log.Debugf("Updating metrics for %d domains...", len(*metrics))
|
|
lastUpdated.SetToCurrentTime()
|
|
for _, metric := range *metrics {
|
|
metric.checkTls()
|
|
}
|
|
log.Debugf("Update done")
|
|
}
|
|
|
|
func run(ctx *cli.Context) error {
|
|
debug := ctx.Bool("debug")
|
|
host := ctx.String("host")
|
|
port := ctx.Int("port")
|
|
domains := ctx.StringSlice("domain")
|
|
|
|
if debug {
|
|
log.SetLevel(log.DebugLevel)
|
|
}
|
|
|
|
// Setup metrics
|
|
registry := prometheus.NewRegistry()
|
|
lastUpdatedMetric := prometheus.NewGauge(
|
|
prometheus.GaugeOpts{
|
|
Name: "certs_status_last_updated",
|
|
},
|
|
)
|
|
registry.MustRegister(lastUpdatedMetric)
|
|
|
|
metrics := make([]CertificateMetrics, 0)
|
|
for _, d := range domains {
|
|
log.Debugf("Parsing '%s'...", d)
|
|
parts := strings.Split(d, ":")
|
|
if len(parts) < 3 {
|
|
log.Errorf("Invalid domain format for '%s'", d)
|
|
return errors.New("Invalid domain format: Expects <domain>:<port>:<alpn>")
|
|
}
|
|
|
|
name := ""
|
|
if len(parts) == 4 {
|
|
name = parts[3]
|
|
} else {
|
|
name = parts[0]
|
|
}
|
|
|
|
port, err := strconv.Atoi(parts[1])
|
|
if err != nil {
|
|
log.Errorf("Failed to parse port of '%s'", d)
|
|
return err
|
|
}
|
|
|
|
// Create the metric, and register it
|
|
// TODO: Make this prettier
|
|
alpn := strings.Split(parts[2], ";")
|
|
log.Debugf("Registering: domain='%s' port='%d' alpn=%v name='%s'", parts[0], port, alpn, name)
|
|
metric := NewCertificateMetrics(
|
|
parts[0],
|
|
port,
|
|
alpn,
|
|
name,
|
|
)
|
|
metric.register(registry)
|
|
metrics = append(metrics, metric)
|
|
}
|
|
log.Infof("Parsed %d domain(s)", len(domains))
|
|
|
|
// Setup the task scheduler
|
|
scheduler, err := gocron.NewScheduler()
|
|
if err != nil {
|
|
log.Error("Failed to set up task scheduler")
|
|
return err
|
|
}
|
|
_, err = scheduler.NewJob(
|
|
gocron.DurationJob(24*time.Hour),
|
|
gocron.NewTask(
|
|
func() {
|
|
updateMetrics(&metrics, lastUpdatedMetric)
|
|
},
|
|
),
|
|
)
|
|
if err != nil {
|
|
log.Error("Failed to create periodic task")
|
|
return err
|
|
}
|
|
|
|
// Perform an initial run to populate the metrics
|
|
log.Info("Performing initial requests...")
|
|
updateMetrics(&metrics, lastUpdatedMetric)
|
|
log.Debug("Done")
|
|
|
|
http.Handle(
|
|
"/metrics", promhttp.HandlerFor(
|
|
registry,
|
|
promhttp.HandlerOpts{
|
|
EnableOpenMetrics: true,
|
|
},
|
|
),
|
|
)
|
|
addr := host + ":" + fmt.Sprint(port)
|
|
log.Infof("Handling requests at %s", addr)
|
|
return http.ListenAndServe(addr, nil)
|
|
}
|
|
|
|
func main() {
|
|
app := &cli.App{
|
|
Action: run,
|
|
Flags: []cli.Flag{
|
|
&cli.BoolFlag{
|
|
Name: "debug",
|
|
Value: false,
|
|
},
|
|
&cli.StringFlag{
|
|
Name: "host",
|
|
Usage: "Host to expose metrics on",
|
|
Value: "0.0.0.0",
|
|
},
|
|
&cli.IntFlag{
|
|
Name: "port",
|
|
Usage: "Port to expose metrics on",
|
|
Value: 8888,
|
|
},
|
|
&cli.StringSliceFlag{
|
|
Name: "domain",
|
|
Usage: "<domain>:<port>",
|
|
Required: true,
|
|
},
|
|
},
|
|
}
|
|
if err := app.Run(os.Args); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
}
|