Automatische Warnung vor ablaufenden Zertifikaten

Von | März 4, 2025

Die Let’s Encrypt Zertifikate haben keine sehr lange Laufzeit, sie werden jedoch bei den meissten Installationen automatisch verlängert, so dass dies kein großer Nachteil der kostenlosen Zertifikate ist.

Ab und zu geht das Erneuern der Zertifikate jedoch schief. Hierfür gab es einen automatischen Prozess von Let’s Encrypt der eine Warnmail versandt hat wenn die Restgültigkeit der Zertifikate eine bestimmte Dauer unterschritten hat.

Let’s Encrypt stellt diesen Service nun aber ein, so dass man von einem Auslaufen der Zertifikate überrascht werden kann. Zur Vermeidung dieser Situation habe ich ein Python3 Script geschrieben, dass eine Datei mit einer Liste von Webadressen mit dazugehöriger Mailadresse erwartet und dem die Warnschwelle übergeben werden kann, so dass bei Unterschreiten der Warnschwelle eine Warnmail versendet wird.

Das Script wird über einen Cronjob gestartet:

11 9 * * 2 /usr/bin/certcheck.py /etc/domain-mails 10
9 9 1 * * /usr/bin/certcheck.py /etc/domain-mails 31
8 9 * * * /usr/bin/certcheck.py /etc/domain-mails 2

Hier nun das dazugehörige Script:

#!/usr/bin/env python3
import sys
import ssl
import socket
import datetime
import concurrent.futures
import smtplib
from email.mime.text import MIMEText

# Konfigurationsvariablen
SMTP_SERVER = "mail.contoso.com"
SMTP_SENDER = "xyz@contoso.com"
SMTP_PORT = 587
SMTP_USERNAME = "username"
SMTP_PASSWORD = "secret"
EXPIRY_WARNING_DAYS = 10

def send_email(recipient, subject, body):
    msg = MIMEText(body)
    msg['Subject'] = subject
    msg['From'] = SMTP_SENDER
    msg['To'] = recipient
    try:
        with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as server:
            server.starttls()
            server.login(SMTP_USERNAME, SMTP_PASSWORD)
            server.sendmail(SMTP_SENDER, [recipient], msg.as_string())
    except Exception as e:
        print(f"Fehler beim Senden der E-Mail an {recipient}: {e}")

def get_certificate_expiry_date(hostname):
    try:
        context = ssl.create_default_context()
        with socket.create_connection((hostname, 443), timeout=5) as sock:
            with context.wrap_socket(sock, server_hostname=hostname) as ssock:
                certificate = ssock.getpeercert()
                expiry_date = datetime.datetime.strptime(certificate['notAfter'], '%b %d %H:%M:%S %Y %Z')
                return expiry_date
    except Exception as e:
        return f"Error: {e}"

def check_certificate_expiry(entry):
    try:
        hostname, email = entry
        expiry_date = get_certificate_expiry_date(hostname)
        if isinstance(expiry_date, datetime.datetime):
            days_to_expiry = (expiry_date - datetime.datetime.utcnow()).days
            message = f"{hostname}: Das Zertifikat läuft in {days_to_expiry} Tagen ab ({expiry_date})"
            if days_to_expiry < EXPIRY_WARNING_DAYS:
                send_email(email, f"Zertifikatswarnung für {hostname}", message)
        else:
            message = f"{hostname}: {expiry_date}"
            send_email(email, f"Zertifikatsfehler für {hostname}", message)
        return message
    except ValueError:
        return f"Ungültiges Format für Eintrag: {entry}"
def check_certificates(endpoints):
    with concurrent.futures.ThreadPoolExecutor(max_workers=10) as executor:
        results = executor.map(check_certificate_expiry, endpoints)

    #for result in results:
    #    print(result)

def main(hosts_file, days):
    try:
        with open(hosts_file, 'r') as file:
            endpoints = []
            for line in file:
                parts = line.strip().split()
                if len(parts) == 2:
                    endpoints.append(tuple(parts))
                else:
                    print(f"Warnung: Ungültige Zeile übersprungen -> {line.strip()}")
        check_certificates(endpoints)
    except FileNotFoundError:
        print(f"Die Datei {hosts_file} wurde nicht gefunden.")
    except Exception as e:
        print(f"Ein Fehler ist aufgetreten: {e}")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Verwendung: python check-certificates.py <hosts_datei> <Anzahl Tage>")
    elif len(sys.argv) == 2:
        main(sys.argv[1], EXPIRY_WARNING_DAYS)
    else:
        EXPIRY_WARNING_DAYS = int(sys.argv[2])
        main(sys.argv[1], EXPIRY_WARNING_DAYS)

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert