summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMartin Basti <mbasti@redhat.com>2015-04-24 13:37:07 +0200
committerPetr Vobornik <pvoborni@redhat.com>2015-06-11 13:12:31 +0200
commitf8c8c360f1957a39ce98df61752abbfa1df9864b (patch)
tree51759e4c4ce6e59dd782dcd8fde72a3279f4954d
parent9aa6124b39267148c4c1b9a8ee4209fb859b9c42 (diff)
downloadfreeipa-f8c8c360f1957a39ce98df61752abbfa1df9864b.tar.gz
freeipa-f8c8c360f1957a39ce98df61752abbfa1df9864b.tar.xz
freeipa-f8c8c360f1957a39ce98df61752abbfa1df9864b.zip
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 <dkupka@redhat.com> Reviewed-By: Petr Spacek <pspacek@redhat.com>
-rw-r--r--ipalib/messages.py12
-rw-r--r--ipalib/plugins/dns.py113
-rw-r--r--ipalib/util.py60
-rw-r--r--ipatests/test_xmlrpc/test_dns_plugin.py20
4 files changed, 202 insertions, 3 deletions
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):
"""
diff --git a/ipatests/test_xmlrpc/test_dns_plugin.py b/ipatests/test_xmlrpc/test_dns_plugin.py
index 50f46bc52..e38ea424d 100644
--- a/ipatests/test_xmlrpc/test_dns_plugin.py
+++ b/ipatests/test_xmlrpc/test_dns_plugin.py
@@ -3375,6 +3375,14 @@ class test_forward_zones(Declarative):
expected={
'value': fwzone2_dnsname,
'summary': None,
+ u'messages': (
+ {u'message': lambda x: x.startswith(
+ u"DNS server %s: query '%s SOA':" %
+ (forwarder1, fwzone2)),
+ u'code': 13006,
+ u'type':u'warning',
+ u'name': u'DNSServerValidationWarning'},
+ ),
'result': {
'dn': fwzone2_dn,
'idnsname': [fwzone2_dnsname],
@@ -3409,6 +3417,7 @@ class test_forward_zones(Declarative):
expected={
'value': fwzone2_dnsname,
'summary': None,
+ 'messages': lambda x: True, # fake forwarders - ignore message
'result': {
'dn': fwzone2_dn,
'idnsname': [fwzone2_dnsname],
@@ -3442,6 +3451,7 @@ class test_forward_zones(Declarative):
expected={
'value': fwzone2_dnsname,
'summary': None,
+ 'messages': lambda x: True, # fake forwarders - ignore message
'result': {
'dn': fwzone2_dn,
'idnsname': [fwzone2_dnsname],
@@ -3465,6 +3475,7 @@ class test_forward_zones(Declarative):
expected={
'value': fwzone3_dnsname,
'summary': None,
+ 'messages': lambda x: True, # fake forwarders - ignore message
'result': {
'dn': fwzone3_dn,
'idnsname': [fwzone3_dnsname],
@@ -3498,6 +3509,7 @@ class test_forward_zones(Declarative):
expected={
'value': fwzone3_dnsname,
'summary': None,
+ 'messages': lambda x: True, # fake forwarders - ignore message
'result': {
'dn': fwzone3_dn,
'idnsname': [fwzone3_dnsname],
@@ -3521,6 +3533,7 @@ class test_forward_zones(Declarative):
expected={
'value': fwzone3_dnsname,
'summary': None,
+ 'messages': lambda x: True, # fake forwarders - ignore message
'result': {
'idnsname': [fwzone3_dnsname],
'idnszoneactive': [u'TRUE'],
@@ -3541,6 +3554,7 @@ class test_forward_zones(Declarative):
expected={
'value': fwzone3_dnsname,
'summary': None,
+ 'messages': lambda x: True, # fake forwarders - ignore message
'result': {
'idnsname': [fwzone3_dnsname],
'idnszoneactive': [u'TRUE'],
@@ -3561,6 +3575,7 @@ class test_forward_zones(Declarative):
expected={
'value': fwzone3_dnsname,
'summary': None,
+ 'messages': lambda x: True, # fake forwarders - ignore message
'result': {
'idnsname': [fwzone3_dnsname],
'idnszoneactive': [u'TRUE'],
@@ -3581,6 +3596,7 @@ class test_forward_zones(Declarative):
expected={
'value': fwzone3_dnsname,
'summary': None,
+ 'messages': lambda x: True, # fake forwarders - ignore message
'result': {
'idnsname': [fwzone3_dnsname],
'idnszoneactive': [u'TRUE'],
@@ -3602,6 +3618,7 @@ class test_forward_zones(Declarative):
expected={
'value': fwzone1_dnsname,
'summary': None,
+ 'messages': lambda x: True, # fake forwarders - ignore message
'result': {
'idnsname': [fwzone1_dnsname],
'idnszoneactive': [u'TRUE'],
@@ -3663,6 +3680,7 @@ class test_forward_zones(Declarative):
expected={
'value': fwzone1_dnsname,
'summary': None,
+ 'messages': lambda x: True, # fake forwarders - ignore message
'result': {
'idnsname': [fwzone1_dnsname],
'idnszoneactive': [u'TRUE'],
@@ -3704,6 +3722,7 @@ class test_forward_zones(Declarative):
expected={
'value': fwzone1_dnsname,
'summary': None,
+ 'messages': lambda x: True, # fake forwarders - ignore message
'result': {
'idnsname': [fwzone1_dnsname],
'idnszoneactive': [u'TRUE'],
@@ -4616,6 +4635,7 @@ class test_forward_master_zones_mutual_exlusion(Declarative):
expected={
'value': zone_findtest_forward_dnsname,
'summary': None,
+ 'messages': lambda x: True, # fake forwarders - ignore message
'result': {
'dn': zone_findtest_forward_dn,
'idnsname': [zone_findtest_forward_dnsname],