Skip to content

Instantly share code, notes, and snippets.

@twirrim
Created January 28, 2025 22:46
Show Gist options
  • Save twirrim/0990eb1ee83a1e9c2a50252ebf6f5241 to your computer and use it in GitHub Desktop.
Save twirrim/0990eb1ee83a1e9c2a50252ebf6f5241 to your computer and use it in GitHub Desktop.
sslcheck.py
#!/usr/bin/env python
import socket
import ssl
import datetime
import logging
import argparse
from multiprocessing.dummy import Pool
CERTS_TO_CHECK = [
("<site.to.check>", 443),
("<site.to.check>", 25),
]
def parse_args():
"""Returns arguments"""
parser = argparse.ArgumentParser()
parser.add_argument("--verbose", action="store_true")
return parser.parse_args()
class AlreadyExpired(Exception):
"""Expired Cert"""
# Raised when certificate has already expired
def ssl_expiry_datetime(target):
"""Returns a datetime object representing the expiry time for the target"""
ssl_date_fmt = r"%b %d %H:%M:%S %Y %Z"
hostname, port = target
cont = ssl.create_default_context()
cont.verify_mode = ssl.CERT_REQUIRED
try:
if port != 25:
conn = cont.wrap_socket(
socket.socket(socket.AF_INET),
server_hostname=hostname,
)
# Don't wait forever!
conn.settimeout(3.0)
conn.connect((hostname, port))
if port == 25:
conn.starttls()
ssl_info = conn.getpeercert()
conn.close()
else:
import smtplib
smtp = smtplib.SMTP(host=hostname, timeout=3)
smtp.connect()
smtp.starttls(context=cont)
ssl_info = smtp.sock.getpeercert(binary_form=False)
smtp.quit()
# parse the string from the certificate into a Python datetime object
expiry_datetime = datetime.datetime.strptime(ssl_info["notAfter"], ssl_date_fmt)
logging.debug(
"%s:%s\t%s",
hostname,
port,
expiry_datetime
)
return expiry_datetime
except:
logging.exception("Oops")
return None
def ssl_valid_time_remaining(hostname):
"""Get the number of days left in a cert's lifetime."""
expires = ssl_expiry_datetime(hostname)
if expires:
return expires - datetime.datetime.now()
return None
def ssl_expires_in(hostname, buffer_days=14):
"""Check if `hostname` SSL cert expires is within `buffer_days`.
Raises `AlreadyExpired` if the cert is past due
"""
remaining = ssl_valid_time_remaining(hostname)
if remaining:
# if the cert expires in less than two weeks, we should reissue it
if remaining < datetime.timedelta(days=0):
# cert has already expired - uhoh!
raise AlreadyExpired("Cert expired %s days ago" % remaining.days)
elif remaining < datetime.timedelta(days=buffer_days):
# expires sooner than the buffer
print(
"{} on port {} expiring in {} days".format(
hostname[0], hostname[1], remaining.days
)
)
else:
print(
"Unable to get certificate expiration date for: {}:{}".format(
hostname[0], hostname[1]
)
)
def main():
""" Ties it all together """
args = parse_args()
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
p = Pool()
p.map(ssl_expires_in, CERTS_TO_CHECK)
if __name__ == "__main__":
main()
@twirrim
Copy link
Author

twirrim commented Jan 29, 2025

I uploaded this because I mentioned to some friends that I had an SSL cert expiry checking script that I use in a cron. Of course, I didn't think to do much cleaning up of this thrown together code before I uploaded it.

Two quirks:

  1. I didn't used to use smtplib, so there's an extraneous "if port" in line 47 that'll never be true that dates back to before then. I forget why I switched over to smtplib, IIRC I was finding issues with straight ssl library, but don't quote me on that!
  2. multiprocessing.dummy.Pool. I've got in the habit of using multiprocessing.Pool's map for doing quick bits of parallelism. I used it without thinking about it, then after a splitsecond of thought realised threading would be fine, and I'd likely not have GIL issues. Rather than re-write a bunch of the code, I just switched to multiprocessing.dummy.Pool, which gets you the same Pool syntax, but uses threads instead.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment