SSL certificate expiration is one of those problems that feels trivial until it brings down your production environment at 2 AM. Most teams only notice it after the fact — when users start seeing browser warnings or when a payment provider refuses the expired cert on a webhook. A proactive monitor costs less than an hour to build and eliminates the category entirely.
This article walks through a production-ready SSL certificate monitor in Python that checks multiple domains, calculates days remaining, and fires alerts via Slack when a cert is about to expire.
What to Check — and What to Ignore
The naive approach — just checking the expiry date — misses several failure modes:
- The certificate is valid but the hostname doesn't match the CN/SAN (misconfiguration after a domain rename)
- The intermediate chain is broken (the cert itself is fine, but some clients reject it)
- The cert was renewed but the server was never restarted, so it still serves the old one
For this monitor we focus on:
- Days until expiration
- Hostname verification
- The actual cert being served (not a cached value)
We skip full chain validation — that adds significant complexity and is better handled by dedicated tools like sslyze in a full audit.
Core Check: Fetching the Live Certificate
Python's ssl and socket modules give us everything we need without external dependencies.
import ssl
import socket
from datetime import datetime, timezone
from dataclasses import dataclass
from typing import Optional
@dataclass
class CertInfo:
hostname: str
expires_at: datetime
days_remaining: int
subject_cn: str
issuer: str
error: Optional[str] = None
def check_cert(hostname: str, port: int = 443, timeout: int = 10) -> CertInfo:
context = ssl.create_default_context()
try:
with socket.create_connection((hostname, port), timeout=timeout) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
cert = ssock.getpeercert()
except ssl.SSLCertVerificationError as e:
return CertInfo(
hostname=hostname,
expires_at=datetime.now(timezone.utc),
days_remaining=-1,
subject_cn="",
issuer="",
error=str(e),
)
except (socket.timeout, ConnectionRefusedError, OSError) as e:
return CertInfo(
hostname=hostname,
expires_at=datetime.now(timezone.utc),
days_remaining=-1,
subject_cn="",
issuer="",
error=str(e),
)
not_after = datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z")
not_after = not_after.replace(tzinfo=timezone.utc)
days_remaining = (not_after - datetime.now(timezone.utc)).days
subject = dict(x[0] for x in cert.get("subject", []))
issuer = dict(x[0] for x in cert.get("issuer", []))
return CertInfo(
hostname=hostname,
expires_at=not_after,
days_remaining=days_remaining,
subject_cn=subject.get("commonName", ""),
issuer=issuer.get("organizationName", ""),
)
A few things worth noting:
-
ssl.create_default_context()enables hostname verification by default. If the cert's SAN doesn't cover the hostname, you get anSSLCertVerificationErrorlogged as an error, not a days-remaining warning. - The
errorfield is populated on connection failures so the caller always gets a structured result, never an unhandled exception. - Timezone-aware
datetimeobjects prevent subtle comparison bugs when your monitor runs near midnight in a different timezone from the cert's timestamp.
Checking Multiple Domains with Per-Domain Thresholds
Different domains warrant different urgency windows. A domain serving a payment flow needs an earlier alert than a rarely-used internal tool.
DOMAINS = [
{"hostname": "example.com", "warn_days": 30, "critical_days": 7},
{"hostname": "api.example.com", "warn_days": 21, "critical_days": 5},
{"hostname": "internal.corp.example.com", "port": 8443, "warn_days": 14, "critical_days": 3},
]
class AlertLevel:
OK = "ok"
WARN = "warn"
CRITICAL = "critical"
ERROR = "error"
def assess(cert: CertInfo, warn_days: int, critical_days: int) -> str:
if cert.error:
return AlertLevel.ERROR
if cert.days_remaining <= critical_days:
return AlertLevel.CRITICAL
if cert.days_remaining <= warn_days:
return AlertLevel.WARN
return AlertLevel.OK
def run_checks() -> list[dict]:
results = []
for domain in DOMAINS:
hostname = domain["hostname"]
port = domain.get("port", 443)
cert = check_cert(hostname, port)
level = assess(cert, domain["warn_days"], domain["critical_days"])
results.append({"cert": cert, "level": level, "config": domain})
return results
Sending Slack Alerts
Slack webhooks are the simplest production-friendly alerting path. No SMTP credentials, no external packages, and messages appear instantly in a monitored channel.
import json
import urllib.request
SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
LEVEL_EMOJI = {
AlertLevel.OK: ":white_check_mark:",
AlertLevel.WARN: ":warning:",
AlertLevel.CRITICAL: ":rotating_light:",
AlertLevel.ERROR: ":x:",
}
def send_slack_alert(results: list[dict]) -> None:
issues = [r for r in results if r["level"] != AlertLevel.OK]
if not issues:
return # no noise when everything is fine
lines = []
for r in issues:
cert = r["cert"]
emoji = LEVEL_EMOJI[r["level"]]
if cert.error:
lines.append(f"{emoji} *{cert.hostname}* — connection error: `{cert.error}`")
else:
lines.append(
f"{emoji} *{cert.hostname}* — expires in *{cert.days_remaining} days* "
f"({cert.expires_at.strftime('%Y-%m-%d')}) — issued by {cert.issuer}"
)
message = {"text": "SSL Certificate Monitor\n" + "\n".join(lines)}
data = json.dumps(message).encode()
req = urllib.request.Request(
SLACK_WEBHOOK_URL,
data=data,
headers={"Content-Type": "application/json"},
)
urllib.request.urlopen(req, timeout=10)
Only alert on issues. If you send an "all certs OK" message every run, on-call engineers will tune it out within a week. Alert on deviation, not on health.
For email alerts, swap the Slack call for smtplib.SMTP_SSL — the structure is identical.
Scheduling Without Cron Complexity
A cron job works for most teams:
0 8 * * * /usr/bin/python3 /opt/monitors/ssl_monitor.py >> /var/log/ssl_monitor.log 2>&1
If you prefer systemd (better logging, restart-on-failure):
# /etc/systemd/system/ssl-monitor.timer
[Unit]
Description=Run SSL monitor daily at 08:00
[Timer]
OnCalendar=*-*-* 08:00:00
Persistent=true
[Install]
WantedBy=timers.target
The Persistent=true flag matters: if the server was down at the scheduled time, systemd executes the job as soon as it comes back up — no silently missed checks.
Hardening the Monitor Itself
A few operational concerns that are easy to miss:
Handle transient failures without silencing real alerts. Networks hiccup. Add a retry with short backoff before concluding a host is unreachable, but cap at 2–3 retries — you don't want the monitor hanging for minutes because one host is flaky.
Log every check, not just failures. When a cert eventually expires and someone asks "why didn't we catch this sooner?", you want a complete run history. Structured JSON logs — one dict per domain per run — make this trivially queryable with jq.
Never hardcode the Slack webhook URL. Use an environment variable or a secrets manager. Webhook URLs are credentials; rotating them after an accidental commit is friction you don't want. Apply the same discipline you would to any API key.
For teams running hardened infrastructure, pairing this monitor with a documented TLS configuration checklist makes incident response much faster. We publish a free SSL/TLS security hardening checklist in PDF and Excel format if you need a reference to build your runbook against.
The Takeaway
The monitor above runs in under 120 lines of standard-library Python, requires no external packages, and catches the three most common SSL failure modes before they affect users. The key decisions to carry forward:
- Always check the live cert, not a cached DNS or CDN value
- Separate warn and critical thresholds per domain — a single global threshold creates alert fatigue
- Log every run, alert only on deviation
- Use systemd timers over cron for anything that needs persistence guarantees
The logical next extension is certificate transparency log monitoring — CT logs will tell you if someone issued a cert for your domain without your knowledge, which is a separate and more serious threat. But that's a subject for another post.
I run AYI NEDJIMI Consultants, a cybersecurity consulting firm. We publish free security hardening checklists — PDF and Excel.
Top comments (0)