Updates on Lets Encrypt Subscriber Agreement & Ending Expiration Notification

Hallo.
Ich habe heute eine E-Mail von Let’s Encrypt erhalten, die Ihr vermutlich auch alle bekommen habt …

--------------------------------------------------------------

Hi,

As a Let’s Encrypt Subscriber, you benefit from access to free, automated TLS certificates. One way we have supported Subscribers is by sending expiration notification emails when it’s time to renew a certificate.

We’re writing to inform you that we intend to discontinue sending expiration notification emails. You can learn more in this blog post. You will receive this reminder email again in the coming months:

Here are some actions you can take today:

Automate with an ACME Client that supports Automated Renewal Information (ARI). ARI enables us to automatically renew your certificates ahead of schedule should the need arise:

Sign up for a third-party monitoring service that may provide expiration emails. We can recommend Red Sift Certificates Lite, which provides free expiration emails for up to 250 active certificates:

Opt in to emails. While we are deprecating expiration notification emails, you can opt in to continue to receive other emails. We’ll keep you informed about technical updates, and other news about Let’s Encrypt and our parent nonprofit, ISRG, based on the preferences you choose:

In accordance with this change, we are updating our Subscriber Agreement, effective 24 February 2025. This is the agreement that governs the relationship between you and ISRG with regards to your acquisition and use of SSL/TLS digital certificates issued by ISRG (via Let’s Encrypt). You don’t need to take any action to continue to use the Let’s Encrypt service but we encourage you to review the new agreement. You can find the latest agreement (v1.5) here:

All the best,
Let’s Encrypt

--------------------------------------------------------------

Unsere „internen“ LE-Zertifikate werden im Moment direkt von der OPNSense geholt und aktualisiert. Eigentlich läuft das vollständig automatisch …

Unsere Schulhomepage hingegen verwendet einen anderen Mechismus. Da waren mir die E-Mail-Erinnerungen immer ganz recht, da das manchmal auch hängengeblieben ist.
Daher: Wie geht Ihr damit um?

Viele Grüße,
Michael

Hallo Michael,

wir holen uns auf einer VM mittels dehydrated ein wildcard-Zertifikat (dns-challenge), welches wir dann mit Ansible entsprechend verteilen (server, opnsense, reverseproxy, …). Das Ansible-Playbook macht dann auch bei den betroffenen Services einen reload oder restart.
Das Ansible-Skript wird allerdings händisch von uns aufgerufen (sonst müssten wir ja einen entsprechenden private ssh-key ungeschützt auf einer VM haben).
Das Ansible rennt da schnell durch und wenn es irgendwo ein Problem gab, dann bekommen wir das da direkt mit. Aufwand: Eine Minute alle 3 Monate.
Lieben Gruß
Raphael

Hallo Michael,

Bei mir sind auch die Wildcard (auch dns-challenge, mit certbot) automatisch verteilt, ich muss da gar nichts mehr machen.

Seite Monitoring: ich habe einen Python-Skript, der jede Nacht per Cronjob alle meine SSL Endpunkte aufrufe, um zu überprüfen, ob alles ok ist.
Falls einen Zertifikat zu nah von Ende liegt, erhalte ich automatisch eine Email.

Gruß

Arnaud

Hallo Arnaud,

würdest du dein Skript zur Verfügung stellen?

Viele Grüße
Steffen

Hi,

Hier meine Bastelrei:

import ssl
import socket
import smtplib
from datetime import datetime
from OpenSSL import crypto
from concurrent import futures

############## CONFIG ################
# EMAIL SERVER

SMTP_SERVER     = "mail.mydomain1.com"
SENDER_EMAIL    = ""
RECIPIENT_EMAIL = ""
SMTP_PASSWORD   = ""
SMTP_PORT = 587

# DOMAINS

DOMAINES = {'MYDOMAIN1.COM': [{'domain': 'mail.mydomain1.com', 'port': 587}, {'domain': 'mail.mydomain1.com', 'port': 465}, {'domain': 'mail.mydomain1.com', 'port': 993}, {'domain': 'www.mydomain1.com', 'port': 443}], 'TEST.EU': [{'domain': 'mail.test.eu', 'port': 993}, {'domain': 'www.test.eu', 'port': 443}]}

# DIVERS

SHELL_OUTPUT = True
SEND_EMAIL = True

############## END CONFIG ################

WARNING = '\033[93m'
SUCCESS = '\033[92m'
ALERT = '\033[38;5;208m'
DANGER = '\033[91m'
INFO = '\033[96m'
ENDC = '\033[0m'

def sendMail(subject, content):
    port           = SMTP_PORT 
    smtp_server    = SMTP_SERVER
    sender_email   = SENDER_EMAIL
    receiver_email = RECIPIENT_EMAIL
    password       = SMTP_PASSWORD
    message        = f"""Subject: {subject}
From: {sender_email}
To: {receiver_email}

{content}"""

    context = ssl.create_default_context()
    with smtplib.SMTP(smtp_server, port) as server:
        server.ehlo()  # Can be omitted
        server.starttls(context=context)
        server.ehlo()  # Can be omitted
        server.login(sender_email, password)
        server.sendmail(sender_email, receiver_email, message)
        
