Current File : //lib64/nagios/plugins/check_ssl_validity |
#! /usr/bin/perl
# nagios: -epn
# Complete (?) check for valid SSL certificate
# Originally by Anders Nordby (anders@fupp.net), 2015-02-16
# Copied with permission on 2019-09-26 (https://github.com/nagios-plugins/nagios-plugins/issues/72)
# and modified to fit the needs of the nagios-plugins project.
# Copyright: GPLv2
# Checks all of the following:
# Fetch SSL certificate from URL (on optional given host)
# Does the certificate contain our hostname?
# Has the certificate expired?
# Download (and cache) CRL
# Has the certificate been revoked?
use Getopt::Std;
use File::Temp qw(tempfile);
use File::Basename;
use Crypt::X509;
use Date::Parse;
use Date::Format qw(ctime);
use POSIX qw(strftime);
use Digest::MD5 qw(md5_hex);
use LWP::Simple;
use Text::Glob qw(match_glob);
use Getopt::Long;
Getopt::Long::Configure('bundling');
GetOptions(
"h" => \$opt_h, "help" => \$opt_h,
"d" => \$opt_d, "debug" => \$opt_d,
"o" => \$opt_o, "ocsp" => \$opt_o,
"ocsp-host=s" => \$opt_ocsp_host,
"C=s" => \$opt_C, "crl-cache-frequency=s" => \$opt_C,
"I=s" => \$opt_I, "ip=s" => \$opt_I,
"p=i" => \$opt_p, "port=i" => \$opt_p,
"H=s" => \$opt_H, "cert-hostname=s" => \$opt_H,
"w=i" => \$opt_w, "warning=i" => \$opt_w,
"c=i" => \$opt_c, "critical=i" => \$opt_c,
"t" => \$opt_t, "timeout" => \$opt_t
);
my $chainfh, $chainfile, $escaped_tempfile, $ocsp_status;
sub usage {
print "check_ssl_validity -H <cert hostname> [-I <IP/host>] [-p <port>]\n[-t <timeout>] [-w <expire warning (days)>] [-c <expire critical (dats)>]\n[-C (CRL update frequency in seconds)] [-d (debug)] [--ocsp] [--ocsp-host]\n";
print "\nWill look for hostname provided with -H in the certificate, but will contact\n";
print "server with host/IP provided by -I (optional)\n";
exit(1);
}
sub updatecrl {
my $url = shift;
my $fn = shift;
my $content = get($url);
if (defined($content)) {
if (open(CACHE, ">$cachefile")) {
print CACHE $content;
} else {
doexit(2, "Could not open file $fn for writing CRL temp file for cert on $host:$port.");
}
close(CACHE);
} else {
doexit(2, "Could not download CRL Distribution Point URL $url for cert on $hosttxt.");
}
}
sub ckserial {
return if ($crserial eq "");
if ($serial eq $crserial) {
if ($crrev ne "") {
$crrevtime = str2time($crrev);
$revtime = $crrevtime-$uxtime;
if ($revtime < 0) {
doexit(2, "Found certificate for $vhost on CRL $crldp revoked already at date $crrev");
} elsif (($revtime/86400) < $crit) {
doexit(2, "Found certificate for $vhost on CRL $crldp revoked at date $crrev, within critical time frame $crit");
} elsif (($revtime/86400) < $warn) {
doexit(1, "Found certificate for $vhost on CRL $crldp revoked at date $crrev, within warning time frame $warn");
}
}
doexit(1, "Found certificate for $vhost on CRL $crldp revoked $crrev. Time to check the revokation date");
}
}
usage unless ($opt_H);
# Defaults
if ($opt_p) {
$port = $opt_p;
} else {
$port = 443;
}
if ($opt_t) {
$tmout = $opt_t;
} else {
$tmout = 10;
}
if ($opt_C) {
$crlupdatefreq = $opt_C;
} else {
$crlupdatefreq = 86400;
}
$vhost = $opt_H;
if ($opt_I) {
$host = $opt_I;
} else {
$host = $vhost;
}
$hosttxt = "$host:$port";
if ($opt_w && $opt_w =~ /^\d+$/) {
$warn = $opt_w;
} else {
$warn = 30;
}
if ($opt_c && $opt_c =~ /^\d+$/) {
$crit = $opt_c;
} else {
$crit = 30;
}
sub doexit {
my $ret = shift;
my $txt = shift;
if ($ret == 0) {
print "OK: ";
}
elsif ($ret == 1) {
print "WARNING: ";
}
elsif ($ret == 2) {
print "CRITICAL: ";
}
else {
print "UNKNOWN: ";
}
print "$txt\n";
exit($ret);
}
$alldata = "";
$cert = "";
$mode = 0;
open(CMD, "echo | openssl s_client -servername $vhost -connect $host:$port 2>&1 |");
while (<CMD>) {
$alldata .= $_;
if ($mode == 0) {
if (/-----BEGIN CERTIFICATE-----/) {
$cert .= $_;
$mode = 1;
}
} elsif ($mode == 1) {
$cert .= $_;
if (/-----END CERTIFICATE-----/) {
$mode = 2;
}
}
}
close(CMD);
$ret = $?;
if ($ret != 0) {
$alldata =~ s@\n@ @g;
$alldata =~ s@\s+$@@;
doexit(2, "Error connecting to $hosttxt: $alldata");
} elsif ($cert eq "") {
doexit(2, "No certificate found on $hosttxt");
} else {
($tmpfh,$tempfile) = tempfile(DIR=>'/tmp',UNLINK=>0);
doexit(2, "Failed to open temp file: $!") unless (defined($tmpfh));
$tmpfh->print($cert);
$tmpfh->close;
}
$dercert = `openssl x509 -in $tempfile -outform DER 2>&1`;
$ret = $?;
if ($ret != 0) {
$dercert =~ s@\n@ @g;
$dercert =~ s@\s+$@@;
doexit(2, "Could not convert certificate from PEM to DER format: $dercert");
}
$decoded = Crypt::X509->new( cert => $dercert );
if ($decoded->error) {
doexit(2, "Could not parse X509 certificate on $hosttxt: " . $decoded->error);
}
$oktxt = "";
$cn = $decoded->subject_cn;
if ($opt_d) { print "Found CN: $cn\n"; }
if ($vhost eq $decoded->subject_cn) {
$oktxt .= "Host $vhost matches CN $vhost on $hosttxt ";
} elsif ($decoded->subject_cn =~ /^.*\.(.*)$/) {
$wcdomain = $1;
$domain = $vhost;
$domain =~ s@^[\w\-]+\.@@;
if ($domain eq $wcdomain) {
$oktxt .= "Host $vhost matches wildcard CN " . $decoded->subject_cn . " on $hosttxt ";
}
}
if ($oktxt eq "") {
# Cert not yet found
if (defined($decoded->SubjectAltName)) {
# Check altnames
$altfound = 0;
foreach $altnametxt (@{$decoded->SubjectAltName}) {
if ($altnametxt =~ /^dNSName=(.*)/) {
$altname = $1;
if ($opt_d) { print "Found SAN: $altname\n"; }
if (match_glob($altname, $vhost)) {
$altfound = 1;
$oktxt .= "Host $vhost found in SAN on $hosttxt ";
last;
}
}
}
if ($altfound == 0) {
doexit(2, "Host $vhost not found in certificate on $hosttxt, not in CN or in alternative names");
}
} else {
doexit(2, "Host $vhost not found in certificate on $hosttxt, not in CN and no alternative names found");
}
}
# Check expire time
$uxtimegmt = strftime "%s", gmtime;
$uxtime = strftime "%s", localtime;
$certtime = $decoded->not_after;
$certdays = ($certtime-$uxtimegmt)/86400;
$certdaysfmt = sprintf("%.1f", $certdays);
if ($certdays < 0) {
doexit(2, "${oktxt}but it is expired ($certdaysfmt days)");
} elsif ($certdays < $crit) {
doexit(2, "${oktxt}but it is expiring in only $certdaysfmt days, critical limit is $crit.");
} elsif ($certdays < $warn) {
doexit(1, "${oktxt}but it is expiring in only $certdaysfmt days, warning limit is $warn.");
}
$serial = $decoded->serial;
$serial = lc(sprintf("%x", $serial));
if ($opt_d) {
print "Certificate serial: $serial\n";
}
if ($opt_o) {
# Do OCSP instead of CRL checking
$ocsp_uri = `openssl x509 -noout -ocsp_uri -in $tempfile`;
$ocsp_uri =~ s/\s+$//;
($chainfh,$chainfile) = tempfile(DIR=>'/tmp',UNLINK=>0);
# Get the certificate chain
$chain_raw = `echo "Q" | openssl s_client -servername $vhost -connect $host:$port -showcerts 2>/dev/null`;
$mode = 0;
for(split /^/, $chain_raw) {
if (/-----BEGIN CERTIFICATE-----/) {
$mode += 1;
}
# Skip the first certificate returned
if ($mode > 1) {
$chain_processed .= $_;
}
if (/-----END CERTIFICATE-----/) {
if ($mode > 1) {
$mode -= 1;
}
}
}
$chainfh->print($chain_processed);
$chainfh->close;
$ocsp_cache = md5_hex($chain_processed);
$ocsp_cache_file = dirname(__FILE__) . "/ssl_validity_data_" . $ocsp_cache;
open(OCSP_CACHE, $ocsp_cache_file);
while (my $line = <OCSP_CACHE>) {
chomp $line;
if ($line =~ /[0-9]+/) {
$next_update_time = $line;
}
}
close(OCSP_CACHE);
$current_time = time();
if ($current_time < $next_update_time) {
# Use cached result
$next_update_time_str = ctime($next_update_time);
chomp $next_update_time_str;
$ocsp_status = "good (cached until $next_update_time_str)";
}
else {
# Time to update
$cmd = "openssl ocsp -issuer $chainfile -verify_other $chainfile -cert $tempfile -url $ocsp_uri -text";
if ($opt_ocsp_host) {
$cmd .= " -header \"Host\" \"$opt_ocsp_host\"";
}
open(CMD, $cmd . " 2>/dev/null |");
$escaped_tempfile = $tempfile;
$escaped_tempfile =~ s/([\\\|\(\)\[\]\{\}\^\$\*\+\?\.])/\1/g;
$ocsp_status = "unknown";
while (<CMD>) {
chomp;
if ($_ =~ s/Next Update: (.*)/$1/) {
$next_update_time = str2time($_);
}
if ($_ =~ s/$escaped_tempfile: (.*)/$1/) {
$ocsp_status = $_;
}
}
if ($ocsp_status =~ /good/) {
open(OCSP_CACHE, ">", $ocsp_cache_file);
print OCSP_CACHE $next_update_time;
close(OCSP_CACHE);
}
}
my $exit_code = 2;
if ($ocsp_status =~ /good/) {
$exit_code = 0;
}
doexit($exit_code, "$oktxt; OCSP responder ($ocsp_uri) says certificate is $ocsp_status");
}
else {
# Do CRL-based checking
@crldps = @{$decoded->CRLDistributionPoints};
$crlskip = 0;
foreach $crldp (@crldps) {
if ($opt_d) {
print "Checking CRL DP $crldp.\n";
}
$cachefile = "/tmp/" . md5_hex($crldp) . "_crl.tmp";
if (-f $cachefile) {
$cacheage = $uxtime-(stat($cachefile))[9];
if ($cacheage > $crlupdatefreq) {
if ($opt_d) { print "Download update, more than a day old.\n"; }
updatecrl($crldp, $cachefile);
} else {
if ($opt_d) { print "Reusing cached copy.\n"; }
}
} else {
if ($opt_d) { print "Download initial copy.\n"; }
updatecrl($crldp, $cachefile);
}
$crl = "";
my $format;
open(my $cachefile_io, '<', $cachefile);
$format = <$cachefile_io> =~ /-----BEGIN X509 CRL-----/ ? 'PEM' : 'DER';
close $cachefile_io;
open(CMD, "openssl crl -inform $format -text -in $cachefile -noout 2>&1 |");
while (<CMD>) {
$crl .= $_;
}
close(CMD);
$ret = $?;
if ($ret != 0) {
$crl =~ s@\n@ @g;
$crl =~ s@\s+$@@;
doexit(2, "Could not parse $format from URL $crldp while checking $hosttxt: $crl");
}
# Crude CRL parsing goes here
$mode = 0;
foreach $cline (split(/\n/, $crl)) {
if ($cline =~ /.*Next Update: (.+)/) {
$nextup = $1;
$nextuptime = str2time($nextup);
$crlvalid = $nextuptime-$uxtime;
if ($opt_d) { print "Next CRL update: $nextup\n"; }
if ($crlvalid < 0) {
doexit(2, "Could not use CRL from $crldp, it expired past next update on $nextup");
}
} elsif ($cline =~ /.*Last Update: (.+)/) {
$lastup = $1;
if ($opt_d) { print "Last CRL update: $lastup\n"; }
} elsif ($mode == 0) {
if ($cline =~ /.*Serial Number: (\S+)/i) {
ckserial;
$crserial = lc($1);
$crrev = "";
} elsif ($cline =~ /.*Revocation Date: (.+)/i) {
$crrev = $1;
}
} elsif ($cline =~ /Signature Algorithm/) {
last;
}
}
ckserial;
}
}
if (-f $tempfile) {
unlink ($tempfile);
}
$oktxt =~ s@\s+$@@;
print "$oktxt, still valid for $certdaysfmt days. ";
if ($crlskip == 0) {
print "Serial $serial not found on any Certificate Revokation Lists.\n";
} else {
print "CRL checks skipped, next check in " . ($crlupdatefreq - $cacheage) . " seconds.\n";
}
exit 0;