diff --git a/README.md b/README.md index d579ad1..e89804c 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,22 @@ DEBU[0000] "letsencrypt/live/example.com-0007/fullchain.pem" : [example.com] ``` +Likewise, you can check the expiration of a remote host with the `remote` command: + +```shell +vbatts@jungle:~$ too-soon -D remote example.com +DEBU[0000] server: example.com +DEBU[0000] version: 772 +DEBU[0000] -- cert serial: 498c7524e0cbd9f3fba887d6b7bba9dacac +DEBU[0000] "example.com:443"(498c7524e0cbd9f3fba887d6b7bba9dacac) : 2025-04-06 18:47:55 +0000 UTC +DEBU[0000] "example.com:443"(498c7524e0cbd9f3fba887d6b7bba9dacac) : [example.com] +DEBU[0000] -- cert serial: 838f6c63ceb1398c6206628315c9fdde +DEBU[0000] -- cert: skipping as there are no DNS names +DEBU[0000] -- cert serial: 498c7524e0cbd9f3fba887d6b7bba9dacac +DEBU[0000] "example.com:443"(498c7524e0cbd9f3fba887d6b7bba9dacac) : 2025-04-06 18:47:55 +0000 UTC +DEBU[0000] "example.com:443"(498c7524e0cbd9f3fba887d6b7bba9dacac) : [example.com] +``` + ## Combo Whether you use a cronjob or a systemd timer, you can chain this command to a daily/weekly job to check an email yourself: diff --git a/main.go b/main.go index 4fac742..71c4dc4 100644 --- a/main.go +++ b/main.go @@ -2,10 +2,13 @@ package main import ( "context" + "crypto/tls" "crypto/x509" "encoding/pem" + "fmt" "net/mail" "os" + "strings" "time" log "github.com/sirupsen/logrus" @@ -46,6 +49,12 @@ func main() { Action: fPEMCheck, ArgsUsage: "[PEM files...]", }, + &cli.Command{ + Name: "remote", + Usage: "check if certificate on a remote host expires too soon", + Action: fHostCheck, + ArgsUsage: "[host/host:port...]", + }, }, Action: func(ctx context.Context, cmd *cli.Command) error { cli.ShowAppHelpAndExit(cmd, 1) @@ -58,8 +67,69 @@ func main() { } } +func fHostCheck(ctx context.Context, cmd *cli.Command) error { + numAlert := 0 + numHostErr := 0 + for i := 0; i <= cmd.Args().Len(); i++ { + host := cmd.Args().Get(i) + if host == "" { + break + } + + // TODO use the url.Parse package to do this, incase there is a protocol and path on there as well + port := "443" + if strings.Contains(host, ":") { + chunks := strings.Split(host, ":") + host = chunks[0] + port = chunks[1] + } + // connect to the domain:port/tcp + dest := fmt.Sprintf("%s:%s", host, port) + conn, err := tls.Dial("tcp", dest, &tls.Config{ + InsecureSkipVerify: true, // TODO add a flag to force this + }) + if err != nil { + numHostErr++ + log.Errorf("%q connection failed: %s", dest, err) + continue + } + defer conn.Close() + + log.Debugf("server: %s", conn.ConnectionState().ServerName) + log.Debugf("version: %d", conn.ConnectionState().Version) + for _, cert := range conn.ConnectionState().PeerCertificates { + log.Debugf(" -- cert serial: %x", cert.SerialNumber) + if len(cert.DNSNames) == 0 { + log.Debugf(" -- cert: skipping as there are no DNS names") + continue + } + + hours := time.Duration(cmd.Int("days") * -24) + alertTime := cert.NotAfter.Add(hours * time.Hour) + today := time.Now() + if today.After(alertTime) { + if today.After(cert.NotAfter) { + log.Warnf("%q(%x) : TIME TO RENEW CERTIFICATE (already expired!)", dest, cert.SerialNumber) + } else { + log.Warnf("%q(%x) : TIME TO RENEW CERTIFICATE (expires in less than %d days)", dest, cert.SerialNumber, cmd.Int("days")) + } + log.Warnf("%q(%x) : %v", dest, cert.SerialNumber, cert.NotAfter) + log.Warnf("%q(%x) : %v", dest, cert.SerialNumber, cert.DNSNames) + numAlert++ + } else { + log.Debugf("%q(%x) : %v", dest, cert.SerialNumber, cert.NotAfter) + log.Debugf("%q(%x) : %v", dest, cert.SerialNumber, cert.DNSNames) + } + } + } + if numAlert != 0 { + return cli.Exit("domain certificates need to be renewed", numAlert) + } + return nil +} + func fPEMCheck(ctx context.Context, cmd *cli.Command) error { - retCode := 0 + numAlert := 0 for i := 0; i <= cmd.Args().Len(); i++ { file := cmd.Args().Get(i) if file == "" { @@ -107,15 +177,15 @@ func fPEMCheck(ctx context.Context, cmd *cli.Command) error { } log.Warnf("%q : %v", file, cert.NotAfter) log.Warnf("%q : %v", file, cert.DNSNames) - retCode++ + numAlert++ } else { log.Debugf("%q : %v", file, cert.NotAfter) log.Debugf("%q : %v", file, cert.DNSNames) } } } - if retCode != 0 { - return cli.Exit("certificates need to be renewed", retCode) + if numAlert != 0 { + return cli.Exit("certificates need to be renewed", numAlert) } return nil }