Barebone Scripts to Check SSL Certificate Expiration

May 14, 2025

Minimal, cron-ready scripts in Bash, Python, Ruby, Node.js (JavaScript), Go, and Powershell to check when your website's SSL certificate expires.

Author
Mike Robbins

Let's Encrypt announced that they'll be Ending Support for Expiration Notification Emails on June 4th, 2025. These emails were sent to anyone who was issued an SSL certificate but didn't renew it in a timely manner, giving you a few weeks of grace period to fix the problem before the certificate actually expired and caused user-facing downtime! These emails continued a grand old tradition from the pre-ACME Certificate Authorities who'd email you multiple times in the weeks before expiration, reminding you to renew.

  • The bad news: there's no more backstop from the CA to give you a friendly warning in advance—your users will surely let you know once it's fully expired!
  • The good news: in many programming languages, it's just a few lines of code to check an SSL certificate's expiration date.

I'll show six code snippets below so you can ensure your team is alerted well in advance of any SSL certificates that are expiring soon.

But isn't SSL certificate renewal totally automated now?

Automated certificate renewals are great, but not entirely foolproof:

The projects above (and other ACME clients) are not to blame: if you browse through these issues, users are generally not finding bugs in the code. Instead, they're hitting a variety of configuration issues, DNS issues, auth tokens, timeouts, dependencies, etc. which break their automated certificate renewals.

As any crufty old engineer who's been doing this long enough knows, the automation will break eventually even if you do everything right. The Let's Encrypt emails were your last line of defense to prevent an avoidable outage, even if you never received one. Now it's up to you to check for upcoming expiration.

How many people could really be having this issue?

From Let's Encrypt's own announcement: "Providing expiration notifications costs Let’s Encrypt tens of thousands of dollars per year [...]" (emphasis added) Holey moley, that's a lot of expiration notification emails!

Let's Encrypt is providing a tremendously valuable service to the public, free of charge, and we are incredibly grateful. It's entirely reasonable that they wouldn't want to bear the operating costs of doing expiration notification.

This change just means that anyone responsible for an HTTPS website or API should probably take a few minutes and set up their own monitors to let them know if and when their certificate renewals are broken for whatever reason.

Code Snippets

Here are scripts in six different programming languages that all do the same thing:

  1. Connect to https://example.com:443/
  2. Read the server's SSL certificate "not after" date
  3. Calculate the number of days remaining
  4. Have an if block that runs if there are less than 14 days until expiration, and an else block that runs if there are more.

I've personally tried to make these scripts as minimal as possible so they're easy for you to understand and customize. You should be able to copy and paste any of these and run them as-is:

Bash (shell script)

We use openssl s_client to open a connection, and pipe its certificate into openssl x509 -noout -enddate to extract a single field:

#!/bin/bash
host="example.com"

end_date=$(echo \
  | openssl s_client -servername "$host" -connect "$host:443" 2>/dev/null \
  | openssl x509 -noout -enddate \
  | cut -d= -f2)

# Convert a string like "Jan 15 23:59:59 2026 GMT" to seconds since epoch
end_epoch=$(date -d "$end_date" +%s)
now_epoch=$(date +%s)

days=$(( (end_epoch - now_epoch) / 86400 ))
if [ "$days" -lt 14 ]; then
  echo "WARNING! SSL certificate expires in only $days days! Somebody should fix it!"
  # TODO: Add alerting here
else
  echo "ok: certificate valid for $days days"
  # TODO: Add a cronjob "OK" check-in here
fi

Python

In Python, the tls.getpeercert()['notAfter'] contains the string we're looking for:

#!/usr/bin/env python3
import ssl, socket
from datetime import datetime

host = 'example.com'

with socket.create_connection((host, 443)) as tcp:
    with ssl.create_default_context().wrap_socket(tcp, server_hostname=host) as tls:
        cert = tls.getpeercert()

# Parse a string like "Jan 15 23:59:59 2026 GMT" into a datetime
not_after = datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z')

days = (not_after - datetime.utcnow()).days
if days < 14:
    print(f"WARNING! SSL certificate expires in only {days} days! Somebody should fix it!")
    # TODO: Add alerting here
else:
    print(f"ok: certificate valid for {days} days")
    # TODO: Add a cronjob "OK" check-in here

Ruby

