Check certificate expiration for domains
I manage a handful of websites (around 60), and to automate my job I wrote a script to check for the expiration of SSL certificates.
Most of the sites I manage use LetsEncrypt and are self-hosted by me on Linode, but sometimes (never?) the certbot renew doesn’t work or some external hosting company decides to renew the certificate 2 days before the expiration. Why? I don’t know.
LetsEncrypt will automatically renew the certificate if less than 30 days are remaining, so this script will rarely report a problem.
So this script runs daily and warns me if some host certificate will expire soon, so I can manually check.
This script used Net::SSL::ExpireDate to check for expiration date, but it seems it doesn’t like Cloudflare certificates, so I added another function to get the certificate expiration date using openssl.
This script is available via github:
https://gist.github.com/sburlot/716884ec89d79147cc4bdcd12d9ffe27
Usage: enter the list of sites by modifying the line:
my @sites = qw/coriolis.ch textfiles.com/;
and run (via crontab, after your certbot renew cron)
Helpful links:
https://prefetch.net/articles/checkcertificate.html
https://www.cilogon.org/cert-expire
#!/usr/bin/perl # vi:set ts=4 nu: use strict; use POSIX 'strftime'; use Net::SSL::ExpireDate; use Date::Parse; use Data::Dumper; use MIME::Lite; my $status = ""; my @sites = qw/coriolis.ch textfiles.com/; my $error_sites = ""; my %expiration_sites; ################################################################################################ sub check_site_with_openssl($) { my $site = shift @_; my $expire_date = `echo | openssl s_client -servername $site -connect $site:443 2>&1 | openssl x509 -noout -enddate 2>&1`; if ($expire_date !~ /notAfter/) { print "Error while getting info for certificate: $site\n"; $error_sites .= "$site has no expiration date\n"; return; } $expire_date =~ s/notAfter=//g; my $time = str2time($expire_date); my $now = time; my $days = int(($time-$now)/86400); $expiration_sites{$site} = $days; $status .= "$site expires in $days days\n"; print "$site expires in $days days\n"; if ($days < 25) { $error_sites .= "$site => in $days day" . ($days > 1 ? "s":"") . "\n"; } } ################################################################################################ sub check_site($) { my $site = shift @_; # we have an error for sites served via Cloudflare: record type is SSL3_AL_FATAL # Net::SSL doesnt support SSL3?? my $ed = Net::SSL::ExpireDate->new( https => $site ); #print Dumper $ed; if (defined $ed->expire_date) { my $expire_date = $ed->expire_date; # return DateTime instance my $time = str2time($expire_date); my $now = time; my $days = int(($time-$now)/86400); $expiration_sites{$site} = $days; print "$site expires in $days days\n"; if ($days < 25) { $error_sites .= "$site => in $days day" . ($days > 1 ? "s":"") . "\n"; } } else { $error_sites .= "$site has no expiration date\n"; # or has another error, but I'll check manually. } } ################################################################################################ sub send_email($) { my $message = shift @_; my $msg = MIME::Lite->new( From => 'me@website.com', To => 'me@website.com', Subject => 'SSL Certificates', Data => "One or more certificates should be renewed:\n\n$message\n" ); $msg->send; } ################################################################################################ print strftime "%F\n", localtime; print "="x30 . "\n"; for my $site (sort @sites) { check_site_with_openssl($site); } # sort desc by expiration foreach my $site (sort { $expiration_sites{$a} <=> $expiration_sites{$b} } keys %expiration_sites) { $status .= "$site expires in " . $expiration_sites{$site} . " days\n" ; } print "="x30 . "\n"; if ($error_sites ne "") { send_email($error_sites); }