Projet

Général

Profil

Paste
Télécharger au format
Statistiques
| Branche: | Révision:

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