root / plugins / ssl / ssl-certificate-expiry @ 94066132
Historique | Voir | Annoter | Télécharger (7,49 ko)
| 1 |
#!/bin/sh -u |
|---|---|
| 2 |
# -*- sh -*- |
| 3 |
# shellcheck shell=dash |
| 4 |
|
| 5 |
: << =cut |
| 6 |
|
| 7 |
=head1 NAME |
| 8 |
|
| 9 |
ssl-certificate-expiry - Plugin to monitor Certificate expiration on multiple services and ports |
| 10 |
|
| 11 |
=head1 CONFIGURATION |
| 12 |
|
| 13 |
[ssl-certificate-expiry] |
| 14 |
env.services www.service.tld blah.example.net_PORT foo.example.net_PORT_STARTTLS |
| 15 |
|
| 16 |
PORT is the TCP port number |
| 17 |
STARTTLS is passed to openssl as "-starttls" argument. Useful for services like SMTP or IMAP implementing StartTLS. |
| 18 |
Current known values are ftp, imap, pop3 and smtp |
| 19 |
PORT is mandatory if STARTTLS is used. |
| 20 |
|
| 21 |
To set warning and critical levels do like this: |
| 22 |
|
| 23 |
[ssl-certificate-expiry] |
| 24 |
env.services ... |
| 25 |
env.warning 30: |
| 26 |
env.proxy PROXYHOST:PORT # optional, enables openssl operation over proxy |
| 27 |
env.checkname yes # optional, checks if used servername is covered by certificate |
| 28 |
env.skip_cert_hashes 2e5ac55d # optional, skip check of certs with those hashes (2e5ac55d is DST Root CA X3, cross-signing Let's Encrypt certs, but expiring on 2021-09-30) |
| 29 |
|
| 30 |
Alternatively, if you want to monitor hosts separately, you can create multiple symlinks named as follows. |
| 31 |
|
| 32 |
ssl-certificate-expiry_HOST_PORT |
| 33 |
|
| 34 |
For example: |
| 35 |
|
| 36 |
ssl-certificate-expiry_www.example.net |
| 37 |
ssl-certificate-expiry_www.example.org_443 |
| 38 |
ssl-certificate-expiry_192.0.2.42_636 |
| 39 |
ssl-certificate-expiry_2001:0DB8::badc:0fee_485 |
| 40 |
ssl-certificate-expiry_mail.example.net_25_smtp |
| 41 |
|
| 42 |
=head2 Cron setup |
| 43 |
|
| 44 |
To avoid having to run the SSL checks during the munin-update, it is possible |
| 45 |
to run it from cron, and save a cachefile to be read during the update, This is |
| 46 |
particularly useful when checking a large number of certificates, or when some |
| 47 |
of the hosts are slow. |
| 48 |
|
| 49 |
To do so, add a cron job running the plugin with cron as the argument: |
| 50 |
|
| 51 |
<minute> * * * <user> /usr/sbin/munin-run/ssl-certificate-expiry cron |
| 52 |
|
| 53 |
<user> should be the user that has write permission to the MUNIN_PLUGSTATE. |
| 54 |
<minute> should be a number between 0 and 59 when the check should run every hour. |
| 55 |
|
| 56 |
If, for any reason, the cron script stops running, the script will revert to |
| 57 |
uncached updates after the cache file is older than an hour. |
| 58 |
|
| 59 |
=head1 AUTHORS |
| 60 |
|
| 61 |
* Pactrick Domack (ssl_) |
| 62 |
* Olivier Mehani (ssl-certificate-expiry, skip_cert_hashes) |
| 63 |
* Martin Schobert (check for intermediate certs) |
| 64 |
* Arndt Kritzner (hostname verification and proxy usage) |
| 65 |
|
| 66 |
* Copyright (C) 2013 Patrick Domack <patrickdk@patrickdk.com> |
| 67 |
* Copyright (C) 2017, 2019, 2021 Olivier Mehani <shtrom+munin@ssji.net> |
| 68 |
* Copyright (C) 2020 Martin Schobert <martin@schobert.cc> |
| 69 |
|
| 70 |
=head1 LICENSE |
| 71 |
|
| 72 |
=cut |
| 73 |
|
| 74 |
# shellcheck disable=SC1091 |
| 75 |
. "${MUNIN_LIBDIR}/plugins/plugin.sh"
|
| 76 |
|
| 77 |
if [ "${MUNIN_DEBUG:-0}" = 1 ]; then
|
| 78 |
set -x |
| 79 |
fi |
| 80 |
|
| 81 |
HOSTPORT=${0##*ssl-certificate-expiry_}
|
| 82 |
CACHEFILE="${MUNIN_PLUGSTATE}/$(basename "${0}").cache"
|
| 83 |
|
| 84 |
if [ "${HOSTPORT}" != "${0}" ] \
|
| 85 |
&& [ -n "${HOSTPORT}" ]; then
|
| 86 |
services="${HOSTPORT}"
|
| 87 |
fi |
| 88 |
|
| 89 |
|
| 90 |
# Read data including a certificate from stdin and output the (fractional) number of days left |
| 91 |
# until the expiry of this certificate. The output is empty if parsing failed. |
| 92 |
parse_valid_days_from_certificate() {
|
| 93 |
local input_data |
| 94 |
local valid_until_string |
| 95 |
local valid_until_epoch |
| 96 |
local now_epoch |
| 97 |
local input_data |
| 98 |
input_data=$(cat) |
| 99 |
|
| 100 |
if echo "$input_data" | grep -q -- "-----BEGIN CERTIFICATE-----"; then |
| 101 |
cert_data=$(echo "$input_data" | openssl x509 -noout -subject_hash -enddate) |
| 102 |
|
| 103 |
# Skip certificate if its hash is in env.skip_cert_hashes |
| 104 |
hash="$(echo "${cert_data}" | head -n 1)"
|
| 105 |
echo "${skip_cert_hashes:-}" | grep -iqwF "${hash}" && return
|
| 106 |
|
| 107 |
valid_until_string=$(echo "$cert_data" \ |
| 108 |
| grep "^notAfter=" | cut -f 2 -d "=") |
| 109 |
if [ -n "$valid_until_string" ]; then |
| 110 |
# FreeBSD requires special arguments for "date" |
| 111 |
if uname | grep -q ^FreeBSD; then |
| 112 |
valid_until_epoch=$(date -j -f '%b %e %T %Y %Z' "$valid_until_string" +%s) |
| 113 |
now_epoch=$(date -j +%s) |
| 114 |
else |
| 115 |
valid_until_epoch=$(date --date="$valid_until_string" +%s) |
| 116 |
now_epoch=$(date +%s) |
| 117 |
fi |
| 118 |
if [ -n "$valid_until_epoch" ]; then |
| 119 |
# calculate the number of days left |
| 120 |
echo "$valid_until_epoch" "$now_epoch" | awk '{ print(($1 - $2) / (24 * 3600)); }'
|
| 121 |
fi |
| 122 |
fi |
| 123 |
fi |
| 124 |
} |
| 125 |
|
| 126 |
|
| 127 |
print_expire_days() {
|
| 128 |
local host="$1" |
| 129 |
local port="$2" |
| 130 |
local starttls="$3" |
| 131 |
|
| 132 |
# Wrap IPv6 addresses in square brackets |
| 133 |
echo "$host" | grep -q ':' && host="[$host]" |
| 134 |
|
| 135 |
local s_client_args='' |
| 136 |
[ -n "$starttls" ] && s_client_args="$s_client_args -starttls $starttls" |
| 137 |
[ -n "${proxy:-}" ] && s_client_args="$s_client_args -proxy $proxy"
|
| 138 |
[ -n "${checkname:-}" ] && [ "$checkname" = "yes" ] && s_client_args="$s_client_args -verify_hostname $host"
|
| 139 |
|
| 140 |
# We extract and check the server certificate, |
| 141 |
# but the end date also depends on intermediate certs. Therefore |
| 142 |
# we want to check intermediate certs as well. |
| 143 |
# |
| 144 |
# The following cryptic lines do: |
| 145 |
# - invoke openssl and connect to a port |
| 146 |
# - print certs, not only the server cert |
| 147 |
# - extract each certificate as a single line |
| 148 |
# - pipe each cert to the parse_valid_days_from_certificate |
| 149 |
# function, which basically is 'openssl x509 -enddate' |
| 150 |
# - get a list of the parse_valid_days_from_certificate |
| 151 |
# results and sort them |
| 152 |
|
| 153 |
local openssl_call |
| 154 |
local openssl_response |
| 155 |
# shellcheck disable=SC2086 |
| 156 |
openssl_call="s_client -servername $host -connect ${host}:${port} -showcerts $s_client_args"
|
| 157 |
# shellcheck disable=SC2086 |
| 158 |
openssl_response=$(echo "" | openssl ${openssl_call} 2>/dev/null)
|
| 159 |
if echo "$openssl_response" | grep -qi "Hostname mismatch"; then |
| 160 |
echo "<>" |
| 161 |
else |
| 162 |
echo "$openssl_response" | \ |
| 163 |
awk '{
|
| 164 |
if ($0 == "-----BEGIN CERTIFICATE-----") cert="" |
| 165 |
else if ($0 == "-----END CERTIFICATE-----") print cert |
| 166 |
else cert=cert$0 |
| 167 |
}' | \ |
| 168 |
while read -r CERT; do |
| 169 |
(printf '\n-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "$CERT") | \ |
| 170 |
parse_valid_days_from_certificate |
| 171 |
done | sort -n | head -n 1 |
| 172 |
fi |
| 173 |
} |
| 174 |
|
| 175 |
my_clean_fieldname() {
|
| 176 |
# if a domain starts with a digit, or its an IP address, prepend '_' |
| 177 |
clean_fieldname "$(echo "$@" | sed -E 's/^([0-9])/_\1/')" |
| 178 |
} |
| 179 |
|
| 180 |
main() {
|
| 181 |
for service in $services; do |
| 182 |
if echo "$service" | grep -q "_"; then |
| 183 |
host=$(echo "$service" | cut -f 1 -d "_") |
| 184 |
port=$(echo "$service" | cut -f 2 -d "_") |
| 185 |
starttls=$(echo "$service" | cut -f 3 -d "_") |
| 186 |
else |
| 187 |
host=$service |
| 188 |
port=443 |
| 189 |
starttls="" |
| 190 |
fi |
| 191 |
fieldname="$(my_clean_fieldname "$service")" |
| 192 |
valid_days=$(print_expire_days "$host" "$port" "$starttls") |
| 193 |
extinfo="" |
| 194 |
[ -z "$valid_days" ] && valid_days="U" |
| 195 |
if [ "$valid_days" = "<>" ]; then |
| 196 |
extinfo="Error: hostname mismatch, " |
| 197 |
valid_days="-1" |
| 198 |
fi |
| 199 |
printf "%s.value %s\\n" "$fieldname" "$valid_days" |
| 200 |
echo "${fieldname}.extinfo ${extinfo}Last checked: $(date)"
|
| 201 |
done |
| 202 |
} |
| 203 |
|
| 204 |
case ${1:-} in
|
| 205 |
config) |
| 206 |
echo "graph_title SSL Certificates Expiration" |
| 207 |
echo 'graph_args --base 1000' |
| 208 |
echo 'graph_vlabel days left' |
| 209 |
echo 'graph_category security' |
| 210 |
echo "graph_info This graph shows the numbers of days before certificate expiry" |
| 211 |
for service in $services; do |
| 212 |
fieldname=$(my_clean_fieldname "$service") |
| 213 |
echo "${fieldname}.label $(echo "${service}" | sed 's/_/:/')"
|
| 214 |
print_thresholds "${fieldname}" warning critical
|
| 215 |
done |
| 216 |
|
| 217 |
exit 0 |
| 218 |
;; |
| 219 |
cron) |
| 220 |
UPDATE="$(main)" |
| 221 |
echo "${UPDATE}" > "${CACHEFILE}"
|
| 222 |
chmod 0644 "${CACHEFILE}"
|
| 223 |
|
| 224 |
exit 0 |
| 225 |
;; |
| 226 |
esac |
| 227 |
|
| 228 |
if [ -n "$(find "${CACHEFILE}" -mmin -60 2>/dev/null)" ]; then
|
| 229 |
cat "${CACHEFILE}"
|
| 230 |
exit 0 |
| 231 |
fi |
| 232 |
|
| 233 |
main |
