cert-monitor

Watches the TLS certificates of your hosts and tells you, once per stage,
before something breaks. It's a single Go binary with a status page and
Telegram and email alerts.
It checks:
- Expiry: alerts at 30, 14, 7 and 1 days left (configurable), and again
when a certificate has expired. Each alert is sent once per threshold
per certificate. Renewing (a new fingerprint) starts over.
- Chain of trust: an unknown issuer, a self-signed certificate, a
missing intermediate, or an expired intermediate.
- Hostname: the certificate must cover the name you connect with (SNI).
- Weak crypto: SHA-1/MD5 signatures on the leaf or intermediates, RSA
keys under 2048 bits, ECDSA under 256, and servers that negotiate
TLS 1.0/1.1.
Hosts can use custom ports and an SNI that differs from the address you
connect to (api.example.com@10.0.0.5:8443), which is useful for checking
one backend behind a load balancer.
Who it's for
Small teams and self-hosters with a handful to a few hundred endpoints, such
as web servers, mail servers on 993/465, or internal APIs. It's for people
who want a heads-up on Telegram or by email rather than a full observability
stack.
Install
Install the binary with Go:
go install github.com/AbdeldjalilChougui/cert-monitor/cmd/cert-monitor@latest
Quick start
go build -o cert-monitor ./cmd/cert-monitor
# One-off check (exit code 0 ok, 1 warning, 2 critical):
./cert-monitor -check example.com mail.example.com:993 api.example.com@10.0.0.5:8443
# Run continuously:
cp config.example.json config.json # edit targets and notifiers
TELEGRAM_BOT_TOKEN=… TELEGRAM_CHAT_ID=… SMTP_PASSWORD=… ./cert-monitor -config config.json
open http://localhost:8090/
${VAR} references in the config are expanded from the environment, so
secrets stay out of the file. Example output:
TARGET STATUS DAYS EXPIRES DETAILS
github.com ok 43 2026-11-29
expired.badssl.com critical -4206 2015-04-12 certificate expired on 2015-04-12; chain: …
wrong.host.badssl.com critical 72 2026-12-28 hostname: certificate is not valid for wrong.host.badssl.com (it covers *.badssl.com, badssl.com)
sha1-intermediate.badssl.com critical -2331 2020-05-30 …; weak signature: intermediate COMODO SSL CA is signed with SHA1-RSA
Config
| key |
default |
|
targets |
— |
host, host:port or sni@host:port |
interval |
6h |
time between check rounds |
timeout |
10s |
per-host dial and handshake timeout |
thresholds |
[30,14,7,1] |
days-left alert stages |
state_file |
(memory) |
where alert dedupe state is kept between restarts |
listen |
:8090 |
status page: /, /api/status, POST /api/check, /healthz |
telegram |
— |
bot_token, chat_id |
smtp |
— |
addr, username, password, from, to (STARTTLS when offered) |
How it works
- Checking. It dials with
InsecureSkipVerify and then verifies by hand
with x509.Verify, VerifyHostname and its own algorithm checks. A
broken certificate is reported in detail ("missing intermediate",
"covers a.test, b.test") instead of failing with one handshake error.
Eight hosts are checked in parallel.
- Dedupe. The state per target is the leaf fingerprint, the most urgent
threshold already alerted, the set of non-expiry problems, and a streak of
connection errors. An expiry alert fires when days-left reaches a stage
that hasn't been alerted for that certificate. Problems alert when their
set changes, and a "resolved" message follows when it empties.
Connection errors must happen twice in a row. State only advances when at
least one notifier delivered, so a Telegram outage means a retry, not a
lost alert. State is saved atomically (temp file + rename).
- Status page. A server-rendered table sorted by urgency, plus JSON.
Tests
go test -race ./...
The tests generate their own PKI at runtime (a root, intermediates and
leaves) and serve it with httptest TLS servers. They cover a healthy
certificate, warning and critical expiry, an expired certificate, hostname
mismatch, SNI picking the right certificate among several, self-signed and
missing-intermediate chains, a 1024-bit RSA key, a SHA-1-signed leaf, a
server capped at TLS 1.1, and connection failure. The alert tests cover the
full threshold sequence including renewal, problem sets and resolution,
the two-strikes rule for errors, state persistence, the Telegram Bot API
(fake server) and SMTP (a minimal in-process SMTP server). The monitor tests
cover one alert per stage across runs, retry after a failed delivery, and
the status page and JSON.
Status and limitations
- No OCSP or CRL revocation checks, no Certificate Transparency lookups,
and no CAA checks.
- STARTTLS protocols (SMTP on 25/587, IMAP on 143, and so on) aren't
supported. Only implicit TLS ports (443, 465, 993, 8443…) are.
- Alerts go to every configured notifier. There's no per-target routing or
quiet hours.
- The status page has no authentication. Bind it to localhost or put it
behind a proxy if your host list is sensitive.
- Weak-algorithm checks cover signatures, key sizes and protocol version,
not individual cipher suites.
License
MIT. See LICENSE.