Skip to content

Instantly share code, notes, and snippets.

@bmatthewshea
Last active March 1, 2023 22:13
Show Gist options
  • Star 31 You must be signed in to star a gist
  • Fork 12 You must be signed in to fork a gist
  • Save bmatthewshea/2f4301b769a46e7eb10d554a52a864b3 to your computer and use it in GitHub Desktop.
Save bmatthewshea/2f4301b769a46e7eb10d554a52a864b3 to your computer and use it in GitHub Desktop.
Retrieve/Check SSL certificate expiration date(s)
#!/bin/bash
# By B Shea Dec2018 & Mar2020
# https://www.holylinux.net
# Test for OpenSSL - if not installed stop here.
if ! [[ -x $(which openssl) ]]; then
printf "\nOpenSSL not found or not executable.\nPlease install OpenSSL before proceeding.\n\n"
exit 1
fi
### user adjustable variables ###
#openssl query timeout:
openssl_timeout="timeout 10"
# 30 days is default on warnings - overidden on command line with '-d':
days_to_warn=30
# default name for file lists
sitelist=./websites.txt
### Clear/list/set defaults for variables ###
epoch_day=86400
epoch_warning=$((days_to_warn*epoch_day))
regex_numbers='^[0-9]+$'
expire="0"
website=""
port=""
tls="0"
sTLS=""
show_tls=""
certfilename=""
location=""
filename=""
displaysite=""
#COLORS
color="0"
RED=$(tput setaf 1) #expired!!
GREEN=$(tput setaf 2) #within bounds
YELLOW=$(tput setaf 3) #warning/date close!
NC=$(tput sgr0) #reset to normal
#
usage="
$(basename "$0") [-h] [-c] [-d DAYS] [-f FILENAME] | [-w WEBSITE] | [-s SITELIST]
Retrieve the expiration date(s) on SSL certificate(s) using OpenSSL.
Usage:
-h Help
-c Color output
-d Amount of days to show warnings (default is 30 days)
Example: -d 15
-f SSL date from FILENAME
Example: -f /home/user/example.pem
-w SSL date from SITE(:PORT) (Port defaults to 443)
Example: -w www.example.com
-s SSL date(s) from SITELIST
Example: -s ./websites.txt
List format: sub.domain.tld:993 (one per line - port optional)
Example:
$ $(basename "$0") -c -d 14 -s ./websites.txt
WARNS (in color) if within 14 days of expiring on each entry in the file list.
"
#FUNCTIONS
is_integer() {
if ! [[ "$1" =~ $regex_numbers ]]; then
printf "\nError.\nNot a number. You used a parameter that requires a whole number.\n$usage"
exit 1
fi
}
menu_input() {
echo
echo "1: Enter file location of certificate"
echo "2: Enter an Internet site in form of subdomain.domain.tld(:port)"
echo
read -p "Enter 1 or 2 (anything else quits): " -n 1 -r
echo
}
get_lookup_input() {
location=""
echo
read -p "Please enter the $lookuptype location: " location
}
set_format() {
set_formatting="%-40s%-25s\n"
set_formatting_green=$set_formatting
set_formatting_yellow=$set_formatting
set_formatting_red=$set_formatting
printf "\nWarning is $days_to_warn days.\n"
printf "Color is "
if [[ $color == "1" ]]; then
set_formatting_green="$GREEN%-40s$NC%-25s\n"
set_formatting_yellow="$YELLOW%-40s$NC%-25s\n"
set_formatting_red="$RED%-40s$NC%-25s\n"
printf "enabled.\n\n"
else
printf "disabled.\n\n"
fi
printf "$set_formatting" "LOCATION" "EXPIRATION DATE"
printf "$set_formatting" "--------" "---------------"
}
parse_port() {
port=443
tls="0"
show_tls=""
parseurl=$(echo $website | awk '$1 ~ /^.*:/' | cut -d':' -f1)
parseport=$(echo $website | awk '$1 ~ /^.*:/' | cut -d':' -f2)
if [[ $parseport =~ $regex_numbers ]]; then # -> port was found
website=$parseurl
port=$parseport
if [[ $port == "587" ]]; then # Use TLS lookup and notify
show_tls=" (TLS)"
tls="1"
fi
fi
}
check_expiry() {
expire="0"
# use epoch times for calcs/compares
today_epoch="$(date +%s)"
sTLS=""
if [[ $tls == "1" ]]; then
sTLS=" -starttls smtp"
fi
if [ "$lookuptype" == "FILENAME" ]; then
expire_date=$(openssl x509 -in $certfilename$sTLS -noout -dates 2>/dev/null | \
awk -F= '/^notAfter/ { print $2; exit }')
else
expire_date=$($openssl_timeout openssl s_client -servername $website -connect $website:$port$sTLS </dev/null 2>/dev/null | \
openssl x509 -noout -dates 2>/dev/null | \
awk -F= '/^notAfter/ { print $2; exit }')
fi
if ! [[ -z $expire_date ]]; then # -> found date-process it:
expire_epoch=$(date +%s -d "$expire_date")
timeleft=`expr $expire_epoch - $today_epoch`
if [[ $timeleft -le $epoch_warning ]]; then #WARN
expire="1"
fi
if [[ $today_epoch -ge $expire_epoch ]]; then #EXPIRE
expire="2"
fi
else
expire="3"
expire_date="N/A "
fi
}
output_site() {
parse_port
check_expiry
if [ "$lookuptype" != "FILENAME" ]; then
display_site="$website:$port$show_tls"
else
display_site="$filename$show_tls"
fi
if [[ $expire == "1" ]]; then
printf "$set_formatting_yellow" "$display_site" "$expire_date !" # YELLOW OUTPUT - warning
elif [[ $expire == "2" ]]; then
printf "$set_formatting_red" "$display_site" "$expire_date !!" # RED OUTPUT - expired
elif [[ $expire == "3" ]]; then
printf "$set_formatting" "$display_site" "$expire_date !!!" # NO COLOR - NOT FOUND
else
printf "$set_formatting_green" "$display_site" "$expire_date" # GREEN OUTPUT
fi
}
#
client_lookup() {
lookuptype="WEBSITE"
if [[ -z $website ]]; then #loop lookup - ask for input
get_lookup_input
website=$location
fi
set_format
output_site
lookuptype=""
website=""
echo
}
file_lookup() {
lookuptype="FILENAME"
if [[ -z $certfilename ]]; then #loop lookup - ask for input
get_lookup_input
certfilename=$location
fi
filename=$(basename -- "$certfilename")
set_format
output_site
lookuptype=""
filename=""
echo
}
list_lookup() {
lookuptype="FILELIST"
file_contents=$(<$sitelist)
set_format
while IFS= read -r website; do
if ! [[ -z $website ]]; then
output_site
fi
done <<<"$file_contents"
lookuptype=""
echo
}
#HANDLE ARGUMENTS
while getopts ':hcd:f:s:w:' option; do
case "$option" in
h) printf "$usage"
exit 0
;;
c) color="1"
;;
d) is_integer "$OPTARG"
if [ "$OPTARG" -ge 1 -a "$OPTARG" -le 365 ]; then
days_to_warn="$OPTARG"
epoch_warning=$((days_to_warn*epoch_day))
else
printf "\nDays must be between 1 and 365\n$usage"
exit 1
fi
;;
f) certfilename=$OPTARG
[[ -r $certfilename ]] && file_lookup || printf "\nFile not found/not readable. Permissions?\n\n"; exit 1;
exit 0
;;
s) sitelist=$OPTARG
[[ -r $sitelist ]] && list_lookup || printf "\nFile not found/not readable. Permissions?\n\n"; exit 1;
exit 0
;;
w) website=$OPTARG
client_lookup
exit 0
;;
:) printf "\nYou specified a flag that needs an argument.\n$usage" 1>&2
exit 1
;;
*) printf "\nI do not understand '"$1" "$2"'.\n$usage" 1>&2
exit 1
;;
esac
done
shift $((OPTIND - 1))
#LOOP RUN (default if no flags)
if [ $# -eq 0 ]; then # no command line arguments/flags found
printf "\nNo flags used or available. Interactive mode.\n"
while :
do
menu_input
if [[ $REPLY == "1" ]]
then
file_lookup
elif [[ $REPLY == "2" ]]
then
client_lookup
else # exit
[[ "$0" = "$BASH_SOURCE" ]] && exit 1 || return 1
fi
echo
done
fi
@bmatthewshea
Copy link
Author

bmatthewshea commented Dec 13, 2018

show_ssl_expire [-h] [-c] [-d DAYS] [-f FILENAME] | [-w WEBSITE] | [-s SITELIST]

Retrieve the expiration date(s) on SSL certificate(s) using OpenSSL.

Usage:
    -h  Help

    -c  Color output

    -d  Amount of days to show warnings (default is 30 days)
        Example: -d 15

    -f  SSL date from FILENAME
        Example: -f /home/user/example.pem

    -w  SSL date from SITE(:PORT) (Port defaults to 443)
        Example: -w www.example.com

    -s  SSL date(s) from SITELIST
        Example:      -s ./websites.txt
        List format:  sub.domain.tld:993 (one per line - port optional)

Example:
    $ show_ssl_expire -c -d 14 -s ./websites.txt

    WARNS (in color) if within 14 days of expiring on each entry in the file list.

Note: If no arguments/flags given the script defaults to interactive mode/loop.

shea22-2019-01-01_190003
shea22-2019-01-01_185545
shea22-2019-01-01_190205

@bmatthewshea
Copy link
Author

bmatthewshea commented Dec 14, 2018

  • added -d (days) to show warnings
  • added -c color flag for file lists
  • added warnings (yellow) based on 30 day default (see -d)

@bmatthewshea
Copy link
Author

  • DRY'ed it down a bit.
  • cleaned up unnecessary comments.
  • other cosmetic/misc improvements

@bmatthewshea
Copy link
Author

bmatthewshea commented Mar 8, 2020

Added OpenSSL TLS checking if port == 587. Also prints "(TLS)" since this is a special lookup.
Moved some stuff around.

SHEA99-2020-03-10_110447

@narkan
Copy link

narkan commented Mar 27, 2020

Loving this, but not been able to get it to work yet ... I just want to check the expiry of all my websites' SSL certificates.

When I create a file called ./websites.txt and enter two urls into it, I get the following:

$ ./show_ssl_expire -c -d 100 -s ./websites.txt

Warning is 100 days.
Color is enabled.

LOCATION                                EXPIRATION DATE          
--------                                ---------------          
comec.org.uk:443                        N/A                      !!!
thecommunitychurch.online:443           N/A                      !!!

Both sites have SSL, one is expired...

@bmatthewshea
Copy link
Author

bmatthewshea commented May 5, 2020

@narkan - Sorry for late reply. Just saw your comment.
Just tested it with your sites above and it worked fine.
I would check that OpenSSL is properly installed and working correctly.
Try this command directly (which works for me):

openssl s_client -servername comec.org.uk -connect comec.org.uk:443 </dev/null 2>/dev/null | \
                  openssl x509 -noout -dates 2>/dev/null | \
                  awk -F= '/^notAfter/ { print $2; exit }'

What does that show? Does OpenSSL load? Errors?

If that doesn't work, check your OpenSSL version as well:

openssl version

OpenSSL 1.1.1  11 Sep 2018

It should output a version and be reasonably up to date. If OpenSSL works, make sure you can use nslookup on the hosts you list. I assume your DNS lookups are working, though..

If you can figure out what 'broke' it, I will add a check to the script so it alerts you. Right now it only alerts you if openssl is missing.

What I show:

SHEA22-2020-05-05_091256

SHEA22-2020-05-05_090201

@narkan
Copy link

narkan commented May 6, 2020

Hi - thanks so much for your reply. I think everythings up to date. (Mac: Catalina OS - 10.15.4 (19E266))

$ openssl s_client -servername comec.org.uk -connect comec.org.uk:443 </dev/null 2>/dev/null | \ openssl x509 -noout -dates 2>/dev/null | \ awk -F= '/^notAfter/ { print $2; exit }'
Jun 25 08:19:18 2020 GMT 
(the correct cert expiry)

$ openssl version
LibreSSL 2.8.3

$ /usr/bin/ssh -V
OpenSSH_8.1p1, LibreSSL 2.7.3

$ ./show_ssl_expire -c -s ./websites.txt 

Warning is 30 days.
Color is enabled.

LOCATION                                EXPIRATION DATE          
--------                                ---------------          
comec.org.uk:443                        N/A                      !!!
thecommunitychurch.online:443           N/A                      !!!

Appreciate your help - this is exactly what I need for those pesky Let's Encrypt certs than only auto-renew 80% of the time!

@masterwebsk
Copy link

masterwebsk commented May 13, 2020

Hello, great script.
Is it possible to add arguments for IP and server hostname? (im often moving websites between servers..)

domain.tld:443 123.123.123.123 server1 date_expiry

Thanks

@bmatthewshea
Copy link
Author

bmatthewshea commented May 20, 2020

Hello, great script.
Is it possible to add arguments for IP and server hostname? (im often moving websites between servers..)

domain.tld:443 123.123.123.123 server1 date_expiry

Thanks

@masterwebsk ,
Yeah, I can do that. I think it's a good idea. I'll add another optional flag. Should have some time this weekend.
Just so we are clear on what you need, here is what the core command in script would do when it queries (after I update) -

openssl s_client -servername subdomain.domain.tld -connect 123.123.123.123:443 </dev/null 2>/dev/null | \ openssl x509 -noout -dates 2>/dev/null | \ awk -F= '/^notAfter/ { print $2; exit }'

^Connects to given IP (could be a private/lan IP, too) using port 443 and uses the -servername shown as the request.
Again I think this would be of benefit, so will add it soon regardless. Let me know, though, if this is what you mean..?

@bmatthewshea
Copy link
Author

bmatthewshea commented May 20, 2020

Hi - thanks so much for your reply. I think everythings up to date. (Mac: Catalina OS - 10.15.4 (19E266))
..
Appreciate your help - this is exactly what I need for those pesky Let's Encrypt certs than only auto-renew 80% of the time!

@narkan ^Yes, that was exactly why I made the script: Short lived LE certificates on dozens of systems I admin. Sick of missing renews that fail for one reason or another. Now I just throw them all in a text list and have cron run this script and send output to my email once a week.

RE: your issue-

I am not sure what it could be?
openssl s_client is working on same machine. Therefore nameservice/DNS is working - which was the only other thing I could think of for the "N/A's". The only other thing it could possibly be is one of the commands inside the BASH script is giving a different result (BSD/Mac versus GNU/Linux versions). As I do not have a Mac on hand anymore I cannot test this hypothesis.

My guess: awk and/or cut commands are having a problem.

Copy/Paste this on your Mac BASH console/terminal:

website=www.example.com:443
parseurl=$(echo $website | awk '$1 ~ /^.*:/' | cut -d':' -f1)
parseport=$(echo $website | awk '$1 ~ /^.*:/' | cut -d':' -f2)

You should show:

echo $parseurl
example.com

echo $parseport
443

SHEA99-2020-05-20_103147

If you do not, we found the issue. Not much else it could be.. I will look through code and find all the commands and try them on a Mac when I have access to one. Once we find the issue I will update script so it works on BSD/Mac, too. Thought I used strict POSIX in script commands. Guess not?

@masterwebsk
Copy link

Hello, great script.
Is it possible to add arguments for IP and server hostname? (im often moving websites between servers..)
domain.tld:443 123.123.123.123 server1 date_expiry
Thanks

@masterwebsk ,
Yeah, I can do that. I think it's a good idea. I'll add another optional flag. Should have some time this weekend.
Just so we are clear on what you need, here is what the core command in script would do when it queries (after I update) -

openssl s_client -servername subdomain.domain.tld -connect 123.123.123.123:443 </dev/null 2>/dev/null | \ openssl x509 -noout -dates 2>/dev/null | \ awk -F= '/^notAfter/ { print $2; exit }'

^Connects to given IP (could be a private/lan IP, too) using port 443 and uses the -servername shown as the request.
Again I think this would be of benefit, so will add it soon regardless. Let me know, though, if this is what you mean..?

Hi, I dont want to put IP´s to domains in my websites.txt - I need/want see real IP in results as I often move websites between servers - to know on which server is actually website already is.: https://jet.masterweb.sk/jet/5rciPkSReN.png

@narkan
Copy link

narkan commented May 21, 2020

That all works as expects.

echo $parseurl
example.com

echo $parseport
443

I think the problem may be with the version of openssl. I had a similar issue with something else recently (can't remember what) and found the below post. I've tried all the suggested fixes (and made things worse I think!!) but still have the issue!
aisingapore/TagUI#86 (comment)

Please don't waste any more of your time on it - I think your script's fine - it's my system that the problem! Thanks for everything tho ;)

@bmatthewshea
Copy link
Author

bmatthewshea commented May 22, 2020

That all works as expects.

echo $parseurl
example.com

echo $parseport
443

@narkan
Yep - not sure what it could be then. ^

I'll have access to a Mac in a few days and I'll try to remember to pick it apart and find the issue.
I want it to work for everyone w/ a BASH shell.

You're welcome / is NP - I enjoy working on it/scripts when time allows.

@bmatthewshea
Copy link
Author

bmatthewshea commented May 22, 2020

Hi, I dont want to put IP´s to domains in my websites.txt - I need/want see real IP in results as I often move websites between servers - to know on which server is actually website already is.: https://jet.masterweb.sk/jet/5rciPkSReN.png

@masterwebsk
So, if I understand, by 'real IP' you want the DNS lookup/IP & 'local' hostname in the script output?

Like:

NAME                    LOCATION                      EXPIRATION DATE
www.example.com:443     (DNS IP) / (hostname)         (Date)

Not quite sure what you mean by hostname? DNS / Hostname queries (by default/normally) are done by checking 'files' (/etc/hosts) first, then the systems DNS service/resolver (be that public or private or both) - in that order. (from /etc/nsswitch.conf).
So is that what you want? A local hostname (if found) under LOCATION? The public hostname (assuming this wasn't found in /etc/hosts) is already shown ( = 'www.').

@masterwebsk
Copy link

Hi,

IP is most important. If hostname is problem - dont do it - it is not important.

@bmatthewshea
Copy link
Author

IP is most important. If hostname is problem - dont do it - it is not important.

Gotcha/Understood. Will look into it asap.

@callmehyde
Copy link

callmehyde commented Aug 10, 2020

Leaving this for anyone else who might run across this on MacOS (Catalina 10.15.6 as of writing). At first I was getting an error that said date: illegal time format. Found the issue was with the date command and how different versions of it work.

By default MacOS ships with a BSD date command while this script expects a GNU one. I wasn't smart enough to figure out how to get it working with the default date command so I installed coreutils through Homebrew.

Once that was done I changed the two usages of date in the check_expiry function to use gdate instead. That seemed to work for me! Hope this helps someone.

check_expiry() {
    expire="0"
    # use epoch times for calcs/compares
    today_epoch="$(gdate +%s)"
    sTLS=""

    if [[ $tls == "1" ]]; then
      sTLS=" -starttls smtp"
    fi

    if [ "$lookuptype" == "FILENAME" ]; then
      expire_date=$(openssl x509 -in $certfilename$sTLS -noout -dates 2>/dev/null | \
                  awk -F= '/^notAfter/ { print $2; exit }')
    else
      expire_date=$($openssl_timeout openssl s_client -servername $website -connect $website:$port$sTLS </dev/null 2>/dev/null | \
                  openssl x509 -noout -dates 2>/dev/null | \
                  awk -F= '/^notAfter/ { print $2; exit }')
    fi
    if ! [[ -z $expire_date ]]; then # -> found date-process it:
      expire_epoch=$(gdate +%s -d "$expire_date")
      timeleft=`expr $expire_epoch - $today_epoch`
      if [[ $timeleft -le $epoch_warning ]]; then #WARN
        expire="1"
      fi
      if [[ $today_epoch -ge $expire_epoch ]]; then #EXPIRE
        expire="2"
      fi
    else
      expire="3"
      expire_date="N/A                     "
    fi
}

@bmatthewshea
Copy link
Author

bmatthewshea commented Oct 31, 2020

@callmehyde -

Once that was done I changed the two usages of date in the check_expiry function to use gdate instead. That seemed to work for me! Hope this helps someone.

      today_epoch="$(gdate +%s)"
      ...
      expire_epoch=$(gdate +%s -d "$expire_date")

Awesome - glad you figured that out. And thanks for the input.

I will add a check to code. Something like this: https://stackoverflow.com/a/8748193/503621

& sorry been really busy with work and haven't had time for any side projects, lately.

@bmatthewshea
Copy link
Author

bmatthewshea commented Nov 2, 2020

This should work (replace line 143 w/ this):

expire_epoch=$(date -j -f "%b %d %T %Y %Z" "$expire_date" "+%s") # BSD VERSION of DATE

That should be the only incompatible statement.
The today_epoch="$(date +%s)" worked for me on a BSD bash shell.

Let me know if that fixes it (without using GNU/"gdate" version).

PS -
I have a new version of this script done - just need to test it some more :

SHEA99-2020-11-02_100005

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment