X-Git-Url: https://git.siccegge.de//index.cgi?p=dane-monitoring-plugins.git;a=blobdiff_plain;f=check_dnssec;fp=check_dnssec;h=1d308539704395b8a2c4012c483b5d41d4027b54;hp=0000000000000000000000000000000000000000;hb=3315d80a23c1087abd5b44f92ce086acd67a5d2c;hpb=8ec7e6188c656b073334b96f0b9136df529cea45 diff --git a/check_dnssec b/check_dnssec new file mode 100755 index 0000000..1d30853 --- /dev/null +++ b/check_dnssec @@ -0,0 +1,140 @@ +#!/usr/bin/python3 + +from __future__ import print_function + +import sys +import argparse +import logging +import hashlib +import codecs + +from unbound import RR_TYPE_SOA, RR_TYPE_DNSKEY, RR_TYPE_NS +from unbound import RR_TYPE_A, RR_TYPE_AAAA +from ldns import LDNS_SECTION_ANSWER +from ldns import ldns_wire2pkt + + +from check_dane.resolve import Resolver, ResolverException +from check_dane.resolve import format_address, dnssec_verify_rrsig_validity + + +def check_main_records(resolver, zone, args): + """Confirms that the necessary records on a zone all verify""" + retval = 0 + + for rrtype in [RR_TYPE_DNSKEY, RR_TYPE_NS, RR_TYPE_SOA]: + result = resolver.resolve(zone, rrtype=rrtype, secure=True) + nretval = dnssec_verify_rrsig_validity(result.packet, args.warndays, args.critdays) + retval = max(nretval, retval) + + return retval + + +def check_nsec_cycle(resolver, zone, args): + """Confirms that NSEC records are completely available""" + return 0 + + +def check_synced(resolver, zone, args): + """Makes sure the zone is at the same serial on all secondaries""" + try: + result = resolver.resolve(zone, RR_TYPE_NS, secure=True) + + if result.data is None: + logging.error("No nameservers found for zone %s", zone) + return 2 + + nameservers = result.data.as_domain_list() + nameserver_ips = [] + for nameserver in nameservers: + ips = [] + for rrtype in [RR_TYPE_AAAA, RR_TYPE_A]: + result = resolver.resolve(nameserver, rrtype=rrtype, secure=True) + if result.data is not None: + ips = ips + [format_address(data, rrtype) for data in result.data.data] + + if ips == []: + logging.warning("Could not find any address for nameserver %s", nameserver) + + nameserver_ips = nameserver_ips + ips + + if nameserver_ips == []: + logging.error("No authoritive nameserver for %s could be resolved", zone) + return 2 + + results = dict() + for ip in nameserver_ips: + newresolver = Resolver(args.ancor, ip) + + # We can't request secure here as the authoritative + # nameservers for the zone won't let us retrieve the + # signature chain below their own zone. We'll later + # recheck the SOA record using the main recursor + # + # Alternatively one could get the DS / DNSKEY for the zone with + # resolver and add it to newresolver as a hint. + result = newresolver.resolve(zone, rrtype=RR_TYPE_SOA) + + s, result = ldns_wire2pkt(result.packet) + if s != 0: + logging.error("Parsing packet failed with errorcode %d", s) + return 2 + + rrs = result.rr_list_by_type(RR_TYPE_SOA, LDNS_SECTION_ANSWER).rrs() + soa = next(rrs) + + serial = str(soa).split()[6] + results[serial] = results.get(serial, []) + [ip] + + if len(results) == 1: + return 0 + else: + logging.error("different SOAs: %s", results) + return 2 + + except ResolverException as e: + logging.error(e.message) + + +def main(): + logging.basicConfig(format='%(levelname)5s %(message)s') + parser = argparse.ArgumentParser() + parser.add_argument("Zone") + + parser.add_argument("--verbose", action="store_true") + parser.add_argument("--quiet", action="store_true") + + parser.add_argument("-a", "--ancor", + action="store", type=str, default="/etc/unbound/root.key", + help="DNSSEC root ancor") + + parser.add_argument("--nsec", action="store_false", + help="Verifies the complete NSEC/NSEC3 cycle (default: false)") + parser.add_argument("--warndays", type=int, default=-1, + help="Days before rrsig expiration to warn") + parser.add_argument("--critdays", type=int, default=-1, + help="Days before rrsig expiration to raise error") + + + args = parser.parse_args() + + if args.verbose: + logging.getLogger().setLevel(logging.DEBUG) + elif args.quiet: + logging.getLogger().setLevel(logging.WARNING) + else: + logging.getLogger().setLevel(logging.INFO) + + resolver = Resolver(args.ancor) + zone = args.Zone.encode('idna').decode() + + retval1 = check_synced(resolver, zone, args) + retval2 = check_main_records(resolver, zone, args) + if args.nsec: + retval3 = check_nsec_cycle(resolver, zone, args) + return max(retval1, retval2, retval3) + else: + return max(retval1, retval2) + +if __name__ == '__main__': + sys.exit(main())