-
-
Save bmatthewshea/2f4301b769a46e7eb10d554a52a864b3 to your computer and use it in GitHub Desktop.
#!/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 |
- added
-d
(days) to show warnings - added
-c
color flag for file lists - added warnings (yellow) based on 30 day default (see
-d
)
- DRY'ed it down a bit.
- cleaned up unnecessary comments.
- other cosmetic/misc improvements
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...
@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:
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!
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
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 - 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
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?
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
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 ;)
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.
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.').
Hi,
IP is most important. If hostname is problem - dont do it - it is not important.
IP is most important. If hostname is problem - dont do it - it is not important.
Gotcha/Understood. Will look into it asap.
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
}
Once that was done I changed the two usages of
date
in thecheck_expiry
function to usegdate
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.
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 :
Note: If no arguments/flags given the script defaults to interactive mode/loop.