Python Ssl Certificate Expiration Checker

Written by

in

Keeping your web services secure means staying ahead of SSL/TLS certificate expirations. An expired certificate can trigger browser warnings, break API integrations, and damage customer trust—all within seconds of the deadline passing. Fortunately, Python makes it easy to automate the tedious task of checking certificate lifespans across dozens or hundreds of hosts. In this guide you’ll learn how to build a robust Python SSL certificate expiration checker, integrate it with monitoring tools, and schedule regular scans so you never get caught off guard.

Why You Need an SSL Expiration Checker

  • Prevent downtime: Browsers block connections to sites with expired certificates, causing immediate service interruptions.
  • Maintain compliance: Many regulatory frameworks (PCI‑DSS, HIPAA, GDPR) require timely renewal of TLS certificates.
  • Boost customer confidence: A valid padlock icon reassures visitors that their data is protected.
  • Scale with growth: Manual checks become impossible as the number of domains and sub‑domains grows.

Core Concepts Behind SSL Certificate Validation

What is an SSL/TLS certificate?

An SSL/TLS certificate binds a public key to a domain name, enabling encrypted communication between a client and a server. The certificate contains metadata such as the Not Before and Not After dates, which define its validity period.

How does Python retrieve certificate data?

Python’s standard library provides two main ways to fetch a certificate:

  • ssl module combined with socket – low‑level, full control over TLS handshake.
  • urllib3 or requests – higher‑level HTTP clients that expose the peer certificate via a response object.

For a dedicated expiration checker, the ssl + socket approach is preferred because it avoids the overhead of an HTTP request and works for any service that speaks TLS, not just HTTP.

Step‑by‑Step: Building the Checker

1. Set up the project environment

# Create a virtual environment (optional but recommended)
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install any third‑party packages (we’ll use tqdm for progress bars)
pip install tqdm

2. Core function to fetch certificate dates

The following function opens a TLS connection to a host, extracts the certificate, and returns the notAfter field as a datetime object.

import socket
import ssl
from datetime import datetime
from typing import Tuple

def get_ssl_expiry(host: str, port: int = 443, timeout: int = 5) -> Tuple[datetime, str]:
    """
    Returns the expiry datetime and the raw certificate string for a given host.
    Raises socket.timeout or ssl.SSLError on failure.
    """
    context = ssl.create_default_context()
    with socket.create_connection((host, port), timeout=timeout) as sock:
        with context.wrap_socket(sock, server_hostname=host) as ssock:
            cert = ssock.getpeercert()
            # cert['notAfter'] format: 'Jun 30 12:00:00 2025 GMT'
            expiry_str = cert['notAfter']
            expiry_date = datetime.strptime(expiry_str, '%b %d %H:%M:%S %Y %Z')
            # Return both for optional logging
            return expiry_date, ssl.DER_cert_to_PEM_cert(ssock.getpeercert(binary_form=True))

3. Helper to calculate days remaining

from datetime import datetime, timezone

def days_until_expiry(expiry: datetime) -> int:
    now = datetime.now(timezone.utc)
    delta = expiry - now
    return delta.days

4. Main script that processes a list of hosts

Below is a ready‑to‑run script that reads a plain‑text file hosts.txt (one host per line), checks each certificate, and prints a color‑coded report. It also writes JSON output for integration with monitoring platforms.

import json
from pathlib import Path
from tqdm import tqdm

HOSTS_FILE = Path('hosts.txt')
OUTPUT_JSON = Path('ssl_report.json')
WARNING_DAYS = 30  # Threshold for "expiring soon"

def load_hosts() -> list:
    return [line.strip() for line in HOSTS_FILE.read_text().splitlines() if line.strip()]

def check_all_hosts(hosts: list) -> list:
    results = []
    for host in tqdm(hosts, desc='Checking SSL certificates'):
        try:
            expiry, _pem = get_ssl_expiry(host)
            days_left = days_until_expiry(expiry)
            status = 'OK'
            if days_left < 0:
                status = 'EXPIRED'
            elif days_left <= WARNING_DAYS:
                status = 'EXPIRING_SOON'
            results.append({
                'host': host,
                'expiry': expiry.isoformat(),
                'days_left': days_left,
                'status': status
            })
        except Exception as exc:
            results.append({
                'host': host,
                'error': str(exc),
                'status': 'ERROR'
            })
    return results

