Python Notes: server cert check

  1. The information presented here is intended for educational use.
  2. The information presented here is provided free of charge, as-is, with no warranty of any kind.
  3. Edit: 2026-03-05

server_cert_check

#!/bin/python3.9
'''
=============================================================================
title  : /var/www/cgi-bin/server_cert_check_101.py
author : Neil Rieck (Bell-ATS, Bell Canada)
created: 2025-03-05
notes  :
1) this is a quick hack which should be good enough for Bell-ATS use
2) This program can be run interactively as well as cron
3) if we were a bit larger then I would read URL_LIST from a database
4) ping fails should be logged in a database (limit emails to one per day?)
ver who when   what
--- --- ------ -----------------------------------------------------------
100 NSR 250305 derived from bell_ats_server_health_100.py
    NSR 250306 started adding code to check certs
    NSR 251024 changes for public use at my personal website
101 NSR 260221 tweaks
    NSR 260302 my hosting service wants to switch to 180 day certs
102 NSR 260303 added a routine to pick new outer-bounds based upon date
    NSR 260305 tweaks
=============================================================================
'''
import platform
from time import sleep
import smtplib
import datetime
import sys
import email.utils  # needed for rfc822 support
import requests
# 1) the next line fixed our python3.6 environment
# 2) it also fixed our python3.9 environment after I reinstalled python3.9
# 3) the next line is required when fetching from OpenVMS systems
requests.packages.urllib3.util.ssl_.DEFAULT_CIPHERS = 'ALL:@SECLEVEL=1'
#
# ===========================================================================
#
#   constants
#
PGM = "server_cert_check"   #
VER = "102"                 # change as required
DEBUG = 2                   # ditto
#
#   note: URL_LIST7 is for production Bell-ATS use
#
URL_LIST7 = [               # 6 plus one alias
    "ats.on.bell.ca",       # alias
    "kawc96.on.bell.ca",    # OpenVMS prod (KAWC90)
    "kawc09.on.bell.ca",    # OpenVMS dvlp (KAWC09)
    "kawc0f.on.bell.ca",    # Linux prod (Kitchener)
    "kawc4n.on.bell.ca",    # Linux dvlp (Kitchener)
    "bfdc0d.on.bell.ca",    # Linux prod (Barrie)
    "bfdc0e.on.bell.ca"     # Linux dvlp (Barrie)
]
URL_LIST1 = [               # 1 address (development)
    "neilrieck.net"         # personal web site
]
#
#   globals
#
this_host = ""              #
#
# ===========================================================================
#
#   code area begins
#
#   test certificate()
#   CAVEAT: max_days will be reduced year-after-year
#
def test_cert(dns, count, max_days):
    state = 0                                                                   # init
    url = f"https://{dns}"
    if DEBUG >= 1:
        print(f"-i-debug: {DEBUG}")
        print(f"-i-url: {url}")
    try:
        # CAVEATS:
        # 1) If you connect with "verify=False" then the certificate is not loaded for testing.
        #    So at this point, I do not know if an expired certificate will ever be seen.
        # 2) If the url fails with requests.get() but works from your browser, then the web server's
        #    chain file (which contains the root and intermediate certs) must be updated.
        #    Browsers maintain a cache of known root servers; this subsystem does not)
        with requests.get(url, stream=True, timeout=(5, 4)) as response:
            certificate_info = response.raw.connection.sock.getpeercert()
        state = 1                                                               # fetch was successful
        if type(certificate_info) != dict:
            certificate_info = dict(certificate_info)
        if DEBUG >= 2:
            print(f"{'-'*60} debug: {count}")
            print("cert: ", certificate_info)
            subject = dict(x[0] for x in certificate_info['subject'])
            print("commonName:", subject['commonName'])
            issuer = dict(x[0] for x in certificate_info['issuer'])
            print("issuer:", issuer['commonName'])
            print('-'*70)
        # ----------------------------------------
        expiry = certificate_info['notAfter']
        print("-i-cert expiry:", expiry)                                        # rfc822 datetime stamp
        tup = email.utils.parsedate_tz(expiry)                                  #
        utc = email.utils.mktime_tz(tup)                                        # utc time stamp (integer)
        dt1 = datetime.datetime.fromtimestamp(utc)                              #
        dt0 = datetime.datetime.now()                                           #
        # ----------------------------------------
        dt9 = dt1 - dt0                                                         #
        days = dt9.days                                                         #
        if days > max_days:
            msg = f"-o-certificate '{dns}' is not a new style {max_days}-day cert"
        elif days > 30:
            msg = f"-i-certificate '{dns}' is okay"
        elif days > 0:
            msg = f"-w-certificate '{dns}' will expire in {days} days"
        else:
            msg = f"-e-certificate '{dns}' is expired"
    except Exception as e:                                                      #
        state = 2                                                               # some kind of error (connect, or verify)
        msg = f"-e-error: {e} while fetching certificate '{dns}'"               #
    return state, msg                                                           #