In Ruby, tls.peer_cert.not_after is already a Time instance:

#!/usr/bin/env ruby
require "socket"
require "openssl"

host = "example.com"

tcp = TCPSocket.new(host, 443)
tls = OpenSSL::SSL::SSLSocket.new(tcp)
tls.hostname = host
tls.connect
not_after = tls.peer_cert.not_after
tls.sysclose
tcp.close

days = ((not_after - Time.now) / 86400).to_i
if days < 14
  puts "WARNING! SSL certificate expires in only #{days} days! Somebody should fix it!"
  # TODO: Add alerting here
else
  puts "ok: certificate valid for #{days} days"
  # TODO: Add a cronjob "OK" check-in here
end

Node.js (JavaScript)

In Node.js, socket.getPeerCertificate().valid_to is a string:

#!/usr/bin/env node
const tls = require('tls');
const host = 'example.com';
const port = 443;

const socket = tls.connect(port, host, {servername: host}, () => {
    const cert = socket.getPeerCertificate();
    const notAfter = new Date(cert.valid_to);
    const days = Math.floor((notAfter - Date.now()) / (1000*86400));
    socket.end();

    if (days < 14) {
        console.log(`WARNING! SSL certificate expires in only ${days} days! Somebody should fix it!`);
        // TODO: Add alerting here
    } else {
        console.log(`ok: certificate valid for ${days} days`);
        // TODO: Add a cronjob "OK" check-in here
    }
});

Go

In Go, conn.ConnectionState().PeerCertificates[0].NotAfter is already a time.Time:

package main

import (
    "crypto/tls"
    "fmt"
    "time"
)

func main() {
    conn, err := tls.Dial("tcp", "example.com:443", nil)
    if err != nil {
        panic(err)
    }

    notAfter := conn.ConnectionState().PeerCertificates[0].NotAfter
    conn.Close()

    days := int(time.Until(notAfter).Hours() / 24)
    if days < 14 {
        fmt.Printf("WARNING! SSL certificate expires in only %d days! Somebody should fix it!\n", days)
        // TODO: Add alerting here
    } else {
        fmt.Printf("ok: certificate valid for %d days\n", days)
        // TODO: Add a cronjob "OK" check-in here
    }
}

Powershell

In Powershell, the X509Certificate2 class has a NotAfter property as a DateTime:

$hostname = "example.com"
$tcp = New-Object System.Net.Sockets.TcpClient($hostname, 443)
$tls = New-Object System.Net.Security.SslStream($tcp.GetStream())
$tls.AuthenticateAsClient($hostname)

# Upgrade to the newer X509Certificate2 class for its NotAfter property
$cert2  = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($tls.RemoteCertificate)
$notAfter = $cert2.NotAfter

$tls.Close()
$tcp.Close()

$days = ($notAfter - (Get-Date)).Days
if ($days -lt 14) {
    Write-Output "WARNING! SSL certificate expires in only $days days! Somebody should fix it!"
    # TODO: Add alerting here
} else {
    Write-Output "ok: certificate valid for $days days"
    # TODO: Add a cronjob "OK" check-in here
}

Using the scripts

  1. Replace example.com with your domain name. (Note: you may eventually want to monitor multiple domains, such as example.com and www.example.com and api.example.com.)
  2. Make the if block do something that gets your attention. (Example: send an email, fire off a Heii On-Call alert, send a Slack message, etc.)
  3. Make the else block do something that lets you know this monitoring script is still running regularly. (Example: check in with a Heii On-Call "Inbound Liveness" trigger.)
  4. Deploy the script with a cron job to run daily.

That's it!

By embedding one of the above scripts into a daily cron job and plugging in your own notifications, you'll eliminate surprise outages from expired certificates. If you'd rather offload this entirely, Heii On-Call offers built-in SSL certificate expiration monitoring, alongside cron job heartbeats, HTTP uptime checks, on-call rotations, and mobile iOS and Android apps, so you're notified well before any preventable outages happen! And if you're a belt-and-suspenders type like me, you'd probably want to deploy one of the above the scripts internally, in addition to external monitoring.

For more information on SSL certificate monitoring, see also:

You can set up triggers with "SSL Certificate Minimum Expiration Duration" on Heii On-Call's free plan in just a few minutes. Sleep easier knowing you'll be alerted well in advance of an easily avoidable customer-facing outage.