mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2026-07-29 23:58:23 -05:00
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:
committed by
Petr Vobornik
parent
c9cbb1493a
commit
9aa6124b39
+19
-4
@@ -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
@@ -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
@@ -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):
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
Reference in New Issue
Block a user