#!/usr/bin/perl # downloaded from http://dns.measurement-factory.com/tools/nagios-plugins/check_zone_rrsig_expiration.html # on 2010-02-07 by Peter Palfrader # $Id: check_zone_rrsig_expiration,v 1.7 2008/11/25 01:36:36 wessels Exp $ # # check_zone_rrsig_expiration # # nagios plugin to check expiration times of RRSIG records. Reminds # you if its time to re-sign your zone. # Copyright (c) 2008, The Measurement Factory, Inc. All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions # are met: # # Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # Neither the name of The Measurement Factory nor the names of its # contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. # Copyright (c) 2010 Peter Palfrader # - various fixes and cleanups # - do more than one zone # Copyright (c) 2012 Peter Palfrader # - add -s option to configure udp packet size. default changed from 4k to 1k # Copyright (c) 2013 Peter Palfrader # - add -r option to override initial refs. # Copyright (c) 2014 Peter Palfrader # - Do not ask for RRSIG directly, instead ask for SOA with dnssec data # usage # # define command { # command_name check-zone-rrsig # command_line /usr/local/libexec/nagios-local/check_zone_rrsig -Z $HOSTADDRESS$ # } # # define service { # name dns-rrsig-service # check_command check-zone-rrsig # ... # } # # define host { # use dns-zone # host_name zone.example.com # alias ZONE example.com # } # # define service { # use dns-rrsig-service # host_name zone.example.com # } use warnings; use strict; use Getopt::Std; use Net::DNS::Resolver; use Time::HiRes qw ( gettimeofday tv_interval); use Time::Local; use List::Util qw ( shuffle ); sub convert_time { my $in = shift; my ($ticks, $unit) = ($in =~ /^(\d+)([smhdw]?)$/); if ($unit eq 's' || $unit eq '') { } elsif ($unit eq 'm') { $ticks *= 60; } elsif ($unit eq 'h') { $ticks *= 60*60; } elsif ($unit eq 'd') { $ticks *= 60*60*24; } elsif ($unit eq 'w') { $ticks *= 60*60*24*7; } else { die "Invalid unit '$unit' in '$in'\n" } return $ticks; } my %opts = (t=>30, s=>1024); getopts('hdt:c:w:s:r:', \%opts); usage() unless scalar @ARGV == 1; usage() if $opts{h}; my $zone = $ARGV[0]; my $data; my $start; my $stop; my $CRIT = 3 * 3600*24; my $WARN = 7 * 3600*24; $CRIT = convert_time($opts{c}) if defined $opts{c}; $WARN = convert_time($opts{w}) if defined $opts{w}; my @refs = qw ( a.root-servers.net b.root-servers.net c.root-servers.net d.root-servers.net e.root-servers.net f.root-servers.net g.root-servers.net h.root-servers.net i.root-servers.net j.root-servers.net k.root-servers.net l.root-servers.net m.root-servers.net ); @refs = split(/\s*,\s*/, $opts{r}) if (defined $opts{r}); $start = [gettimeofday()]; do_recursion(); do_queries(); $stop = [gettimeofday()]; do_analyze(); sub do_recursion { my $done = 0; my $res = Net::DNS::Resolver->new; do { print STDERR "\nRECURSE\n" if $opts{d}; my $pkt; my $prettyrefs = (scalar @refs) ? join(", ", @refs) : "empty set(!?)"; foreach my $ns (shuffle @refs) { print STDERR "sending query for $zone NS to $ns\n" if $opts{d}; $res->nameserver($ns); $res->udp_timeout($opts{t}); $res->udppacketsize($opts{s}); $pkt = $res->send($zone, 'NS'); last if $pkt; } critical("No response to seed query for $zone from $prettyrefs.") unless $pkt; critical($pkt->header->rcode . " from " . $pkt->answerfrom) unless ($pkt->header->rcode eq 'NOERROR'); @refs = (); foreach my $rr ($pkt->authority, $pkt->answer) { print STDERR $rr->string, "\n" if $opts{d}; push (@refs, $rr->nsdname) if $rr->type eq 'NS'; next unless lc($rr->name) eq lc($zone); add_nslist_to_data($pkt); #print STDERR "Adding for $zone: ", $pkt->string, "\n" if $opts{d}; $done = 1; } critical("No new references after querying for $zone NS from $prettyrefs. Packet was ".$pkt->string) unless (scalar @refs); } while (! $done); } sub do_queries { my $n; do { $n = 0; foreach my $ns (keys %$data) { next if $data->{$ns}->{done}; print STDERR "\nQUERY \@$ns SOA $zone\n" if $opts{d}; my $pkt = send_query($zone, 'SOA', $ns); add_nslist_to_data($pkt); $data->{$ns}->{queries}->{SOA} = $pkt; print STDERR "done with $ns\n" if $opts{d}; $data->{$ns}->{done} = 1; $n++; } } while ($n); } sub do_analyze { my $nscount = 0; my $NOW = time; my %MAX_EXP_BY_TYPE; foreach my $ns (keys %$data) { print STDERR "\nANALYZE $ns\n" if $opts{d}; my $pkt = $data->{$ns}->{queries}->{SOA}; critical("No response from $ns") unless $pkt; print STDERR $pkt->string if $opts{d}; critical($pkt->header->rcode . " from $ns") unless ($pkt->header->rcode eq 'NOERROR'); critical("$ns is lame") unless $pkt->header->ancount; foreach my $rr ($pkt->answer) { next unless $rr->type eq 'RRSIG'; my $exp = sigrr_exp_epoch($rr); my $T = $rr->typecovered; if (!defined($MAX_EXP_BY_TYPE{$T}->{exp}) || $exp > $MAX_EXP_BY_TYPE{$T}->{exp}) { $MAX_EXP_BY_TYPE{$T}->{exp} = $exp; $MAX_EXP_BY_TYPE{$T}->{ns} = $ns; } } $nscount++; } warning("No nameservers found. Is '$zone' a zone?") if ($nscount < 1); warning("No RRSIGs found") unless %MAX_EXP_BY_TYPE; my $min_exp = undef; my $min_ns = undef; my $min_type = undef; foreach my $T (keys %MAX_EXP_BY_TYPE) { printf STDERR ("%s RRSIG expires in %.1f days\n", $T, ($MAX_EXP_BY_TYPE{$T}->{exp}-$NOW)/86400) if $opts{d}; if (!defined($min_exp) || $MAX_EXP_BY_TYPE{$T}->{exp} < $min_exp) { $min_exp = $MAX_EXP_BY_TYPE{$T}->{exp}; $min_ns = $MAX_EXP_BY_TYPE{$T}->{ns}; $min_type = $T; } } critical("$min_ns has expired RRSIGs") if ($min_exp < $NOW); if ($min_exp - $NOW < ($CRIT)) { my $ND = sprintf "%3.1f days", ($min_exp-$NOW)/86400; critical("$min_type RRSIG expires in $ND at $min_ns") } if ($min_exp - $NOW < ($WARN)) { my $ND = sprintf "%3.1f days", ($min_exp-$NOW)/86400; warning("$min_type RRSIG expires in $ND at $min_ns") } success(sprintf("No RRSIGs at zone apex expiring in the next %3.1f days", $WARN/86400)); } sub sigrr_exp_epoch { my $rr = shift; die unless $rr->type eq 'RRSIG'; my $exp = $rr->sigexpiration; die "bad exp time '$exp'" unless $exp =~ /^(\d\d\d\d)(\d\d)(\d\d)(\d\d)(\d\d)(\d\d)$/; my $exp_epoch = timegm($6,$5,$4,$3,$2-1,$1); return $exp_epoch; } sub add_nslist_to_data { my $pkt = shift; foreach my $ns (get_nslist($pkt)) { next if defined $data->{$ns}->{done}; print STDERR "adding NS $ns\n" if $opts{d}; $data->{$ns}->{done} |= 0; } } sub success { output('OK', shift); exit(0); } sub warning { output('WARNING', shift); exit(1); } sub critical { output('CRITICAL', shift); exit(2); } sub output { my $state = shift; my $msg = shift; $stop = [gettimeofday()] unless $stop; my $latency = tv_interval($start, $stop); printf "ZONE %s: %s; (%.2fs) |time=%.6fs;;;0.000000\n", $state, $msg, $latency, $latency; } sub usage { print STDERR "usage: $0 [-d] [-w=] [-c=] [-t=] [-r=[,[,..]]] [-s=] \n"; exit 3; } sub send_query { my $qname = shift; my $qtype = shift; my $server = shift; my $res = Net::DNS::Resolver->new; $res->nameserver($server) if $server; $res->udp_timeout($opts{t}); $res->dnssec(1); $res->retry(2); $res->udppacketsize($opts{s}); my $pkt = $res->send($qname, $qtype); unless ($pkt) { $res->usevc(1); $res->tcp_timeout($opts{t}); $pkt = $res->send($qname, $qtype); } return $pkt; } sub get_nslist { my $pkt = shift; return () unless $pkt; return () if (!$pkt->authority && !$pkt->answer); my @nslist; foreach my $rr ($pkt->authority, $pkt->answer) { next unless ($rr->type eq 'NS'); next unless lc($rr->name) eq lc($zone); push(@nslist, lc($rr->nsdname)); } return @nslist; }