How can I check SSL/TLS certificate expiration for a list of domains using Python?
Quick Answer
Use Python's built-in ssl and socket modules to open a TLS connection to each domain on port 443, retrieve the peer certificate's notAfter field, parse it into a datetime, and compare the days remaining against a warning threshold. This requires no external dependencies and mirrors exactly what a browser checks when it shows a certificate warning.
Detailed Answer
Think of a TLS certificate like a passport with an expiration date stamped inside — a border agent doesn't need to call the passport office to know it's expired, they just read the date printed on the document itself. This script does the equivalent of a border agent's ID check: connect, ask to see the certificate, read the expiration date directly, and flag anyone whose certificate is about to run out before anyone gets stuck at the border.
This script exists because certificate expiration is one of the most preventable categories of production outage, and yet it happens constantly — a certificate renewed manually eighteen months ago quietly expires at 2 AM, taking down an entire service's HTTPS endpoint, and the team finds out from a customer complaint instead of a dashboard. Python's standard library ssl module was chosen deliberately over an external tool because it requires zero extra dependencies, which matters for lightweight monitoring scripts that need to run reliably in constrained environments like a cron job on a bastion host or a minimal container image.
The script opens a raw TCP socket to each domain on port 443, wraps it with an SSL context via wrap_socket, which performs the actual TLS handshake including certificate presentation from the server, and calls getpeercert() to retrieve the certificate's fields as a Python dictionary. Critically, this only works with certificate verification enabled, the default in create_default_context(), because getpeercert() returns an empty dict if verification is disabled, a subtle trap engineers hit when they've disabled verification elsewhere in their codebase and copy-paste that pattern here. The notAfter field comes back as a specific string format that must be parsed with datetime.strptime using the exact matching format string, and the days-remaining calculation is a simple datetime subtraction.
In production, this check is typically wired into a Prometheus blackbox exporter or run as a scheduled job feeding a dashboard, with graduated alert thresholds — a 30-day warning gives teams time to file a renewal ticket, while a 7-day warning should page on-call directly since it indicates the earlier warning was missed. What to monitor beyond just days-remaining: the certificate's issuer chain, since an internal CA cert expiring is just as dangerous as a public one, and Subject Alternative Names coverage, since a certificate can be not-expired but still wrong for a newly added subdomain that was never included when the cert was issued.
The non-obvious gotcha is that this script only checks the certificate actively being served by the connection it makes — if a service sits behind a load balancer with multiple backend instances serving slightly different or differently-aged certificates due to a partially-rolled-out renewal, a single check from one script run might see the newly renewed cert on one backend and report all clear while a different backend instance is still serving the old, soon-to-expire certificate to some fraction of real traffic. Thorough checks connect multiple times or target specific backend IPs directly rather than trusting DNS round-robin to represent the whole fleet in one shot.