3 # downloaded from http://dns.measurement-factory.com/tools/nagios-plugins/check_zone_rrsig_expiration.html
4 # on 2010-02-07 by Peter Palfrader
6 # $Id: check_zone_rrsig_expiration,v 1.7 2008/11/25 01:36:36 wessels Exp $
8 # check_zone_rrsig_expiration
10 # nagios plugin to check expiration times of RRSIG records. Reminds
11 # you if its time to re-sign your zone.
13 # Copyright (c) 2008, The Measurement Factory, Inc. All rights reserved.
15 # Redistribution and use in source and binary forms, with or without
16 # modification, are permitted provided that the following conditions
19 # Redistributions of source code must retain the above copyright
20 # notice, this list of conditions and the following disclaimer.
21 # Redistributions in binary form must reproduce the above copyright
22 # notice, this list of conditions and the following disclaimer in the
23 # documentation and/or other materials provided with the distribution.
24 # Neither the name of The Measurement Factory nor the names of its
25 # contributors may be used to endorse or promote products derived
26 # from this software without specific prior written permission.
28 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
29 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
30 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
31 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
32 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
33 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
34 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
35 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
36 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
37 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
38 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
39 # POSSIBILITY OF SUCH DAMAGE.
41 # Copyright (c) 2010 Peter Palfrader <peter@palfrader.org>
42 # - various fixes and cleanups
43 # - do more than one zone
44 # Copyright (c) 2012 Peter Palfrader <peter@palfrader.org>
45 # - add -s option to configure udp packet size. default changed from 4k to 1k
46 # Copyright (c) 2013 Peter Palfrader <peter@palfrader.org>
47 # - add -r option to override initial refs.
48 # Copyright (c) 2014 Peter Palfrader <peter@palfrader.org>
49 # - Do not ask for RRSIG directly, instead ask for SOA with dnssec data
55 # command_name check-zone-rrsig
56 # command_line /usr/local/libexec/nagios-local/check_zone_rrsig -Z $HOSTADDRESS$
60 # name dns-rrsig-service
61 # check_command check-zone-rrsig
67 # host_name zone.example.com
68 # alias ZONE example.com
72 # use dns-rrsig-service
73 # host_name zone.example.com
80 use Net::DNS::Resolver;
81 use Time::HiRes qw ( gettimeofday tv_interval);
83 use List::Util qw ( shuffle );
87 my ($ticks, $unit) = ($in =~ /^(\d+)([smhdw]?)$/);
89 if ($unit eq 's' || $unit eq '') { }
90 elsif ($unit eq 'm') { $ticks *= 60; }
91 elsif ($unit eq 'h') { $ticks *= 60*60; }
92 elsif ($unit eq 'd') { $ticks *= 60*60*24; }
93 elsif ($unit eq 'w') { $ticks *= 60*60*24*7; }
94 else { die "Invalid unit '$unit' in '$in'\n" }
98 my %opts = (t=>30, s=>1024);
99 getopts('hdt:c:w:s:r:', \%opts);
100 usage() unless scalar @ARGV == 1;
107 my $CRIT = 3 * 3600*24;
108 my $WARN = 7 * 3600*24;
110 $CRIT = convert_time($opts{c}) if defined $opts{c};
111 $WARN = convert_time($opts{w}) if defined $opts{w};
128 @refs = split(/\s*,\s*/, $opts{r}) if (defined $opts{r});
130 $start = [gettimeofday()];
133 $stop = [gettimeofday()];
138 my $res = Net::DNS::Resolver->new;
140 print STDERR "\nRECURSE\n" if $opts{d};
142 my $prettyrefs = (scalar @refs) ? join(", ", @refs) : "empty set(!?)";
143 foreach my $ns (shuffle @refs) {
144 print STDERR "sending query for $zone NS to $ns\n" if $opts{d};
145 $res->nameserver($ns);
146 $res->udp_timeout($opts{t});
147 $res->udppacketsize($opts{s});
148 $pkt = $res->send($zone, 'NS');
151 critical("No response to seed query for $zone from $prettyrefs.") unless $pkt;
152 critical($pkt->header->rcode . " from " . $pkt->answerfrom)
153 unless ($pkt->header->rcode eq 'NOERROR');
155 foreach my $rr ($pkt->authority, $pkt->answer) {
156 print STDERR $rr->string, "\n" if $opts{d};
157 push (@refs, $rr->nsdname) if $rr->type eq 'NS';
158 next unless lc($rr->name) eq lc($zone);
159 add_nslist_to_data($pkt);
160 #print STDERR "Adding for $zone: ", $pkt->string, "\n" if $opts{d};
163 critical("No new references after querying for $zone NS from $prettyrefs. Packet was ".$pkt->string) unless (scalar @refs);
172 foreach my $ns (keys %$data) {
173 next if $data->{$ns}->{done};
174 print STDERR "\nQUERY \@$ns SOA $zone\n" if $opts{d};
176 my $pkt = send_query($zone, 'SOA', $ns);
177 add_nslist_to_data($pkt);
178 $data->{$ns}->{queries}->{SOA} = $pkt;
180 print STDERR "done with $ns\n" if $opts{d};
181 $data->{$ns}->{done} = 1;
191 foreach my $ns (keys %$data) {
192 print STDERR "\nANALYZE $ns\n" if $opts{d};
193 my $pkt = $data->{$ns}->{queries}->{SOA};
194 critical("No response from $ns") unless $pkt;
195 print STDERR $pkt->string if $opts{d};
196 critical($pkt->header->rcode . " from $ns")
197 unless ($pkt->header->rcode eq 'NOERROR');
198 critical("$ns is lame") unless $pkt->header->ancount;
199 foreach my $rr ($pkt->answer) {
200 next unless $rr->type eq 'RRSIG';
201 my $exp = sigrr_exp_epoch($rr);
202 my $T = $rr->typecovered;
203 if (!defined($MAX_EXP_BY_TYPE{$T}->{exp}) || $exp > $MAX_EXP_BY_TYPE{$T}->{exp}) {
204 $MAX_EXP_BY_TYPE{$T}->{exp} = $exp;
205 $MAX_EXP_BY_TYPE{$T}->{ns} = $ns;
210 warning("No nameservers found. Is '$zone' a zone?") if ($nscount < 1);
211 warning("No RRSIGs found") unless %MAX_EXP_BY_TYPE;
214 my $min_type = undef;
215 foreach my $T (keys %MAX_EXP_BY_TYPE) {
216 printf STDERR ("%s RRSIG expires in %.1f days\n", $T, ($MAX_EXP_BY_TYPE{$T}->{exp}-$NOW)/86400) if $opts{d};
217 if (!defined($min_exp) || $MAX_EXP_BY_TYPE{$T}->{exp} < $min_exp) {
218 $min_exp = $MAX_EXP_BY_TYPE{$T}->{exp};
219 $min_ns = $MAX_EXP_BY_TYPE{$T}->{ns};
223 critical("$min_ns has expired RRSIGs") if ($min_exp < $NOW);
224 if ($min_exp - $NOW < ($CRIT)) {
225 my $ND = sprintf "%3.1f days", ($min_exp-$NOW)/86400;
226 critical("$min_type RRSIG expires in $ND at $min_ns")
228 if ($min_exp - $NOW < ($WARN)) {
229 my $ND = sprintf "%3.1f days", ($min_exp-$NOW)/86400;
230 warning("$min_type RRSIG expires in $ND at $min_ns")
232 success(sprintf("No RRSIGs at zone apex expiring in the next %3.1f days", $WARN/86400));
235 sub sigrr_exp_epoch {
237 die unless $rr->type eq 'RRSIG';
238 my $exp = $rr->sigexpiration;
239 die "bad exp time '$exp'"
240 unless $exp =~ /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/;
241 my $exp_epoch = timegm($6,$5,$4,$3,$2-1,$1);
245 sub add_nslist_to_data {
247 foreach my $ns (get_nslist($pkt)) {
248 next if defined $data->{$ns}->{done};
249 print STDERR "adding NS $ns\n" if $opts{d};
250 $data->{$ns}->{done} |= 0;
260 output('WARNING', shift);
265 output('CRITICAL', shift);
272 $stop = [gettimeofday()] unless $stop;
273 my $latency = tv_interval($start, $stop);
274 printf "ZONE %s: %s; (%.2fs) |time=%.6fs;;;0.000000\n",
282 print STDERR "usage: $0 [-d] [-w=<warn>] [-c=<crit>] [-t=<timeout>] [-r=<initialns1>[,<initialns2>[,..]]] [-s=<packet-size>] <zone>\n";
290 my $res = Net::DNS::Resolver->new;
291 $res->nameserver($server) if $server;
292 $res->udp_timeout($opts{t});
295 $res->udppacketsize($opts{s});
296 my $pkt = $res->send($qname, $qtype);
299 $res->tcp_timeout($opts{t});
300 $pkt = $res->send($qname, $qtype);
307 return () unless $pkt;
308 return () if (!$pkt->authority && !$pkt->answer);
310 foreach my $rr ($pkt->authority, $pkt->answer) {
311 next unless ($rr->type eq 'NS');
312 next unless lc($rr->name) eq lc($zone);
313 push(@nslist, lc($rr->nsdname));