def print_report(results: list):
    for r in results:
        if r['status'] == 'OK':
            print(f"\033[92m{r['host']}: OK – {r['days_left']} days left\033[0m")
        elif r['status'] == 'EXPIRING_SOON':
            print(f"\033[93m{r['host']}: EXPIRING SOON – {r['days_left']} days left\033[0m")
        elif r['status'] == 'EXPIRED':
            print(f"\033[91m{r['host']}: EXPIRED!\033[0m")
        else:
            print(f"\033[95m{r['host']}: ERROR – {r['error']}\033[0m")

def main():
    hosts = load_hosts()
    results = check_all_hosts(hosts)
    print_report(results)
    OUTPUT_JSON.write_text(json.dumps(results, indent=2))
    print(f"\nReport saved to {OUTPUT_JSON}")

if __name__ == '__main__':
    main()

5. Sample hosts.txt file

example.com
api.myservice.io
mail.google.com
expired.badssl.com

Enhancing the Checker for Production Use

  • Parallel execution: Use concurrent.futures.ThreadPoolExecutor to check dozens of hosts simultaneously, reducing total runtime.
  • Alerting integration: Push the JSON report to Slack, Microsoft Teams, or PagerDuty via webhook when status is EXPIRING_SOON or EXPIRED.
  • Certificate pinning support: Store the PEM hash of a known good certificate and compare it on each run to detect unauthorized replacements.
  • Support for non‑standard ports: Extend the host list to include host:port pairs for services like SMTP (port 587) or LDAP (port 636).
  • Logging & persistence: Write daily snapshots to a time‑series database (InfluxDB, Prometheus) for trend analysis.

Scheduling the Checker with Cron (Linux) or Task Scheduler (Windows)

Linux (cron)

# Edit the crontab for the user that runs the script
crontab -e

# Run the checker every day at 02:00 AM
0 2 * * * /usr/bin/python3 /path/to/ssl_checker.py >> /var/log/ssl_check.log 2>&1

Windows (Task Scheduler)

  1. Open Task Scheduler and create a new task.
  2. Set the trigger to “Daily” and choose a convenient time.
  3. In the “Action” tab, point to python.exe and add the script path as an argument.
  4. Enable “Run whether user is logged on or not” and store the password.

Integrating with Popular Monitoring Platforms

Prometheus Exporter

Expose each certificate’s days_left as a gauge metric. A minimal Flask exporter could look like this:

from flask import Flask, Response
from prometheus_client import Gauge, generate_latest, CollectorRegistry

app = Flask(__name__)
registry = CollectorRegistry()
days_gauge = Gauge('ssl_certificate_days_left',
                  'Number of days until SSL certificate expires',
                  ['host'], registry=registry)

@app.route('/metrics')
def metrics():
    for result in check_all_hosts(load_hosts()):
        if result['status'] in ('OK', 'EXPIRING_SOON'):
            days_gauge.labels(host=result['host']).set(result['days_left'])
    return Response(generate_latest(registry), mimetype='text/plain')

Slack Notification Example

import requests

SLACK_WEBHOOK = 'https://hooks.slack.com/services/XXXXX/XXXXX/XXXXX'

def send_slack_alert(message: str):
    payload = {'text': message}
    requests.post(SLACK_WEBHOOK, json=payload)

# Inside the main loop, after checking:
if r['status'] == 'EXPIRING_SOON':
    send_slack_alert(f":warning: *{r['host']}* expires in {r['days_left']} days.")
elif r['status'] == 'EXPIRED':
    send_slack_alert(f":x: *{r['host']}* has *expired*!")

Testing and Debugging Tips

  • Use openssl s_client -connect example.com:443 -servername example.com to manually inspect a certificate and compare with the script output.
  • Run the script with PYTHONVERBOSE=1 to see low

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *