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.

Certificate expiry monitoringRenewal jobCertbot, cert-managerServed certwhat clients seeExternal checkdays remainingAlert early21 and 7 daysMonitor the certificate clients receive, not the renewal job's logs.
  1. Check the certificate served on port 443 from outside, for every hostname (apex, www, api, admin).
  2. Use your uptime tool's certificate expiry alerts, the Prometheus blackbox exporter, or a scheduled script.
  3. Alert at 21 days (renewal should have happened at 30 days for 90-day certificates) and page at 7 days.
  4. After renewal, verify that the web server reloaded: compare the served certificate's expiry with the file on disk.
  5. 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 }
days remaining on a 90-day certificateissued90 daysnormal renewalrenews herewarning alert21 dayscritical alert7 days0 days30 days90 daysAn alert at 21 days means renewal has failed for over a weekdjango-deployment.com
Shorter certificate lifetimes, which CAs are moving toward, need proportionally earlier alerts.

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.

cause and fixCauseFixchallenge fails after DNS changefix DNS or firewall; renewDNS-01 API token expiredrotate token; monitor itrenewed but not reloadeddeploy hook reloads Nginxforgotten hostnamemonitor every served nameExternal monitoring catches all four, whatever the causedjango-deployment.com
Renewal logs say what the job did; external checks say what users get.

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_expiry considers 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

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.

Learning path · step 6 of 13

Keeping it running

Keep going