Monitor TLS Certificate Expiry for Django Sites
Catch expiring HTTPS certificates before users do: check expiry from outside with uptime monitors, Prometheus blackbox exporter, or a scheduled script, cover every domain and certificate path, alert at 21 and 7 days, and verify that renewal actually reloads Nginx.
Problem statement
Automatic renewal with Certbot, Caddy, or cert-manager works until it silently stops: a DNS change breaks the challenge, a firewall blocks port 80, an API token for DNS-01 expires, or the certificate renews on disk but Nginx keeps serving the old one because it was never reloaded. Users find out when browsers show a full-page security warning. Independent monitoring of the certificate actually served is the safety net.
- Check the certificate served on port 443 from outside, for every hostname (apex, www, api, admin).
- Use your uptime tool's certificate expiry alerts, the Prometheus blackbox exporter, or a scheduled script.
- Alert at 21 days (renewal should have happened at 30 days for 90-day certificates) and page at 7 days.
- After renewal, verify that the web server reloaded: compare the served certificate's expiry with the file on disk.
- Track internal certificates too: database TLS, mTLS client CAs, and service-to-service certificates.
Step-by-step solution
1. Check what clients actually see
echo | openssl s_client -connect shop.example.com:443 -servername shop.example.com 2>/dev/null \
| openssl x509 -noout -enddate -subject
Always pass -servername so SNI selects the right certificate, and check each hostname separately; one server can present different certificates per name.
2. Use your monitoring tool
Most uptime services and Uptime Kuma can alert on certificate expiry for HTTPS monitors. With Prometheus, the blackbox exporter exposes probe_ssl_earliest_cert_expiry:
# prometheus alert rule
- alert: TLSCertificateExpiringSoon
expr: (probe_ssl_earliest_cert_expiry - time()) / 86400 < 21
for: 1h
labels: { severity: warning }
annotations:
summary: "Certificate for {{ $labels.instance }} expires in less than 21 days"
- alert: TLSCertificateExpiringCritical
expr: (probe_ssl_earliest_cert_expiry - time()) / 86400 < 7
for: 10m
labels: { severity: critical }
3. A minimal scheduled script
Without a monitoring stack, a daily cron job on a machine outside the web server's network works:
import socket, ssl, datetime, sys
def days_left(host, port=443):
ctx = ssl.create_default_context()
with socket.create_connection((host, port), timeout=10) as sock:
with ctx.wrap_socket(sock, server_hostname=host) as tls:
cert = tls.getpeercert()
expires = datetime.datetime.strptime(cert["notAfter"], "%b %d %H:%M:%S %Y %Z")
return (expires - datetime.datetime.utcnow()).days
for host in ["shop.example.com", "www.shop.example.com", "api.shop.example.com"]:
left = days_left(host)
if left < 21:
print(f"{host}: certificate expires in {left} days", file=sys.stderr)
sys.exit(1)
Send the output to email or chat through cron's MAILTO or a webhook.
4. Verify reloads after renewal
Certbot renews files under /etc/letsencrypt/live/, but Nginx keeps the old certificate in memory until reloaded. Add a deploy hook:
# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh
#!/bin/sh
systemctl reload nginx
Then compare the served expiry with the file:
openssl x509 -noout -enddate -in /etc/letsencrypt/live/shop.example.com/fullchain.pem
If the file is newer than what clients see, the reload is missing.
5. Inventory every certificate
List every certificate the Django system depends on: public sites, API and admin hostnames, customer custom domains, database server certificates (if you verify them), mTLS CAs, and certificates on load balancers or CDNs. Each needs either automatic renewal with monitoring or a calendar entry.
Explanation
Certificates are checked by every client on every connection, and an expired one breaks HTTPS completely, with no graceful degradation. Automatic renewal reduces the risk but adds hidden dependencies (DNS, ports, API tokens, reload hooks). Measuring the expiry of the certificate actually served on each hostname, from outside your infrastructure, verifies the whole chain at once and gives weeks of warning while there is still time to fix it calmly.
Edge cases / notes
- Intermediate certificates.
probe_ssl_earliest_cert_expiryconsiders the whole chain; an expiring intermediate also triggers it. - CDNs and load balancers. If a CDN terminates TLS, monitor the CDN's certificate, and separately the origin's if CDN-to-origin TLS is verified.
- Shorter lifetimes. Public CAs are reducing certificate lifetimes over the coming years, which makes automated renewal and monitoring mandatory.
Next steps
- Fix renewal problems with Fix Certbot Renewal Failed.
- Add a monitor in Monitor a Django Site With Uptime Kuma.
- On Kubernetes, see Automate HTTPS With cert-manager.
FAQ
Why monitor if Certbot renews automatically?
Renewal can fail silently, or succeed without the web server reloading. Monitoring checks what users actually receive.
When should certificate alerts fire?
A warning around 21 days and a critical alert at 7 days for 90-day certificates.
How do I check a certificate's expiry from the command line?
Use openssl s_client -connect host:443 -servername host piped to openssl x509 -noout -enddate.
Do I need to monitor every subdomain?
Yes. Different hostnames can have different certificates and renewal paths.