5
# Checks an X.509 certificate:
6
# - checks if the server is running and delivers a valid certificate
7
# - checks if the CA matches a given pattern
8
# - checks the validity
10
# See the INSTALL file for installation instructions
12
# Copyright (c) 2007-2012 ETH Zurich.
14
# This module is free software; you can redistribute it and/or modify it
15
# under the terms of GNU general public license (gpl) version 3.
16
# See the LICENSE file for details.
19
# enable substitution with:
20
# $ svn propset svn:keywords "Id Revision HeadURL Source Date"
22
# $Id: check_ssl_cert 1321 2012-12-07 07:24:19Z corti $
24
# $HeadURL: https://svn.id.ethz.ch/nagios_plugins/check_ssl_cert/check_ssl_cert $
25
# $Date: 2012-12-07 08:24:19 +0100 (Fri, 07 Dec 2012) $
27
################################################################################
33
VALID_ATTRIBUTES=",startdate,enddate,subject,issuer,modulus,serial,hash,email,ocsp_uri,fingerprint,"
35
################################################################################
38
################################################################################
39
# Prints usage information
41
# $1 error message (optional)
48
#### The following line is 80 characters long (helps to fit the help text in a standard terminal)
49
######--------------------------------------------------------------------------------
52
echo "Usage: check_ssl_cert -H host [OPTIONS]"
55
echo " -H,--host host server"
58
echo " -A,--noauth ignore authority warnings (expiration only)"
59
echo " --altnames matches the pattern specified in -n with alternate"
61
echo " -C,--clientcert path use client certificate to authenticate"
62
echo " --clientpass phrase set passphrase for client certificate."
63
echo " -c,--critical days minimum number of days a certificate has to be valid"
64
echo " to issue a critical status"
65
echo " -e,--email address pattern to match the email address contained in the"
67
echo " -f,--file file local file path (works with -H localhost only)"
68
echo " -h,--help,-? this help message"
69
echo " --long-output list append the specified comma separated (no spaces) list"
70
echo " of attributes to the plugin output on additional lines."
71
echo " Valid attributes are:"
72
echo " enddate, startdate, subject, issuer, modulus, serial,"
73
echo " hash, email, ocsp_uri and fingerprint."
74
echo " 'all' will include all the available attributes."
75
echo " -i,--issuer issuer pattern to match the issuer of the certificate"
76
echo " -n,--cn name pattern to match the CN of the certificate"
77
echo " -N,--host-cn match CN with the host name"
78
echo " -o,--org org pattern to match the organization of the certificate"
79
echo " --openssl path path of the openssl binary to be used"
80
echo " -p,--port port TCP port"
81
echo " -P,--protocol protocol use the specific protocol {http|smtp|pop3|imap|ftp}"
83
echo " smtp,pop3,imap,ftp: switch to TLS"
84
echo " -s,--selfsigned allows self-signed certificates"
85
echo " -r,--rootcert path root certificate or directory to be used for"
86
echo " certficate validation"
87
echo " -t,--timeout seconds timeout after the specified time"
88
echo " (defaults to 15 seconds)"
89
echo " --temp dir directory where to store the temporary files"
90
echo " -v,--verbose verbose output"
91
echo " -V,--version version"
92
echo " -w,--warning days minimum number of days a certificate has to be valid"
93
echo " to issue a warning status"
95
echo "Deprecated options:"
96
echo " -d,--days days minimum number of days a certificate has to be valid"
97
echo " (see --critical and --warning)"
99
echo "Report bugs to: Matteo Corti <matteo.corti@id.ethz.ch>"
106
################################################################################
107
# Exits with a critical message
111
if [ -n "${CN}" ] ; then
114
printf "${SHORTNAME} CRITICAL$tmp: $1${PERFORMANCE_DATA}${LONG_OUTPUT}\n"
118
################################################################################
119
# Exits with a warning message
123
if [ -n "${CN}" ] ; then
126
printf "${SHORTNAME} WARN$tmp: $1${PERFORMANCE_DATA}${LONG_OUTPUT}\n"
130
################################################################################
131
# Exits with an 'unkown' status
135
if [ -n "${CN}" ] ; then
138
printf "${SHORTNAME} UNKNOWN$tmp: $1\n"
142
################################################################################
143
# Executes command with a timeout
145
# $1 timeout in seconds
147
# Returns 1 if timed out 0 otherwise
152
# start the command in a subshell to avoid problem with pipes
153
# (spawn accepts one command)
154
command="/bin/sh -c \"$2\""
156
if [ -n "${EXPECT}" ] ; then
157
expect -c "set echo \"-noecho\"; set timeout $time; spawn -noecho $command; expect timeout { exit 1 } eof { exit 0 }"
160
critical "Timeout after ${time} seconds"
169
################################################################################
170
# Checks if a given program is available and executable
173
# Returns 1 if the program exists and is executable
174
check_required_prog() {
176
PROG=$(which $1 2> /dev/null)
178
if [ -z "$PROG" ] ; then
179
critical "cannot find $1"
182
if [ ! -x "$PROG" ] ; then
183
critical "$PROG is not executable"
188
################################################################################
189
# Tries to fetch the certificate
191
fetch_certificate() {
193
# check if a protocol was specified (if not HTTP switch to TLS)
194
if [ -n "${PROTOCOL}" -a "${PROTOCOL}" != "http" -a "${PROTOCOL}" != "https" ] ; then
196
case "${PROTOCOL}" in
200
timeout $TIMEOUT "echo 'Q' | $OPENSSL s_client ${CLIENT} ${CLIENTPASS} -starttls ${PROTOCOL} -connect $HOST:$PORT ${SERVERNAME} -verify 6 ${ROOT_CA} 2> ${ERROR} 1> ${CERT}"
205
unknown "Error: unsupported protocol ${PROTOCOL}"
209
elif [ -n "${FILE}" ] ; then
211
if [ "${HOST}" = "localhost" ] ; then
213
timeout $TIMEOUT "/bin/cat '${FILE}' 2> ${ERROR} 1> ${CERT}"
217
unknown "Error: option 'file' works with -H localhost only"
223
timeout $TIMEOUT "echo 'Q' | $OPENSSL s_client ${CLIENT} ${CLIENTPASS} -connect $HOST:$PORT ${SERVERNAME} -verify 6 ${ROOT_CA} 2> ${ERROR} 1> ${CERT}"
227
if [ $? -ne 0 ] ; then
228
critical "Error: $(head -n 1 ${ERROR})"
237
################################################################################
239
################################################################################
247
# set the default temp dir if not set
248
if [ -z "${TMPDIR}" ] ; then
252
################################################################################
253
# process command line options
255
# we do no use getopts since it is unable to process long options
261
########################################
262
# options without arguments
264
-A|--noauth) NOAUTH=1; shift ;;
266
--altnames) ALTNAMES=1; shift ;;
268
-h|--help|-\?) usage; exit 0 ;;
270
-N|--host-cn) COMMON_NAME="__HOST__"; shift ;;
272
-s|--selfsigned) SELFSIGNED=1; shift ;;
274
-v|--verbose) VERBOSE=1; shift ;;
276
-V|--version) echo "check_ssl_cert version ${VERSION}"; exit 3; ;;
278
########################################
279
# options with arguments
281
-c|--critical) if [ $# -gt 1 ]; then
284
unknown "-c,--critical requires an argument"
287
# deprecated option: used to be as --warning
288
-d|--days) if [ $# -gt 1 ]; then
291
unknown "-d,--days requires an argument"
294
-e|--email) if [ $# -gt 1 ]; then
297
unknown "-e,--email requires an argument"
300
-f|--file) if [ $# -gt 1 ]; then
303
unknown "-f,--file requires an argument"
306
-H|--host) if [ $# -gt 1 ]; then
309
unknown "-H,--host requires an argument"
312
-i|--issuer) if [ $# -gt 1 ]; then
315
unknown "-i,--issuer requires an argument"
318
--long-output) if [ $# -gt 1 ]; then
319
LONG_OUTPUT_ATTR=$2; shift 2
321
unknown "--long-output requires an argument"
324
-n|--cn) if [ $# -gt 1 ]; then
325
COMMON_NAME=$2; shift 2
327
unknown "-n,--cn requires an argument"
330
-o|--org) if [ $# -gt 1 ]; then
331
ORGANIZATION=$2; shift 2
333
unknown "-o,--org requires an argument"
336
--openssl) if [ $# -gt 1 ]; then
339
unknown "--openssl requires an argument"
342
-p|--port) if [ $# -gt 1 ]; then
345
unknown "-p,--port requires an argument"
348
-P|--protocol) if [ $# -gt 1 ]; then
351
unknown "-P,--protocol requires an argument"
354
-r|--rootcert) if [ $# -gt 1 ]; then
357
unknown "-r,--rootcert requires an argument"
360
-C|--clientcert) if [ $# -gt 1 ]; then
361
CLIENT_CERT=$2; shift 2
363
unknown "-c,--clientcert requires an argument"
366
--clientpass) if [ $# -gt 1 ]; then
367
CLIENT_PASS=$2; shift 2
369
unknown "--clientpass requires an argument"
372
-t|--timeout) if [ $# -gt 1 ]; then
375
unknown "-t,--timeout requires an argument"
378
--temp) if [ $# -gt 1 ] ; then
382
unknown "--temp requires an argument"
385
-w|--warning) if [ $# -gt 1 ]; then
388
unknown "-w,--warning requires an argument"
391
########################################
395
-*) unknown "invalid option: $1" ;;
402
################################################################################
403
# Set COMMON_NAME to hostname if -N was given as argument
404
if [ "$COMMON_NAME" = "__HOST__" ] ; then
408
################################################################################
413
if [ -z "${HOST}" ] ; then
414
usage "No host specified"
417
if [ -n "${ALTNAMES}" -a -z "${COMMON_NAME}" ] ; then
418
unknown "--altnames requires a common name to match (--cn or --host-cn)"
421
if [ -n "${ROOT_CA}" ] ; then
422
if [ ! -r ${ROOT_CA} ] ; then
423
unknown "Cannot read root certificate ${ROOT_CA}"
425
if [ -d ${ROOT_CA} ] ; then
426
ROOT_CA="-CApath ${ROOT_CA}"
427
elif [ -f ${ROOT_CA} ] ; then
428
ROOT_CA="-CAfile ${ROOT_CA}"
430
unknown "Root certificate of unknown type $(file ${ROOT_CA} 2> /dev/null)"
434
if [ -n "${CLIENT_CERT}" ] ; then
435
if [ ! -r ${CLIENT_CERT} ] ; then
436
unknown "Cannot read client certificate ${CLIENT_CERT}"
440
if [ -n "${CRITICAL}" ] ; then
441
if ! echo "${CRITICAL}" | grep -q '[0-9][0-9]*' ; then
442
unknown "invalid number of days ${CRITICAL}"
446
if [ -n "${WARNING}" ] ; then
447
if ! echo ${WARNING} | grep -q '[0-9][0-9]*' ; then
448
unknown "invalid number of days ${WARNING}"
452
if [ -n "${CRITICAL}" -a -n "${WARNING}" ] ; then
453
if [ ${WARNING} -le ${CRITICAL} ] ; then
454
unknown "--warning (${WARNING}) is less than or equal to --critical (${CRITICAL})"
458
if [ -n "${TMPDIR}" ] ; then
459
if [ ! -d ${TMPDIR} ] ; then
460
unknown "${TMPDIR} is not a directory";
462
if [ ! -w ${TMPDIR} ] ; then
463
unknown "${TMPDIR} is not writable";
467
if [ -n "${OPENSSL}" ] ; then
468
if [ ! -x ${OPENSSL} ] ; then
469
unknown "${OPENSSL} ist not an executable"
471
if [ $(basename ${OPENSSL}) != 'openssl' ] ; then
472
unknown "${OPENSSL} ist not an openssl executable"
476
#######################
477
# Check needed programs
480
if [ -z "${OPENSSL}" ] ; then
481
check_required_prog openssl
486
EXPECT=$(which expect 2> /dev/null)
487
test -x "${EXPECT}" || EXPECT=""
488
if [ -z "${EXPECT}" -a -n "${VERBOSE}" ] ; then
489
echo "Expect not found: disabling timeouts"
492
# Perl with Date::Parse (optional)
493
PERL=$(which perl 2> /dev/null)
494
test -x "${PERL}" || PERL=""
495
if [ -z "${PERL}" -a -n "${VERBOSE}" ] ; then
496
echo "Perl not found: disabling date computations"
498
if ! ${PERL} -e "use Date::Parse;" > /dev/null 2>&1 ; then
499
if [ -n "${VERBOSE}" ] ; then
500
echo "Perl module Date::Parse not installed: disabling date computations"
505
################################################################################
506
# check if openssl s_client supports the -servername option
508
# openssl s_client does not have a -help option
509
# => we supply an invalid command line option to get the help
513
if ${OPENSSL} s_client not_a_real_option 2>&1 | grep -q -- -servername ; then
514
SERVERNAME="-servername ${HOST}"
516
if [ -n "${VERBOSE}" ] ; then
517
echo "'${OPENSSL} s_client' does not support '-servername': disabling virtual server support"
521
################################################################################
522
# fetch the X.509 certificate
524
# temporary storage for the certificate and the errors
526
CERT=$( mktemp -t "$( basename $0 )XXXXXX" 2> /dev/null )
527
if [ -z "${CERT}" ] || [ ! -w "${CERT}" ] ; then
528
unknown 'temporary file creation failure.'
531
ERROR=$( mktemp -t "$( basename $0 )XXXXXX" 2> /dev/null )
532
if [ -z "${ERROR}" ] || [ ! -w "${ERROR}" ] ; then
533
unknown 'temporary file creation failure.'
536
if [ -n "${VERBOSE}" ] ; then
537
echo "downloading certificate to ${TMPDIR}"
541
if [ -n "${CLIENT_CERT}" ] ; then
542
CLIENT="-cert ${CLIENT_CERT}"
546
if [ -n "${CLIENT_PASS}" ] ; then
547
CLIENTPASS="-pass pass:${CLIENT_PASS}"
550
# cleanup before program termination
551
# using named signals to be POSIX compliant
552
trap "rm -f $CERT $ERROR" EXIT HUP INT QUIT TERM
556
if grep -q 'sslv3\ alert\ unexpected\ message' ${ERROR} ; then
558
if [ -n "${SERVERNAME}" ] ; then
560
# some OpenSSL versions have problems with the -servername option
562
if [ -n "${VERBOSE}" ] ; then
563
echo "'${OPENSSL} s_client' returned an error: trying without '-servername'"
571
if grep -q 'sslv3\ alert\ unexpected\ message' ${ERROR} ; then
573
critical "cannot fetch certificate: OpenSSL got an unexpected message"
579
if ! grep -q "CERTIFICATE" ${CERT} ; then
580
if [ -n "${FILE}" ] ; then
581
critical "'${FILE}' is not a valid certificate file"
585
# http://stackoverflow.com/questions/1251999/sed-how-can-i-replace-a-newline-n
587
# - create a branch label via :a
588
# - the N command appends a newline and and the next line of the input
589
# file to the pattern space
590
# - if we are before the last line, branch to the created label $!ba
591
# ($! means not to do it on the last line (as there should be one final newline))
592
# - finally the substitution replaces every newline with a space on
595
ERROR_MESSAGE=$(sed -e ':a' -e 'N' -e '$!ba' -e 's/\n/; /g' ${ERROR})
596
if [ -n "${VERBOSE}" ] ; then
597
echo "Error: ${ERROR_MESSAGE}"
599
critical "No certificate returned (${ERROR_MESSAGE})"
603
################################################################################
604
# parse the X.509 certificate
606
DATE=$($OPENSSL x509 -in ${CERT} -enddate -noout | sed -e "s/^notAfter=//")
607
CN=$($OPENSSL x509 -in ${CERT} -subject -noout | sed -e "s/^.*\/CN=//" -e "s/\/[A-Za-z][A-Za-z]*=.*$//")
609
CA_O=$($OPENSSL x509 -in ${CERT} -issuer -noout | sed -e "s/^.*\/O=//" -e "s/\/[A-Z][A-Z]*=.*$//")
610
CA_CN=$($OPENSSL x509 -in ${CERT} -issuer -noout | sed -e "s/^.*\/CN=//" -e "s/\/[A-Za-z][A-Za-z]*=.*$//")
613
################################################################################
614
# Generate the long output
615
if [ -n "${LONG_OUTPUT_ATTR}" ] ; then
619
if ! echo "${VALID_ATTRIBUTES}" | grep -q ",${ATTR}," ; then
620
unknown "Invalid certificate attribute: ${ATTR}"
622
value=$(${OPENSSL} x509 -in ${CERT} -noout -${ATTR} | sed -e "s/.*=//")
623
LONG_OUTPUT="${LONG_OUTPUT}\n${ATTR}: ${value}"
629
if [ "${LONG_OUTPUT_ATTR}" = "all" ] ; then
630
LONG_OUTPUT_ATTR=${VALID_ATTRIBUTES}
632
attributes=$( echo ${LONG_OUTPUT_ATTR} | tr ',' "\n" )
633
for attribute in $attributes ; do
634
check_attr ${attribute}
639
################################################################################
640
# compute for how many days the certificate will be valid
642
if [ -n "${PERL}" ] ; then
644
CERT_END_DATE=$($OPENSSL x509 -in ${CERT} -noout -enddate | sed -e "s/.*=//")
646
DAYS_VALID=$( perl - "${CERT_END_DATE}" <<-"EOF"
653
my $cert_date = str2time( $ARGV[0] );
655
my $days = int (( $cert_date - time ) / 86400 + 0.5);
663
if [ -n "${VERBOSE}" ] ; then
664
if [ ${DAYS_VALID} -ge 0 ] ; then
665
echo "The certificate will expire in ${DAYS_VALID} day(s)"
667
echo "The certificate expired "$((- DAYS_VALID))" day(s) ago"
672
PERFORMANCE_DATA="|days=$DAYS_VALID;${WARNING};${CRITICAL};;"
678
################################################################################
679
# check the CN (this will not work as expected with wildcard certificates)
681
if [ -n "$COMMON_NAME" ] ; then
689
# check alterante names
690
if [ -n "${ALTNAMES}" ] ; then
691
for alt_name in $( $OPENSSL x509 -in ${CERT} -text | \
692
grep --after-context=1 '509v3 Subject Alternative Name:' | \
693
tail -n 1 | sed -e "s/DNS://g" | sed -e "s/,//g" ) ; do
695
$alt_name) ok='true' ;;
700
if [ -z "$ok" ] ; then
701
critical "invalid CN ('$CN' does not match '$COMMON_NAME')"
706
################################################################################
709
if [ -n "$ISSUER" ] ; then
714
if echo $CA_CN | grep -q "^$ISSUER$" ; then
716
CA_ISSUER_MATCHED="${CA_CN}"
719
if echo $CA_O | grep -q "^$ISSUER$" ; then
721
CA_ISSUER_MATCHED="${CA_O}"
724
if [ -z "$ok" ] ; then
725
critical "invalid CA ('$ISSUER' does not match '$CA_O' or '$CA_CN')"
730
CA_ISSUER_MATCHED="${CA_CN}"
734
################################################################################
737
# we always check expired certificates
738
if ! $OPENSSL x509 -in ${CERT} -noout -checkend 0 ; then
739
critical "certificate is expired (was valid until $DATE)"
742
if [ -n "${CRITICAL}" ] ; then
744
if ! $OPENSSL x509 -in ${CERT} -noout -checkend $(( ${CRITICAL} * 86400 )) ; then
745
critical "certificate will expire on $DATE"
750
if [ -n "${WARNING}" ] ; then
752
if ! $OPENSSL x509 -in ${CERT} -noout -checkend $(( ${WARNING} * 86400 )) ; then
753
warning "certificate will expire on $DATE"
758
################################################################################
759
# check the organization
761
if [ -n "$ORGANIZATION" ] ; then
763
ORG=$($OPENSSL x509 -in ${CERT} -subject -noout | sed -e "s/.*\/O=//" -e "s/\/.*//")
765
if ! echo $ORG | grep -q "^$ORGANIZATION" ; then
766
critical "invalid organization ('$ORGANIZATION' does not match '$ORG')"
771
################################################################################
772
# check the organization
774
if [ -n "$ADDR" ] ; then
776
EMAIL=$($OPENSSL x509 -in ${CERT} -email -noout)
778
if [ -n "${VERBOSE}" ] ; then
779
echo "checking email (${ADDR}): ${EMAIL}"
782
if [ -z "${EMAIL}" ] ; then
783
critical "the certficate does not contain an email address"
786
if ! echo $EMAIL | grep -q "^$ADDR" ; then
787
critical "invalid email ($ADDR does not match $EMAIL)"
792
################################################################################
793
# Check if the certificate was verified
795
if [ -z "${NOAUTH}" ] && grep -q '^verify\ error:' ${ERROR} ; then
797
if grep -q '^verify\ error:num=[0-9][0-9]*:self\ signed\ certificate' ${ERROR} ; then
799
if [ -z "${SELFSIGNED}" ] ; then
800
critical "Cannot verify certificate\nself signed certificate"
802
SELFSIGNEDCERT="self signed "
808
details=$(grep '^verify\ error:' ${ERROR} | sed -e "s/verify\ error:num=[0-9]*:/verification error: /" )
810
critical "Cannot verify certificate\n${details}"
816
################################################################################
817
# If we get this far, assume all is well. :)
819
# if --altnames was specified we show the specified CN instead of
821
if [ -n "${ALTNAMES}" -a -n "${COMMON_NAME}" ] ; then
825
if [ -n "${DAYS_VALID}" ] ; then
827
if [ ${DAYS_VALID} -gt 1 ] ; then
828
DAYS_VALID=" (expires in ${DAYS_VALID} days)"
829
elif [ ${DAYS_VALID} -eq 1 ] ; then
830
DAYS_VALID=" (expires tomorrow)"
831
elif [ ${DAYS_VALID} -eq 0 ] ; then
832
DAYS_VALID=" (expires today)"
833
elif [ ${DAYS_VALID} -eq -1 ] ; then
834
DAYS_VALID=" (expired yesterday)"
836
DAYS_VALID=" (expired ${DAYS_VALID} days ago)"
840
echo "${SHORTNAME} OK - X.509 ${SELFSIGNEDCERT}certificate for '${CN}' from '${CA_ISSUER_MATCHED}' valid until ${DATE}${DAYS_VALID}${PERFORMANCE_DATA}${LONG_OUTPUT}"
846
if [ "${1}" != "--source-only" ]; then