DNSSEC: Improve global forwarders validation

Validation now provides more detailed information and less false
positives failures.

https://fedorahosted.org/freeipa/ticket/4657

Reviewed-By: David Kupka <dkupka@redhat.com>
Reviewed-By: Petr Spacek <pspacek@redhat.com>
This commit is contained in:
Martin Basti
2015-06-11 13:12:31 +02:00
committed by Petr Vobornik
parent c9cbb1493a
commit 9aa6124b39
5 changed files with 188 additions and 65 deletions
+19 -4
View File
@@ -179,14 +179,14 @@ class OptionSemanticChangedWarning(PublicMessage):
u"%(hint)s") u"%(hint)s")
class DNSServerNotRespondingWarning(PublicMessage): class DNSServerValidationWarning(PublicMessage):
""" """
**13006** Used when a DNS server is not responding to queries **13006** Used when a DNS server is not to able to resolve query
""" """
errno = 13006 errno = 13006
type = "warning" type = "warning"
format = _(u"DNS server %(server)s not responding.") format = _(u"DNS server %(server)s: %(error)s.")
class DNSServerDoesNotSupportDNSSECWarning(PublicMessage): class DNSServerDoesNotSupportDNSSECWarning(PublicMessage):
@@ -196,10 +196,11 @@ class DNSServerDoesNotSupportDNSSECWarning(PublicMessage):
errno = 13007 errno = 13007
type = "warning" type = "warning"
format = _(u"DNS server %(server)s does not support DNSSEC. " format = _(u"DNS server %(server)s does not support DNSSEC: %(error)s.\n"
u"If DNSSEC validation is enabled on IPA server(s), " u"If DNSSEC validation is enabled on IPA server(s), "
u"please disable it.") u"please disable it.")
class ForwardzoneIsNotEffectiveWarning(PublicMessage): class ForwardzoneIsNotEffectiveWarning(PublicMessage):
""" """
**13008** Forwardzone is not effective, forwarding will not work because **13008** Forwardzone is not effective, forwarding will not work because
@@ -214,6 +215,20 @@ class ForwardzoneIsNotEffectiveWarning(PublicMessage):
u"\"%(ns_rec)s\" to parent zone \"%(authzone)s\".") u"\"%(ns_rec)s\" to parent zone \"%(authzone)s\".")
class DNSServerDoesNotSupportEDNS0Warning(PublicMessage):
"""
**13009** Used when a DNS server does not support EDNS0, required for
DNSSEC support
"""
errno = 13009
type = "warning"
format = _(u"DNS server %(server)s does not support EDNS0 (RFC 6891): "
u"%(error)s.\n"
u"If DNSSEC validation is enabled on IPA server(s), "
u"please disable it.")
def iter_messages(variables, base): def iter_messages(variables, base):
"""Return a tuple with all subclasses """Return a tuple with all subclasses
""" """
+36 -27
View File
@@ -43,7 +43,10 @@ from ipalib.util import (normalize_zonemgr,
get_dns_forward_zone_update_policy, get_dns_forward_zone_update_policy,
get_dns_reverse_zone_update_policy, get_dns_reverse_zone_update_policy,
get_reverse_zone_default, REVERSE_DNS_ZONES, get_reverse_zone_default, REVERSE_DNS_ZONES,
normalize_zone, validate_dnssec_forwarder) normalize_zone, validate_dnssec_global_forwarder,
DNSSECSignatureMissingError, UnresolvableRecordError,
EDNS0UnsupportedError)
from ipapython.ipautil import CheckedIPAddress, is_host_resolvable from ipapython.ipautil import CheckedIPAddress, is_host_resolvable
from ipapython.dnsutil import DNSName from ipapython.dnsutil import DNSName
@@ -4261,41 +4264,47 @@ class dnsconfig_mod(LDAPUpdate):
__doc__ = _('Modify global DNS configuration.') __doc__ = _('Modify global DNS configuration.')
def interactive_prompt_callback(self, kw): def interactive_prompt_callback(self, kw):
# show informative message on client side
# server cannot send messages asynchronous
if kw.get('idnsforwarders', False): if kw.get('idnsforwarders', False):
self.Backend.textui.print_plain("Server will check forwarder(s).") self.Backend.textui.print_plain(
self.Backend.textui.print_plain("This may take some time, please wait ...") _("Server will check DNS forwarder(s)."))
self.Backend.textui.print_plain(
_("This may take some time, please wait ..."))
def execute(self, *keys, **options): def execute(self, *keys, **options):
# test dnssec forwarders # test dnssec forwarders
non_dnssec_forwarders = []
not_responding_forwarders = []
forwarders = options.get('idnsforwarders') forwarders = options.get('idnsforwarders')
if forwarders:
for forwarder in forwarders:
dnssec_status = validate_dnssec_forwarder(forwarder)
if dnssec_status is None:
not_responding_forwarders.append(forwarder)
elif dnssec_status is False:
non_dnssec_forwarders.append(forwarder)
result = super(dnsconfig_mod, self).execute(*keys, **options) result = super(dnsconfig_mod, self).execute(*keys, **options)
self.obj.postprocess_result(result) self.obj.postprocess_result(result)
# add messages if forwarders:
for forwarder in not_responding_forwarders: for forwarder in forwarders:
messages.add_message( try:
options['version'], validate_dnssec_global_forwarder(forwarder, log=self.log)
result, messages.DNSServerNotRespondingWarning( except DNSSECSignatureMissingError as e:
server=forwarder, messages.add_message(
) options['version'],
) result, messages.DNSServerDoesNotSupportDNSSECWarning(
for forwarder in non_dnssec_forwarders: server=forwarder, error=e,
messages.add_message( )
options['version'], )
result, messages.DNSServerDoesNotSupportDNSSECWarning( except EDNS0UnsupportedError as e:
server=forwarder, messages.add_message(
) options['version'],
) result, messages.DNSServerDoesNotSupportEDNS0Warning(
server=forwarder, error=e,
)
)
except UnresolvableRecordError as e:
messages.add_message(
options['version'],
result, messages.DNSServerValidationWarning(
server=forwarder, error=e
)
)
return result return result
+109 -21
View File
@@ -36,7 +36,7 @@ from dns import resolver, rdatatype
from dns.exception import DNSException from dns.exception import DNSException
from netaddr.core import AddrFormatError from netaddr.core import AddrFormatError
from ipalib import errors from ipalib import errors, messages
from ipalib.text import _ from ipalib.text import _
from ipapython.ssh import SSHPublicKey from ipapython.ssh import SSHPublicKey
from ipapython.dn import DN, RDN from ipapython.dn import DN, RDN
@@ -559,38 +559,126 @@ def validate_hostmask(ugettext, hostmask):
return _('invalid hostmask') return _('invalid hostmask')
def validate_dnssec_forwarder(ip_addr): class ForwarderValidationError(Exception):
"""Test DNS forwarder properties. format = None
:returns: def __init__(self, format=None, message=None, **kw):
True if forwarder works as expected and supports DNSSEC. messages.process_message_arguments(self, format, message, **kw)
False if forwarder does not support DNSSEC. super(ForwarderValidationError, self).__init__(self.msg)
None if forwarder does not respond.
class UnresolvableRecordError(ForwarderValidationError):
format = _("query '%(owner)s %(rtype)s': %(error)s")
class EDNS0UnsupportedError(ForwarderValidationError):
format = _("query '%(owner)s %(rtype)s' with EDNS0: %(error)s")
class DNSSECSignatureMissingError(ForwarderValidationError):
format = _("answer to query '%(owner)s %(rtype)s' is missing DNSSEC "
"signatures (no RRSIG data)")
def _log_response(log, e):
""" """
ip_addr = str(ip_addr) If exception contains response from server, log this response to debug log
:param log: if log is None, do not log
:param e: DNSException
"""
assert isinstance(e, DNSException)
if log is not None:
response = getattr(e, 'kwargs', {}).get('response')
if response:
log.debug("DNSException: %s; server response: %s", e, response)
def _resolve_record(owner, rtype, nameserver_ip=None, edns0=False,
dnssec=False, timeout=10):
"""
:param nameserver_ip: if None, default resolvers will be used
:param edns0: enables EDNS0
:param dnssec: enabled EDNS0, flags: DO
:raise DNSException: if error occurs
"""
assert isinstance(nameserver_ip, basestring)
assert isinstance(rtype, basestring)
res = dns.resolver.Resolver() res = dns.resolver.Resolver()
res.nameservers = [ip_addr] if nameserver_ip:
res.lifetime = 10 # wait max 10 seconds for reply res.nameservers = [nameserver_ip]
res.lifetime = timeout
# enable Authenticated Data + Checking Disabled flags # Recursion Desired,
res.set_flags(dns.flags.AD | dns.flags.CD) # this option prevents to get answers in authority section instead of answer
res.set_flags(dns.flags.RD)
# enable EDNS v0 + enable DNSSEC-Ok flag if dnssec:
res.use_edns(0, dns.flags.DO, 0) res.use_edns(0, dns.flags.DO, 4096)
res.set_flags(dns.flags.RD)
elif edns0:
res.use_edns(0, 0, 4096)
return res.query(owner, rtype)
def _validate_edns0_forwarder(owner, rtype, ip_addr, log=None, timeout=10):
"""
Validate if forwarder supports EDNS0
:raise UnresolvableRecordError: record cannot be resolved
:raise EDNS0UnsupportedError: EDNS0 is not supported by forwarder
"""
try:
_resolve_record(owner, rtype, nameserver_ip=ip_addr, timeout=timeout)
except DNSException as e:
_log_response(log, e)
raise UnresolvableRecordError(owner=owner, rtype=rtype, ip=ip_addr,
error=e)
try:
_resolve_record(owner, rtype, nameserver_ip=ip_addr, edns0=True,
timeout=timeout)
except DNSException as e:
_log_response(log, e)
raise EDNS0UnsupportedError(owner=owner, rtype=rtype, ip=ip_addr,
error=e)
def validate_dnssec_global_forwarder(ip_addr, log=None, timeout=10):
"""Test DNS forwarder properties. against root zone.
Global forwarders should be able return signed root zone
:raise UnresolvableRecordError: record cannot be resolved
:raise EDNS0UnsupportedError: EDNS0 is not supported by forwarder
:raise DNSSECSignatureMissingError: did not receive RRSIG for root zone
"""
ip_addr = str(ip_addr)
owner = "."
rtype = "SOA"
_validate_edns0_forwarder(owner, rtype, ip_addr, log=log, timeout=timeout)
# DNS root has to be signed # DNS root has to be signed
try: try:
ans = res.query('.', 'NS') ans = _resolve_record(owner, rtype, nameserver_ip=ip_addr, dnssec=True,
except DNSException: timeout=timeout)
return None except DNSException as e:
_log_response(log, e)
raise UnresolvableRecordError(owner=owner, rtype=rtype, ip=ip_addr,
error=e)
try: try:
ans.response.find_rrset(ans.response.answer, dns.name.root, ans.response.find_rrset(
dns.rdataclass.IN, dns.rdatatype.RRSIG, dns.rdatatype.NS) ans.response.answer, dns.name.root, dns.rdataclass.IN,
dns.rdatatype.RRSIG, dns.rdatatype.SOA
)
except KeyError: except KeyError:
return False raise DNSSECSignatureMissingError(owner=owner, rtype=rtype, ip=ip_addr)
return True
def validate_idna_domain(value): def validate_idna_domain(value):
+21 -11
View File
@@ -42,7 +42,8 @@ from ipaplatform.tasks import tasks
from ipalib.util import (validate_zonemgr_str, normalize_zonemgr, from ipalib.util import (validate_zonemgr_str, normalize_zonemgr,
get_dns_forward_zone_update_policy, get_dns_reverse_zone_update_policy, get_dns_forward_zone_update_policy, get_dns_reverse_zone_update_policy,
normalize_zone, get_reverse_zone_default, zone_is_reverse, normalize_zone, get_reverse_zone_default, zone_is_reverse,
validate_dnssec_forwarder) validate_dnssec_global_forwarder, DNSSECSignatureMissingError,
EDNS0UnsupportedError, UnresolvableRecordError)
from ipalib.constants import CACERT from ipalib.constants import CACERT
NAMED_CONF = paths.NAMED_CONF NAMED_CONF = paths.NAMED_CONF
@@ -463,23 +464,32 @@ def check_reverse_zones(ip_addresses, reverse_zones, options, unattended, search
return ret_reverse_zones return ret_reverse_zones
def check_forwarders(dns_forwarders, logger): def check_forwarders(dns_forwarders, logger):
print "Checking forwarders, please wait ..." print "Checking DNS forwarders, please wait ..."
forwarders_dnssec_valid = True forwarders_dnssec_valid = True
for forwarder in dns_forwarders: for forwarder in dns_forwarders:
logger.debug("Checking forwarder: %s", forwarder) logger.debug("Checking DNS server: %s", forwarder)
result = validate_dnssec_forwarder(forwarder) try:
if result is None: validate_dnssec_global_forwarder(forwarder, log=logger)
logger.error("Forwarder %s does not work", forwarder) except DNSSECSignatureMissingError as e:
raise RuntimeError("Forwarder %s does not respond" % forwarder)
elif result is False:
forwarders_dnssec_valid = False forwarders_dnssec_valid = False
logger.warning("DNS forwarder %s does not return DNSSEC signatures in answers", forwarder) logger.warning("DNS server %s does not support DNSSEC: %s",
forwarder, e)
logger.warning("Please fix forwarder configuration to enable DNSSEC support.\n" logger.warning("Please fix forwarder configuration to enable DNSSEC support.\n"
"(For BIND 9 add directive \"dnssec-enable yes;\" to \"options {}\")") "(For BIND 9 add directive \"dnssec-enable yes;\" to \"options {}\")")
print ("WARNING: DNS forwarder %s does not return DNSSEC " print "DNS server %s: %s" % (forwarder, e)
"signatures in answers" % forwarder)
print "Please fix forwarder configuration to enable DNSSEC support." print "Please fix forwarder configuration to enable DNSSEC support."
print "(For BIND 9 add directive \"dnssec-enable yes;\" to \"options {}\")" print "(For BIND 9 add directive \"dnssec-enable yes;\" to \"options {}\")"
except EDNS0UnsupportedError as e:
forwarders_dnssec_valid = False
logger.warning("DNS server %s does not support ENDS0 "
"(RFC 6891): %s", forwarder, e)
logger.warning("Please fix forwarder configuration. "
"DNSSEC support cannot be enabled without EDNS0")
print ("WARNING: DNS server %s does not support EDNS0 "
"(RFC 6891): %s" % (forwarder, e))
except UnresolvableRecordError as e:
logger.error("DNS server %s: %s", forwarder, e)
raise RuntimeError("DNS server %s: %s" % (forwarder, e))
return forwarders_dnssec_valid return forwarders_dnssec_valid
+3 -2
View File
@@ -1751,10 +1751,11 @@ class test_dns(Declarative):
'value': None, 'value': None,
'summary': None, 'summary': None,
u'messages': ( u'messages': (
{u'message': u'DNS server 172.16.31.80 not responding.', {u'message': lambda x: x.startswith(
u"DNS server %s: query '. SOA':" % fwd_ip),
u'code': 13006, u'code': 13006,
u'type':u'warning', u'type':u'warning',
u'name': u'DNSServerNotRespondingWarning'}, u'name': u'DNSServerValidationWarning'},
), ),
'result': { 'result': {
'idnsforwarders': [fwd_ip], 'idnsforwarders': [fwd_ip],