From f8c8c360f1957a39ce98df61752abbfa1df9864b Mon Sep 17 00:00:00 2001 From: Martin Basti Date: Fri, 24 Apr 2015 13:37:07 +0200 Subject: DNSSEC: validate forward zone forwarders Show warning messages if DNSSEC validation is failing for particular FW zone or if the specified forwarders do not work https://fedorahosted.org/freeipa/ticket/4657 Reviewed-By: David Kupka Reviewed-By: Petr Spacek --- ipalib/messages.py | 12 ++++++ ipalib/plugins/dns.py | 113 +++++++++++++++++++++++++++++++++++++++++++++++++- ipalib/util.py | 60 ++++++++++++++++++++++++++- 3 files changed, 182 insertions(+), 3 deletions(-) (limited to 'ipalib') diff --git a/ipalib/messages.py b/ipalib/messages.py index 236b683b3..84f0a722d 100644 --- a/ipalib/messages.py +++ b/ipalib/messages.py @@ -229,6 +229,18 @@ class DNSServerDoesNotSupportEDNS0Warning(PublicMessage): u"please disable it.") +class DNSSECValidationFailingWarning(PublicMessage): + """ + **13010** Used when a DNSSEC validation failed on IPA DNS server + """ + + errno = 13010 + type = "warning" + format = _(u"DNSSEC validation failed: %(error)s.\n" + u"Please verify your DNSSEC signatures or disable DNSSEC " + u"validation on all IPA servers.") + + def iter_messages(variables, base): """Return a tuple with all subclasses """ diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py index c9dc1e547..f47aa7494 100644 --- a/ipalib/plugins/dns.py +++ b/ipalib/plugins/dns.py @@ -26,6 +26,7 @@ import re import binascii import dns.name import dns.exception +import dns.rdatatype import dns.resolver import encodings.idna @@ -45,7 +46,9 @@ from ipalib.util import (normalize_zonemgr, get_reverse_zone_default, REVERSE_DNS_ZONES, normalize_zone, validate_dnssec_global_forwarder, DNSSECSignatureMissingError, UnresolvableRecordError, - EDNS0UnsupportedError) + EDNS0UnsupportedError, DNSSECValidationError, + validate_dnssec_zone_forwarder_step1, + validate_dnssec_zone_forwarder_step2) from ipapython.ipautil import CheckedIPAddress, is_host_resolvable from ipapython.dnsutil import DNSName @@ -4340,11 +4343,100 @@ class dnsforwardzone(DNSZoneBase): _add_warning_fw_zone_is_not_effective(result, fwzone, options['version']) + def _warning_if_forwarders_do_not_work(self, result, new_zone, + *keys, **options): + fwzone = keys[-1] + forwarders = options.get('idnsforwarders', []) + any_forwarder_work = False + + for forwarder in forwarders: + try: + validate_dnssec_zone_forwarder_step1(forwarder, fwzone, + log=self.log) + except UnresolvableRecordError as e: + messages.add_message( + options['version'], + result, messages.DNSServerValidationWarning( + server=forwarder, error=e + ) + ) + except EDNS0UnsupportedError as e: + messages.add_message( + options['version'], + result, messages.DNSServerDoesNotSupportEDNS0Warning( + server=forwarder, error=e + ) + ) + else: + any_forwarder_work = True + + if not any_forwarder_work: + # do not test DNSSEC validation if there is no valid forwarder + return + + # resolve IP address of any DNS replica + # FIXME: https://fedorahosted.org/bind-dyndb-ldap/ticket/143 + # we currenly should to test all IPA DNS replica, because DNSSEC + # validation is configured just in named.conf per replica + + ipa_dns_masters = [normalize_zone(x) for x in + api.Object.dnsrecord.get_dns_masters()] + + if not ipa_dns_masters: + # something very bad happened, DNS is installed, but no IPA DNS + # servers available + self.log.error("No IPA DNS server can be found, but integrated DNS " + "is installed") + return + + ipa_dns_ip = None + for rdtype in (dns.rdatatype.A, dns.rdatatype.AAAA): + try: + ans = dns.resolver.query(ipa_dns_masters[0], rdtype) + except dns.exception.DNSException: + continue + else: + ipa_dns_ip = str(ans.rrset.items[0]) + break + + if not ipa_dns_ip: + self.log.error("Cannot resolve %s hostname", ipa_dns_masters[0]) + return + + # sleep a bit, adding new zone to BIND from LDAP may take a while + if new_zone: + time.sleep(5) + + # Test if IPA is able to receive replies from forwarders + try: + validate_dnssec_zone_forwarder_step2(ipa_dns_ip, fwzone, + log=self.log) + except DNSSECValidationError as e: + messages.add_message( + options['version'], + result, messages.DNSSECValidationFailingWarning(error=e) + ) + except UnresolvableRecordError as e: + messages.add_message( + options['version'], + result, messages.DNSServerValidationWarning( + server=ipa_dns_ip, error=e + ) + ) @register() class dnsforwardzone_add(DNSZoneBase_add): __doc__ = _('Create new DNS forward zone.') + def interactive_prompt_callback(self, kw): + # show informative message on client side + # server cannot send messages asynchronous + if kw.get('idnsforwarders', False): + self.Backend.textui.print_plain( + _("Server will check DNS forwarder(s).")) + self.Backend.textui.print_plain( + _("This may take some time, please wait ...")) + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): assert isinstance(dn, DN) @@ -4364,6 +4456,10 @@ class dnsforwardzone_add(DNSZoneBase_add): def execute(self, *keys, **options): result = super(dnsforwardzone_add, self).execute(*keys, **options) self.obj._warning_fw_zone_is_not_effective(result, *keys, **options) + if options.get('idnsforwarders'): + print result, keys, options + self.obj._warning_if_forwarders_do_not_work( + result, True, *keys, **options) return result @@ -4378,6 +4474,15 @@ class dnsforwardzone_del(DNSZoneBase_del): class dnsforwardzone_mod(DNSZoneBase_mod): __doc__ = _('Modify DNS forward zone.') + def interactive_prompt_callback(self, kw): + # show informative message on client side + # server cannot send messages asynchronous + if kw.get('idnsforwarders', False): + self.Backend.textui.print_plain( + _("Server will check DNS forwarder(s).")) + self.Backend.textui.print_plain( + _("This may take some time, please wait ...")) + def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options): try: entry = ldap.get_entry(dn) @@ -4406,6 +4511,12 @@ class dnsforwardzone_mod(DNSZoneBase_mod): return dn + def execute(self, *keys, **options): + result = super(dnsforwardzone_mod, self).execute(*keys, **options) + if options.get('idnsforwarders'): + self.obj._warning_if_forwarders_do_not_work(result, False, *keys, + **options) + return result @register() class dnsforwardzone_find(DNSZoneBase_find): diff --git a/ipalib/util.py b/ipalib/util.py index 8d7b66638..5810c774a 100644 --- a/ipalib/util.py +++ b/ipalib/util.py @@ -34,6 +34,7 @@ from types import NoneType from weakref import WeakKeyDictionary from dns import resolver, rdatatype from dns.exception import DNSException +from dns.resolver import NXDOMAIN from netaddr.core import AddrFormatError from ipalib import errors, messages @@ -580,6 +581,11 @@ class DNSSECSignatureMissingError(ForwarderValidationError): "signatures (no RRSIG data)") +class DNSSECValidationError(ForwarderValidationError): + format = _("requested record '%(owner)s %(rtype)s' was refused by IPA " + "server %(ip)s because DNSSEC signature is not valid") + + def _log_response(log, e): """ If exception contains response from server, log this response to debug log @@ -594,11 +600,12 @@ def _log_response(log, e): def _resolve_record(owner, rtype, nameserver_ip=None, edns0=False, - dnssec=False, timeout=10): + dnssec=False, flag_cd=False, timeout=10): """ :param nameserver_ip: if None, default resolvers will be used :param edns0: enables EDNS0 :param dnssec: enabled EDNS0, flags: DO + :param flag_cd: requires dnssec=True, adds flag CD :raise DNSException: if error occurs """ assert isinstance(nameserver_ip, basestring) @@ -615,7 +622,10 @@ def _resolve_record(owner, rtype, nameserver_ip=None, edns0=False, if dnssec: res.use_edns(0, dns.flags.DO, 4096) - res.set_flags(dns.flags.RD) + flags = dns.flags.RD + if flag_cd: + flags = flags | dns.flags.CD + res.set_flags(flags) elif edns0: res.use_edns(0, 0, 4096) @@ -680,6 +690,52 @@ def validate_dnssec_global_forwarder(ip_addr, log=None, timeout=10): raise DNSSECSignatureMissingError(owner=owner, rtype=rtype, ip=ip_addr) +def validate_dnssec_zone_forwarder_step1(ip_addr, fwzone, log=None, timeout=10): + """ + Only forwarders in forward zones can be validated in this way + :raise UnresolvableRecordError: record cannot be resolved + :raise EDNS0UnsupportedError: ENDS0 is not supported by forwarder + """ + _validate_edns0_forwarder(fwzone, "SOA", ip_addr, log=log, timeout=timeout) + + +def validate_dnssec_zone_forwarder_step2(ipa_ip_addr, fwzone, log=None, + timeout=10): + """ + This step must be executed after forwarders is added into LDAP, and only + when we are sure the forwarders work. + Query will be send to IPA DNS server, to verify if reply passed, + or DNSSEC validation failed. + Only forwarders in forward zones can be validated in this way + :raise UnresolvableRecordError: record cannot be resolved + :raise DNSSECValidationError: response from forwarder is not DNSSEC valid + """ + rtype = "SOA" + try: + _resolve_record(fwzone, rtype, nameserver_ip=ipa_ip_addr, edns0=True, + timeout=timeout) + except DNSException as e: + _log_response(log, e) + else: + return + + try: + _resolve_record(fwzone, rtype, nameserver_ip=ipa_ip_addr, dnssec=True, + flag_cd=True, timeout=timeout) + except NXDOMAIN as e: + # sometimes CD flag is ignored and NXDomain is returned + # this may cause false positive detection + _log_response(log, e) + raise DNSSECValidationError(owner=fwzone, rtype=rtype, ip=ipa_ip_addr) + except DNSException as e: + _log_response(log, e) + raise UnresolvableRecordError(owner=fwzone, rtype=rtype, ip=ipa_ip_addr, + error=e) + else: + # record is not DNSSEC valid, because it can be received with CD flag + # only + raise DNSSECValidationError(owner=fwzone, rtype=rtype, ip=ipa_ip_addr) + def validate_idna_domain(value): """ -- cgit