Dev.to Security 🔐 Cybersecurity 👁 0 📖 7 min read

Monitor SSL Certificate Expiry with Python and GitHub Actions

A website that worked fine yesterday suddenly shows a big red warning today: "Your connection is not private." The site itself is fine. Only the SSL certificate ran out. Most visitors won't click past that screen. They'

A website that worked fine yesterday suddenly shows a big red warning today: "Your connection is not private." The site itself is fine. Only the SSL certificate ran out.

Most visitors won't click past that screen. They'll assume something is wrong and leave.

This usually happens for a boring reason. Auto-renewal was set up once, then a hosting move, a changed DNS record or a forgotten payment quietly broke it. Nobody noticed until the certificate expired.

This guide builds a small check that runs every day and warns you weeks before it happens. It uses one short Python script and GitHub Actions, and it's free.

Why this matters more now

SSL certificates are getting shorter-lived. Since March 15, 2026, publicly trusted certificates can last at most 200 days, down from 398. That drops to 100 days in March 2027 and 47 days in March 2029.

Shorter lifetimes mean more renewals. The first certificates issued under the 200-day limit are reaching their end around now.

If your renewals are automatic, this is mostly fine. But automatic only works while it keeps working, and that's what the check is for.

What you need

  • One or more websites served over HTTPS
  • A GitHub repository for the script. A new, empty one is fine

The script uses only what Python already includes, so there's nothing to install.

Step 1: Add the script

Create a file called check_ssl.py in your repository:

import os
import socket
import ssl
import sys
from datetime import datetime, timezone

DOMAINS = os.environ["DOMAINS"].split(",")   # for example: example.com,shop.example.com
WARN_DAYS = int(os.environ.get("WARN_DAYS", "21"))


def days_left(domain):
    context = ssl.create_default_context()
    with socket.create_connection((domain, 443), timeout=10) as sock:
        with context.wrap_socket(sock, server_hostname=domain) as tls:
            cert = tls.getpeercert()
    expires = datetime.fromtimestamp(ssl.cert_time_to_seconds(cert["notAfter"]), timezone.utc)
    return (expires - datetime.now(timezone.utc)).days


problems = []
for domain in (d.strip() for d in DOMAINS):
    try:
        left = days_left(domain)
    except Exception as error:  # expired, wrong name, site down...
        problems.append(f"{domain}: could not check the certificate ({error})")
        continue
    print(f"{domain}: {left} days left")
    if left < WARN_DAYS:
        problems.append(f"{domain}: certificate expires in {left} days")

if problems:
    print("\nProblems found:")
    print("\n".join(f"- {p}" for p in problems))
    sys.exit(1)

Here's how it works:

  1. It connects to each site on port 443, the normal port for HTTPS.
  2. It reads the expiry date from the certificate the site presents.
  3. It counts the days left.
  4. If any site has fewer days than the limit, or the certificate can't be checked at all, the script ends with an error.

That last part matters. When a script exits with an error, GitHub marks the run as failed, and that's what triggers your alert.

The check also catches certificates that are already expired or don't match the site name. In those cases the connection itself fails, and the script reports the reason.

Step 2: Add the workflow

Create .github/workflows/ssl-check.yml:

name: SSL certificate check

on:
  schedule:
    - cron: '41 5 * * *'   # every day at 05:41 UTC
  workflow_dispatch:        # adds a "Run workflow" button for testing

permissions:
  contents: read

jobs:
  check:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4   # use the latest major version

      - name: Check certificates
        env:
          DOMAINS: example.com,www.example.com   # change this
          WARN_DAYS: '21'
        run: python3 check_ssl.py

Change DOMAINS to your own sites, separated by commas. List every hostname you use, including subdomains. A certificate for example.com doesn't tell you anything about shop.example.com.

Two small choices are worth noting:

  • The odd start time. 41 5 avoids the top of the hour, when GitHub is busiest and scheduled runs are more likely to be delayed.
  • The timeout. If a site hangs, the job stops after 5 minutes instead of running for hours.

A run like this takes a few seconds, but GitHub rounds each job up to a full minute. Running it daily uses about 30 minutes a month. That's nothing on a public repository, and a small part of the free allowance on a private one.

Step 3: Get alerted

By default, GitHub sends an email when a scheduled workflow fails. It goes to the person who last edited the schedule in the workflow file, as long as your notification settings allow it. Check your GitHub notification settings for Actions to be sure.

That may be all you need. If you'd like the alert somewhere you'll see it faster, add a message to a Discord channel. Put this step at the end of the job:

      - name: Send alert
        if: failure()
        env:
          WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK }}
        run: |
          curl -sS -X POST -H "Content-Type: application/json" \
            -d '{"content": "SSL certificate check failed. Open the workflow run for details."}' \
            "$WEBHOOK_URL"

Add your Discord webhook address as a repository secret named DISCORD_WEBHOOK. Never paste it into the workflow file, because anyone who has the address can post to your channel.

If webhooks are new to you, I wrote a beginner guide to how webhooks work.

Step 4: Test it

Go to the Actions tab in your repository, choose "SSL certificate check" and click "Run workflow". You should see a green check and a line for each site, like example.com: 63 days left.

To see an alert, change WARN_DAYS to 999 and run it again. Every site will count as "too close to expiring," the run will fail, and you'll get your email. Change it back to 21 afterwards.

Choosing the warning window

WARN_DAYS sets how early you want to be told.

Many automatic renewal setups start renewing a few weeks before the certificate expires. If you set the warning to 21 days, an alert usually means renewal should have happened by now and didn't. That's the useful signal.

If you renew by hand, use a longer window, like 30 days, so you have time to act.

What to do when the alert arrives

Don't panic. Weeks of warning is the point.

  1. Check whether your hosting provider or certificate tool renews automatically, and whether it reported an error.
  2. Look for recent changes: a new host, changed DNS records, a moved domain, or a payment that failed.
  3. Renew the certificate and run the workflow again to confirm.

A quick manual check

If you just want to look at one site right now, this command shows the expiry date:

echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate

It's handy for a one-off look. The script is better for anything you want to keep an eye on.

What this check can't do

  • It only checks what you list. A forgotten subdomain isn't watched until you add it.
  • It only checks port 443. Mail servers and other services use their own certificates.
  • It doesn't fix anything. It tells you a problem is coming, and you still have to renew the certificate.
  • It only sees what the server shows. If you use a service that serves different certificates to different visitors, you may see only one of them.

Will it keep running?

There's a catch that's easy to miss. In a public repository, GitHub switches off scheduled workflows after 60 days without any repository activity. A monitor that quietly stops is worse than none, because you think you're covered.

Put a reminder in your calendar or glance at the Actions tab now and then. I go into this more in how to monitor your automations when something breaks.

Quick answers

How long does an SSL certificate last?
Public certificates can now last at most 200 days. That limit falls to 100 days in March 2027 and 47 days in March 2029. Free authorities like Let's Encrypt already issue shorter ones.

What happens when a certificate expires?
Browsers show a full-page security warning. Most visitors leave, and some tools and apps refuse to connect at all.

How often should I check?
Once a day is plenty. A certificate doesn't go from healthy to expired in an hour.

Can this check a site that isn't mine?
Yes. It only opens a normal HTTPS connection, the same as a browser does. It's still a good idea to keep the list short and to your own sites.

The short version

An expired certificate is one of the most avoidable outages there is. It doesn't come as a surprise. The date has been printed on the certificate the whole time.

A daily check turns it into a routine warning weeks ahead. If you want a check for the site itself, my Python uptime monitor guide pairs well with this one.

You can find more practical automation guides on Procwire.

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.