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>
This commit is contained in:
Martin Basti 2015-04-24 13:37:07 +02:00 committed by Petr Vobornik
parent 9aa6124b39
commit f8c8c360f1
4 changed files with 202 additions and 3 deletions

View File

@ -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
"""

View File

@ -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):

View File

@ -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):
"""

View File

@ -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],