#
#   the work horse
#
def test_url_list(this_host="", max_days=365):
    pfxToVerb = {"-?-": "total",
                 "-i-": "pass",
                 "-o-": "option",
                 "-w-": "warn",
                 "-e-": "fail"}                                                 #
    stats = []                                                                  #
    for i in range(len(pfxToVerb)):                                             #
        stats.append(0)                                                         #
    count = 0                                                                   #
    for dns in URL_LIST1:
        count += 1
        print(f"{'='*60} test: {count}")                                        #
        for retry in range(1, 5):                                               #
            if retry < 4:                                                       #
                print(f'-i-testing dns: {dns} try: {retry}')
                state, msg = test_cert(dns, count, max_days)                    #
                if DEBUG >= 1:
                    print(f"-i-debug-state: {state} msg: {msg}")
                if state == 1:                                                  # we made a connection...
                    break                                                       # ...so exit from here
                else:
                    print(f"-w-state: {state} so falling through")
            else:                                                               # oops, no more counts remaining
                msg = f"-e-could not connect/verify '{dns}' after {retry} tries"
                print(msg)                                                      #
                break                                                           #
            print(f"-w-oops, something went wrong on try: {retry}")
            sleep(1)                                                            #
        # ------------------------------------------
        pfx = msg[0:3]                                                          # extract prefix (eg. "-i-")
        verb = pfxToVerb.get(pfx, "oops")                                       #
        if verb == "oops":
            pfx = "-e-"
            verb = pfxToVerb.get(pfx, "oops")
        idx = list(pfxToVerb).index(pfx)                                        #
        stats[idx] = stats[idx] + 1                                             # update stats
        if (verb == "pass"):                                                    #
            print(f"-i-resp: {msg} (pass)")
        else:
            print(f"-e-resp: {msg} {verb}")
            stamp = datetime.datetime.now().strftime("%Y%m%d.%H%M%S")
            msg = (
                f"reporting host: {this_host}\n"
                f"program: {PGM}\n"
                f"time: {stamp}\n"
                f"message: {msg}"
            )
            mail_option = 0                                                     # 0=none; 1=developer; 2=distribution list; 3=both
            if (mail_option & 1) == 1:
                send_mail(msg, dns, "n.rieck@bell.net")
            if (mail_option & 2) == 2:
                send_mail(msg, dns, "ats_adm_list")
        stats[0] += 1                                                           # update total
        print(f"{'='*70}")
        sleep(2)
    print("\nExit report card:")
    count = -1
    for verb in pfxToVerb.values():
        count += 1
        print(f"  {verb: <6} : {stats[count]}")
    print("="*70)
    if stats[0] != stats[1]:                                                    # if total <> pass
        return(1)                                                               # abnormal exit
    return(0)                                                                   # normal exit
#
#   send mail
#
def send_mail(msg="no-message", url="", dst="neil.rieck@bell.ca"):
    global this_host
    if url == "":
        details = ""
    else:
        details = f" for: {url}"
    try:
        print(f"-i-mailing to: {dst}")
        sender = f"root@{this_host}"
        receivers = dst
        message = (
            f"From: {sender}\n"
            f"To: {receivers}\n"
            f"Subject: certificate test failed {details}\n\n"
            f"{msg}")
        print(message)
        smtpObj = smtplib.SMTP('localhost')
        smtpObj.sendmail(sender, receivers, message)
    except Exception as e:
        print(f"-e-error: {e}")

#
#   certificate lifetime is changing
#   CAVEAT: this information was fresh as of 2026-03-03
#
def get_cert_max_days():
    # January 1 2025 days = 365
    # March 15, 2026 days = 200
    # March 15, 2027 days = 100
    # March 15, 2029 days = 47
    curr_date = datetime.datetime.now().strftime("%Y%m%d")
    if (curr_date >= "20290301"):
        temp = 47
    elif (curr_date >= "20270301"):
        temp = 100
    elif (curr_date >= "20260301"):
        temp = 200
    else:
        temp = 365
    return (temp)
#
#   that grand poobah
#
def main():
    global this_host
    print(f"\n{PGM}-{VER}")
    print("="*70)
    this_host = platform.node()
    stamp = datetime.datetime.now().strftime("%Y%m%d.%H%M%S")
    max_days = get_cert_max_days()
    print(f"-i-running on host: {this_host} at: {stamp}")
    print(f"-i-cert max life: {max_days}")
    rc = test_url_list(this_host, max_days)
    print(f"-i-adios rc: {rc}")
    sys.exit(rc)                                            # important if a cron task
#
#   catch-all
#
if __name__ == "__main__":
    main()
#

Links


 Back to Home
 Neil Rieck
 Waterloo, Ontario, Canada.