class PrintShell:
    def __init__(self):
        pass
        
    def danger(self, text, result, width=40):
        print(f'  {text + " ":.<{width}}{DANGER} {result}{ENDC}')
        
    def alert(self, text, result, width=40):
        print(f'  {text + " ":.<{width}}{ALERT} {result}{ENDC}')
        
    def warning(self, text, result, width=40):
        print(f'  {text + " ":.<{width}}{WARNING} {result}{ENDC}')
        
    def info(self, text, result, width=40):
        print(f'  {text + " ":.<{width}}{INFO} {result}{ENDC}')
        
    def success(self, text, result, width=40):
        print(f'  {text + " ":.<{width}}{SUCCESS} {result}{ENDC}')
    
    def printsh(text, result, color, width=40):
        print(f'  {text + " ":.<40}{color} {result}{ENDC}')

now = datetime.now()
out = PrintShell()

def CertLimitSSL(hostname, port):
    """Return standard SSL cert from hostname:port"""
    ctx = ssl.create_default_context()
    s = ctx.wrap_socket(socket.socket(), server_hostname=hostname)
    s.settimeout(10)
    s.connect((hostname, port))
    cert = s.getpeercert()
    s.close()
    return cert


def CertLimitSTARTTLS(hostname):
    """Return SSL cert from STARTTLS connection at hostname:587"""
    connection = smtplib.SMTP(hostname, timeout=10)
    connection.connect(hostname,587)
    connection.starttls()
    cert = ssl.DER_cert_to_PEM_cert(connection.sock.getpeercert(binary_form=True))
    connection.quit()
    return crypto.load_certificate(crypto.FILETYPE_PEM, cert)

def get_level(limit):
    if (limit-now).days <= 7:
        return 'danger'
    if (limit-now).days <= 14:
        return 'alert'
    if (limit-now).days <= 28:
        return 'warning'
    return 'success'

def checkOnDom(domain):
    port = domain['port']
    hostname = domain['domain']
    url = hostname + ":" + str(port)
    domain['url'] = url
    domain['level'] = None
    domain['status'] = None

    # Check per SMTP STARTTLS connection
    if port == 587:
        cert = CertLimitSTARTTLS(hostname)
        limit = datetime.strptime(cert.get_notAfter().decode(),"%Y%m%d%H%M%SZ")
        date = limit.strftime("%d-%m-%Y %H:%M:%S")
        domain['level'] = get_level(limit)
        domain['status'] = date

        if domain['level'] in ['warning', 'danger']:
            subject = f"Update certificate {hostname} port {port}"
            content = f"The cert from {hostname}:{port} will be disabled at {limit}."
            domain['email_content'] = f"{subject}\n{content}"
        
    # Check other standarts SSL cert
    else:
        try:
            cert = CertLimitSSL(hostname, port)
            limit = datetime.strptime(cert['notAfter'], "%b %d %H:%M:%S %Y GMT")
            date = limit.strftime("%d-%m-%Y %H:%M:%S")
            domain['status'] = date
            domain['level'] = get_level(limit)

            if domain['level'] in ['warning', 'danger']:
                subject = f"Update certificate {hostname} port {port}"
                content = f"The cert from {hostname}:{port} will be disabled at {limit}."
                domain['email_content'] = f"{subject}\n{content}\n"

        except ssl.SSLCertVerificationError:
            domain['level'] = 'danger'
            domain['status'] = 'Invalid certificate !'

            subject = f"{hostname} port {port} : Certificate is not valid"
            content = f"{url} has not a valid certificate !"
            domain['email_content'] = f"{subject}\n{content}"

        except (ConnectionRefusedError, TimeoutError, socket.timeout, OSError):
            domain['level'] = 'danger'
            domain['status'] = 'No response !'

            subject = f"Cannot connect to {hostname} port {port}"
            content = f"{url} is not reachable !"
            domain['email_content'] = f"{subject}\n{content}"

    return domain

def checkCertDom(domains_list):
    with futures.ThreadPoolExecutor(20) as executor:
        res = executor.map(checkOnDom, domains_list)
        return res

email_warning = False
email_content = ''
email_subject = 'Certificate problem'

for key, dom in DOMAINES.items():
    print(f'\033[96m{key:<20}{"":-<60}\033[0m')
    for entry in sorted(list(checkCertDom(dom)), key=lambda d:d['url']):
        if SHELL_OUTPUT:
            method = getattr(out, entry['level'])
            method(entry['url'], entry['status'])
        if entry['level'] in ['warning', 'danger']:
            email_warning = True
            email_content += f"{entry['email_content']}\n\n"
            
if SEND_EMAIL and email_warning:
    sendMail(email_subject, email_content)

print(f"""
Validity of certificates :
{SUCCESS}More than 28 days
{WARNING}Less than 28 days
{ALERT}Less than 14 days
{DANGER}Less than 7 days
{ENDC}""")

Es ist notwendig die Liste von Domains + SMTP Server zu konfigurieren.
Als mycertscript.py speichern, und man kann es einfach mit python3 mycertscript.py aufrufen.

Gruß

Arnaud

1 „Gefällt mir“