Script to check SSL Cert Expiration via nagios

Someone on the sage-members mailing list asked about checking SSL expiration dates. We use the following script to check them via nagios (actually, we use a slightly older version that takes hostname and port instead of URL, but this is the next version we plan to roll out). I thought I’d post it here (with the permission of the author, who is no longer at Tufts) for others to use:

#!/usr/local/bin/perl
###########################################################################
#####                                                                     #
#####     check_cert.pl -- check  HTTPS,  IMAPS,  LDAPS or SMTP (with     #
#####     STARTTLS) certificate expiration and, optionally, naming.       #
#####                                                                     #
############| mike ryan                       -- 02/03/06, 14:15 |#########

use 5.6.1;
use strict;
use warnings;
no warnings qw(redefine);

use Date::Parse qw(str2time);
use English;
use Getopt::Long;
use URI;

use lib '/usr/local/nagios/libexec';
use utils qw(%ERRORS);

sub usage {
    my %params = @_;

    if (defined($params{'message'})) {
        my $message = $params{'message'};
        $message =~ s/n*$/n/s;
        print STDERR $message;
    }

    print STDERR <<EOF;
Usage: $PROGRAM_NAME --warn=<days> --critical=<days> --host=<hostname> --protocol=<protocol> [<options>]
    --critical=<days>       number of days before certificate expiration
                            to return a critical status

    --help                  display this message

    --url=<url>             URL to check.  supported schemes: file,
                            https, imaps, ldaps, smtp, smtps.

    --name=<name>           expected name on certificate

    --warning=<days>        number of days before certificate expiration
                            to return a warning status
EOF

    if (!defined($params{'error'}) || $params{'error'}) {
        print "UNKNOWN: bad usagen";
        exit($ERRORS{'UNKNOWN'});
    }
}

###########################################################################
#####                                                                     #
#####     init.                                                           #
#####                                                                     #
###########################################################################

my %opt;
GetOptions(%opt,
    'critical=i',
    'help',
    'name=s',
    'url=s',
    'warning=i',
) || usage();

if (defined($opt{'help'})) {
    usage();
}

# check for required options
foreach my $option (qw{critical warning url}) {
    if (!defined($opt{$option})) {
        usage(message => "--$option required");
    }
}

# check URL scheme type
my $uri = URI->new($opt{'url'});
my $scheme = lc($uri->scheme);
if (!defined($scheme) || ($scheme eq '')) {
    usage(message => "invalid URL");
}

###########################################################################
#####                                                                     #
#####     retrieve certificate                                            #
#####                                                                     #
###########################################################################

my $certdata;

my $host = $uri->opaque();
$host =~ s/^/+//;
my $port = undef;
if ($host =~ s/:(d+)//) {
    $port = $1;
}
$host ||= 'localhost';

if ($scheme eq 'file') {
    $certdata = read_file($uri->path());

} elsif ($scheme eq 'https') {
    $port ||= 443;

    $certdata = s_client("", "-connect $host:$port");

} elsif ($scheme eq 'imaps') {
    $port ||= 993;

    $certdata = s_client(". logout", "-connect $host:$port");

} elsif ($scheme eq 'ldaps') {
    $port ||= 636;

    $certdata = s_client("", "-connect $host:$port");

} elsif ($scheme eq 'smtp') {
    $port ||= 25;

    $certdata = s_client("quit", "-starttls smtp -connect $host:$port");

} elsif ($scheme eq 'smtps') {
    $port ||= 465;

    $certdata = s_client("quit", "-connect $host:$port");

} else {
    usage(message => "unsupported scheme ($scheme)");
}

if (!defined($certdata)) {
    print "UNKNOWN: failed to retrieve certificate datan";
    exit($ERRORS{'UNKNOWN'});
}

# make sure certificate validity isn't in the future.
my $check_start_time = time;
if (!defined($certdata->{'not_before'})) {
    print "UNKNOWN: failed to parse Not Before validityn";
    exit($ERRORS{'UNKNOWN'});
}
if ($check_start_time < $certdata->{'not_before'}) {
    printf("CRITICAL: certificate not valid until %sn", scalar(localtime($certdata->{'not_before'})));
    exit($ERRORS{'CRITICAL'});
}

# make sure certificate validity isn't in the past.
if (!defined($certdata->{'not_after'})) {
    print "UNKNOWN: failed to parse Not After validitiyn";
    exit($ERRORS{'UNKNOWN'});
}
if ($check_start_time > $certdata->{'not_after'}) {
    printf("CRITICAL: certificate expired %sn", scalar(localtime($certdata->{'not_after'})));
    exit($ERRORS{'CRITICAL'});
}

# check for impending expiration.
my $expires_in = int(($certdata->{'not_after'} - $check_start_time) / (24*60*60));
if ($expires_in <= $opt{'critical'}) {
    printf("CRITICAL: certificate expires in $expires_in days (%s)n", scalar(localtime($certdata->{'not_after'})));
    exit($ERRORS{'CRITICAL'});
}
if ($expires_in <= $opt{'warning'}) {
    printf("WARNING: certificate expires in $expires_in days (%s)n", scalar(localtime($certdata->{'not_after'})));
    exit($ERRORS{'WARNING'});
}

# optionally check for name mismatch.
if (defined($opt{'name'})) {
    if (!defined($certdata->{'cn'})) {
        print "UNKNOWN: failed to parse certificate namen";
        exit($ERRORS{'UNKNOWN'});
    }
    if (lc($opt{'name'}) ne lc($certdata->{'cn'})) {
        printf("CRITICAL: certificate name mismatch (expected %s, got %s)n", $opt{'name'}, $certdata->{'cn'});
        exit($ERRORS{'CRITICAL'});
    }
}

# note good certificate.
printf("OK: certificate expires in %d days (%s)n", $expires_in, scalar(localtime($certdata->{'not_after'})));
exit($ERRORS{'OK'});

###########################################################################
#####                                                                     #
#####     certificate retrieval functions.                                #
#####                                                                     #
###########################################################################

sub s_client {
    my ($command, $arguments) = @_;

    return(parse_certificate(scalar(`/usr/bin/echo $command | /usr/local/bin/openssl s_client $arguments 2>/dev/null | /usr/local/bin/openssl x509 -text 2>/dev/null`)));
}

sub read_file {
    my ($filename) = @_;

    return(parse_certificate(scalar(`/usr/local/bin/openssl x509 -in $filename -text 2>/dev/null`)));
}

sub parse_certificate {
    my ($text) = @_;

    my %result;

    foreach my $line (split(/[rn]+/, $text)) {
        if ($line =~ /^s+Not Before: (.*)$/ ){
            $result{'not_before'} = str2time($1);
        } elsif ($line =~ /^s+Not After : (.*)$/ ){
            $result{'not_after'} = str2time($1);
        } elsif ($line =~ /^s+Subject:.*, CN=(.*)$/ ){
            $result{'cn'} = $1;
        }
    }

    if (scalar(keys(%result)) == 0) {
        print("CRITICAL: failed to retrieve certificaten");
        exit($ERRORS{'CRITICAL'});
    }

    return(%result);
}