diff options
-rw-r--r-- | API.txt | 12 | ||||
-rw-r--r-- | ipalib/plugins/dns.py | 87 | ||||
-rw-r--r-- | ipapython/ipautil.py | 19 | ||||
-rw-r--r-- | ipaserver/install/bindinstance.py | 8 | ||||
-rw-r--r-- | ipaserver/install/plugins/Makefile.am | 1 | ||||
-rw-r--r-- | ipaserver/install/plugins/dns.py | 65 | ||||
-rw-r--r-- | tests/test_xmlrpc/test_dns_plugin.py | 86 |
7 files changed, 264 insertions, 14 deletions
@@ -1067,7 +1067,7 @@ output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None) output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None)) output: Output('value', <type 'unicode'>, None) command: dnszone_add -args: 1,19,3 +args: 1,21,3 arg: Str('idnsname', attribute=True, cli_name='name', multivalue=False, primary_key=True, required=True) option: Str('name_from_ip', attribute=False, cli_name='name_from_ip', multivalue=False, required=False) option: Str('idnssoamname', attribute=True, cli_name='name_server', multivalue=False, required=True) @@ -1081,6 +1081,8 @@ option: Int('dnsttl', attribute=True, cli_name='ttl', multivalue=False, required option: StrEnum('dnsclass', attribute=True, cli_name='class', multivalue=False, required=False, values=(u'IN', u'CS', u'CH', u'HS')) option: Str('idnsupdatepolicy', attribute=True, cli_name='update_policy', multivalue=False, required=False) option: Bool('idnsallowdynupdate', attribute=True, autofill=True, cli_name='dynamic_update', default=False, multivalue=False, required=False) +option: Str('idnsallowquery', attribute=True, autofill=True, cli_name='allow_query', default=u'any;', multivalue=False, required=False) +option: Str('idnsallowtransfer', attribute=True, autofill=True, cli_name='allow_transfer', default=u'none;', multivalue=False, required=False) option: Str('setattr*', cli_name='setattr', exclude='webui') option: Str('addattr*', cli_name='addattr', exclude='webui') option: Flag('force', autofill=True, default=False) @@ -1111,7 +1113,7 @@ output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None) output: Output('result', <type 'bool'>, None) output: Output('value', <type 'unicode'>, None) command: dnszone_find -args: 1,21,4 +args: 1,23,4 arg: Str('criteria?', noextrawhitespace=False) option: Str('idnsname', attribute=True, autofill=False, cli_name='name', multivalue=False, primary_key=True, query=True, required=False) option: Str('name_from_ip', attribute=False, autofill=False, cli_name='name_from_ip', multivalue=False, query=True, required=False) @@ -1127,6 +1129,8 @@ option: StrEnum('dnsclass', attribute=True, autofill=False, cli_name='class', mu option: Str('idnsupdatepolicy', attribute=True, autofill=False, cli_name='update_policy', multivalue=False, query=True, required=False) option: Bool('idnszoneactive', attribute=True, autofill=False, cli_name='zone_active', multivalue=False, query=True, required=False) option: Bool('idnsallowdynupdate', attribute=True, autofill=False, cli_name='dynamic_update', default=False, multivalue=False, query=True, required=False) +option: Str('idnsallowquery', attribute=True, autofill=False, cli_name='allow_query', default=u'any;', multivalue=False, query=True, required=False) +option: Str('idnsallowtransfer', attribute=True, autofill=False, cli_name='allow_transfer', default=u'none;', multivalue=False, query=True, required=False) option: Int('timelimit?', autofill=False, minvalue=0) option: Int('sizelimit?', autofill=False, minvalue=0) option: Flag('forward_only', autofill=True, cli_name='forward_only', default=False) @@ -1139,7 +1143,7 @@ output: ListOfEntries('result', (<type 'list'>, <type 'tuple'>), Gettext('A list output: Output('count', <type 'int'>, None) output: Output('truncated', <type 'bool'>, None) command: dnszone_mod -args: 1,19,3 +args: 1,21,3 arg: Str('idnsname', attribute=True, cli_name='name', multivalue=False, primary_key=True, query=True, required=True) option: Str('name_from_ip', attribute=False, autofill=False, cli_name='name_from_ip', multivalue=False, required=False) option: Str('idnssoamname', attribute=True, autofill=False, cli_name='name_server', multivalue=False, required=False) @@ -1153,6 +1157,8 @@ option: Int('dnsttl', attribute=True, autofill=False, cli_name='ttl', multivalue option: StrEnum('dnsclass', attribute=True, autofill=False, cli_name='class', multivalue=False, required=False, values=(u'IN', u'CS', u'CH', u'HS')) option: Str('idnsupdatepolicy', attribute=True, autofill=False, cli_name='update_policy', multivalue=False, required=False) option: Bool('idnsallowdynupdate', attribute=True, autofill=False, cli_name='dynamic_update', default=False, multivalue=False, required=False) +option: Str('idnsallowquery', attribute=True, autofill=False, cli_name='allow_query', default=u'any;', multivalue=False, required=False) +option: Str('idnsallowtransfer', attribute=True, autofill=False, cli_name='allow_transfer', default=u'none;', multivalue=False, required=False) option: Str('setattr*', cli_name='setattr', exclude='webui') option: Str('addattr*', cli_name='addattr', exclude='webui') option: Str('delattr*', cli_name='delattr', exclude='webui') diff --git a/ipalib/plugins/dns.py b/ipalib/plugins/dns.py index 495a21b1..0b54aae0 100644 --- a/ipalib/plugins/dns.py +++ b/ipalib/plugins/dns.py @@ -30,7 +30,7 @@ from ipalib.plugins.baseldap import * from ipalib import _, ngettext from ipalib.util import validate_zonemgr, normalize_zonemgr, validate_hostname from ipapython import dnsclient -from ipapython.ipautil import valid_ip +from ipapython.ipautil import valid_ip, CheckedIPAddress from ldap import explode_dn __doc__ = _(""" @@ -48,6 +48,9 @@ EXAMPLES: ipa dnszone-mod example.com --dynamic-update=TRUE \\ --update-policy="grant EXAMPLE.COM krb5-self * A; grant EXAMPLE.COM krb5-self * AAAA;" + Modify the zone to allow zone transfers for local network only: + ipa dnszone-mod example.com --allow-transfer=10.0.0.0/8 + Add new reverse zone specified by network IP address: ipa dnszone-add --name-from-ip=80.142.15.0/24 \\ --name-server=nameserver.example.com @@ -225,6 +228,68 @@ def _validate_ipnet(ugettext, ipnet): return _('invalid IP network format') return None +def _validate_bind_aci(ugettext, bind_acis): + if not bind_acis: + return + + bind_acis = bind_acis.split(';') + if bind_acis[-1]: + return _('each ACL element must be terminated with a semicolon') + else: + bind_acis.pop(-1) + + for bind_aci in bind_acis: + if bind_aci in ("any", "none"): + continue + + if bind_aci in ("localhost", "localnets"): + return _('ACL name "%s" is not supported') % bind_aci + + if bind_aci.startswith('!'): + bind_aci = bind_aci[1:] + + try: + ip = CheckedIPAddress(bind_aci, parse_netmask=True, + allow_network=True) + except (netaddr.AddrFormatError, ValueError), e: + return unicode(e) + except UnboundLocalError: + return _(u"invalid address format") + +def _normalize_bind_aci(bind_acis): + if not bind_acis: + return + bind_acis = bind_acis.split(';') + normalized = [] + for bind_aci in bind_acis: + if not bind_aci: + continue + if bind_aci in ("any", "none", "localhost", "localnets"): + normalized.append(bind_aci) + continue + + prefix = "" + if bind_aci.startswith('!'): + bind_aci = bind_aci[1:] + prefix = "!" + + try: + ip = CheckedIPAddress(bind_aci, parse_netmask=True, + allow_network=True) + if '/' in bind_aci: # addr with netmask + netmask = "/%s" % ip.prefixlen + else: + netmask = "" + normalized.append(u"%s%s%s" % (prefix, str(ip), netmask)) + continue + except: + normalized.append(bind_aci) + continue + + acis = u';'.join(normalized) + acis += u';' + return acis + def _domain_name_validator(ugettext, value): try: # Allow domain name which is not fully qualified. These are supported @@ -1150,7 +1215,7 @@ class dnszone(LDAPObject): default_attributes = [ 'idnsname', 'idnszoneactive', 'idnssoamname', 'idnssoarname', 'idnssoaserial', 'idnssoarefresh', 'idnssoaretry', 'idnssoaexpire', - 'idnssoaminimum' + 'idnssoaminimum', 'idnsallowquery', 'idnsallowtransfer' ] + _record_attributes label = _('DNS Zones') label_singular = _('DNS Zone') @@ -1254,6 +1319,24 @@ class dnszone(LDAPObject): default=False, autofill=True ), + Str('idnsallowquery?', + _validate_bind_aci, + normalizer=_normalize_bind_aci, + cli_name='allow_query', + label=_('Allow query'), + doc=_('Semicolon separated list of IP addresses or networks which are allowed to issue queries'), + default=u'any;', # anyone can issue queries by default + autofill=True, + ), + Str('idnsallowtransfer?', + _validate_bind_aci, + normalizer=_normalize_bind_aci, + cli_name='allow_transfer', + label=_('Allow transfer'), + doc=_('Semicolon separated list of IP addresses or networks which are allowed to transfer the zone'), + default=u'none;', # no one can issue queries by default + autofill=True, + ), ) api.register(dnszone) diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py index d9b0455e..596787ff 100644 --- a/ipapython/ipautil.py +++ b/ipapython/ipautil.py @@ -77,7 +77,9 @@ class CheckedIPAddress(netaddr.IPAddress): # and don't allow IP addresses such as '1.1.1' in the same time netaddr_ip_flags = netaddr.INET_PTON - def __init__(self, addr, match_local=False, parse_netmask=True): + def __init__(self, addr, match_local=False, parse_netmask=True, + allow_network=False, allow_loopback=False, + allow_broadcast=False, allow_multicast=False): if isinstance(addr, CheckedIPAddress): super(CheckedIPAddress, self).__init__(addr, flags=self.netaddr_ip_flags) self.prefixlen = addr.prefixlen @@ -98,20 +100,23 @@ class CheckedIPAddress(netaddr.IPAddress): try: addr = netaddr.IPAddress(addr, flags=self.netaddr_ip_flags) except ValueError: - net = netaddr.IPNetwork(addr) + net = netaddr.IPNetwork(addr, flags=self.netaddr_ip_flags) if not parse_netmask: raise ValueError("netmask and prefix length not allowed here") addr = net.ip if addr.version not in (4, 6): raise ValueError("unsupported IP version") - if addr.is_loopback(): + + if not allow_loopback and addr.is_loopback(): raise ValueError("cannot use loopback IP address") - if addr.is_reserved() or addr in netaddr.ip.IPV4_6TO4: + if (not addr.is_loopback() and addr.is_reserved()) \ + or addr in netaddr.ip.IPV4_6TO4: raise ValueError("cannot use IANA reserved IP address") + if addr.is_link_local(): raise ValueError("cannot use link-local IP address") - if addr.is_multicast(): + if not allow_multicast and addr.is_multicast(): raise ValueError("cannot use multicast IP address") if match_local: @@ -143,9 +148,9 @@ class CheckedIPAddress(netaddr.IPAddress): elif addr.version == 6: net = netaddr.IPNetwork(str(addr) + '/64') - if addr == net.network: + if not allow_network and addr == net.network: raise ValueError("cannot use IP network address") - if addr.version == 4 and addr == net.broadcast: + if not allow_broadcast and addr.version == 4 and addr == net.broadcast: raise ValueError("cannot use broadcast IP address") super(CheckedIPAddress, self).__init__(addr, flags=self.netaddr_ip_flags) diff --git a/ipaserver/install/bindinstance.py b/ipaserver/install/bindinstance.py index 2fa12565..9dc12e27 100644 --- a/ipaserver/install/bindinstance.py +++ b/ipaserver/install/bindinstance.py @@ -214,7 +214,9 @@ def add_zone(name, zonemgr=None, dns_backup=None, ns_hostname=None, ns_ip_addres idnssoarname=unicode(zonemgr), ip_address=unicode(ns_ip_address), idnsallowdynupdate=True, - idnsupdatepolicy=unicode(update_policy)) + idnsupdatepolicy=unicode(update_policy), + idnsallowquery=u'any', + idnsallowtransfer=u'none',) except (errors.DuplicateEntry, errors.EmptyModlist): pass @@ -252,7 +254,9 @@ def add_reverse_zone(zone, ns_hostname=None, ns_ip_address=None, idnssoamname=unicode(ns_main+'.'), idnsallowdynupdate=True, ip_address=unicode(ns_ip_address), - idnsupdatepolicy=unicode(update_policy)) + idnsupdatepolicy=unicode(update_policy), + idnsallowquery=u'any', + idnsallowtransfer=u'none',) except (errors.DuplicateEntry, errors.EmptyModlist): pass diff --git a/ipaserver/install/plugins/Makefile.am b/ipaserver/install/plugins/Makefile.am index cfa84c36..e3b2e989 100644 --- a/ipaserver/install/plugins/Makefile.am +++ b/ipaserver/install/plugins/Makefile.am @@ -6,6 +6,7 @@ app_PYTHON = \ baseupdate.py \ fix_replica_memberof.py \ rename_managed.py \ + dns.py \ updateclient.py \ $(NULL) diff --git a/ipaserver/install/plugins/dns.py b/ipaserver/install/plugins/dns.py new file mode 100644 index 00000000..6d72db43 --- /dev/null +++ b/ipaserver/install/plugins/dns.py @@ -0,0 +1,65 @@ +# Authors: +# Martin Kosek <mkosek@redhat.com> +# +# Copyright (C) 2012 Red Hat +# see file 'COPYING' for use and warranty information +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +from ipaserver.install.plugins import MIDDLE +from ipaserver.install.plugins.baseupdate import PostUpdate +from ipaserver.install.plugins import baseupdate +from ipalib import api, errors + +class update_dnszone_acls(PostUpdate): + """ + Set AllowQuery and AllowTransfer ACLs in all zones that may be configured + in an upgraded FreeIPA instance. + + Upgrading to new version of bind-dyndb-ldap and having these ACLs empty + would result in a leak of potentially sensitive DNS information as + zone transfers are enabled for all hosts if not disabled in named.conf + or LDAP. + + This plugin disables the zone transfer by default so that it needs to be + explicitly enabled by FreeIPA Administrator. + """ + order=MIDDLE + + def execute(self, **options): + ldap = self.obj.backend + + try: + zones = api.Command.dnszone_find()['result'] + except errors.NotFound: + self.log.info('No DNS zone to update found') + return (False, False, []) + + for zone in zones: + update = {} + if not zone.get('idnsallowquery'): + # allow query from any client by default + update['idnsallowquery'] = u'any;' + + if not zone.get('idnsallowtransfer'): + # do not open zone transfers by default + update['idnsallowtransfer'] = u'none;' + + if update: + api.Command.dnszone_mod(zone[u'idnsname'][0], **update) + + + return (False, False, []) + +api.register(update_dnszone_acls) diff --git a/tests/test_xmlrpc/test_dns_plugin.py b/tests/test_xmlrpc/test_dns_plugin.py index 4ad67ce8..5d05d3af 100644 --- a/tests/test_xmlrpc/test_dns_plugin.py +++ b/tests/test_xmlrpc/test_dns_plugin.py @@ -115,6 +115,8 @@ class test_dns(Declarative): 'idnssoaexpire': [fuzzy_digits], 'idnssoaminimum': [fuzzy_digits], 'idnsallowdynupdate': [u'FALSE'], + 'idnsallowtransfer': [u'none;'], + 'idnsallowquery': [u'any;'], 'objectclass': [u'top', u'idnsrecord', u'idnszone'], }, }, @@ -169,6 +171,8 @@ class test_dns(Declarative): 'idnssoaexpire': [fuzzy_digits], 'idnssoaminimum': [fuzzy_digits], 'idnsallowdynupdate': [u'FALSE'], + 'idnsallowtransfer': [u'none;'], + 'idnsallowquery': [u'any;'], 'objectclass': [u'top', u'idnsrecord', u'idnszone'], }, }, @@ -202,6 +206,8 @@ class test_dns(Declarative): 'idnssoaretry': [fuzzy_digits], 'idnssoaexpire': [fuzzy_digits], 'idnssoaminimum': [fuzzy_digits], + 'idnsallowtransfer': [u'none;'], + 'idnsallowquery': [u'any;'], }, }, ), @@ -224,6 +230,8 @@ class test_dns(Declarative): 'idnssoaretry': [fuzzy_digits], 'idnssoaexpire': [fuzzy_digits], 'idnssoaminimum': [fuzzy_digits], + 'idnsallowtransfer': [u'none;'], + 'idnsallowquery': [u'any;'], }, }, ), @@ -254,6 +262,8 @@ class test_dns(Declarative): 'idnssoaexpire': [fuzzy_digits], 'idnssoaminimum': [fuzzy_digits], 'idnsallowdynupdate': [u'FALSE'], + 'idnsallowtransfer': [u'none;'], + 'idnsallowquery': [u'any;'], 'objectclass': [u'top', u'idnsrecord', u'idnszone'], }, }, @@ -279,6 +289,8 @@ class test_dns(Declarative): 'idnssoaretry': [fuzzy_digits], 'idnssoaexpire': [fuzzy_digits], 'idnssoaminimum': [fuzzy_digits], + 'idnsallowtransfer': [u'none;'], + 'idnsallowquery': [u'any;'], }, { 'dn': unicode(dnszone1_dn), @@ -292,6 +304,8 @@ class test_dns(Declarative): 'idnssoaretry': [fuzzy_digits], 'idnssoaexpire': [fuzzy_digits], 'idnssoaminimum': [fuzzy_digits], + 'idnsallowtransfer': [u'none;'], + 'idnsallowquery': [u'any;'], }], }, ), @@ -316,6 +330,8 @@ class test_dns(Declarative): 'idnssoaretry': [fuzzy_digits], 'idnssoaexpire': [fuzzy_digits], 'idnssoaminimum': [fuzzy_digits], + 'idnsallowtransfer': [u'none;'], + 'idnsallowquery': [u'any;'], }], }, ), @@ -361,6 +377,8 @@ class test_dns(Declarative): 'idnssoaretry': [fuzzy_digits], 'idnssoaexpire': [fuzzy_digits], 'idnssoaminimum': [fuzzy_digits], + 'idnsallowtransfer': [u'none;'], + 'idnsallowquery': [u'any;'], }, }, ), @@ -395,6 +413,8 @@ class test_dns(Declarative): 'idnssoaretry': [fuzzy_digits], 'idnssoaexpire': [fuzzy_digits], 'idnssoaminimum': [fuzzy_digits], + 'idnsallowtransfer': [u'none;'], + 'idnsallowquery': [u'any;'], }, }, ), @@ -746,6 +766,8 @@ class test_dns(Declarative): 'idnssoaexpire': [fuzzy_digits], 'idnssoaminimum': [fuzzy_digits], 'idnsallowdynupdate': [u'FALSE'], + 'idnsallowtransfer': [u'none;'], + 'idnsallowquery': [u'any;'], 'objectclass': [u'top', u'idnsrecord', u'idnszone'], }, }, @@ -788,6 +810,70 @@ class test_dns(Declarative): dict( + desc='Try to add invalid allow-query to zone %r' % dnszone1, + command=('dnszone_mod', [dnszone1], {'idnsallowquery': u'localhost'}), + expected=errors.ValidationError(name='idnsallowquery', error=''), + ), + + dict( + desc='Add allow-query ACL to zone %r' % dnszone1, + command=('dnszone_mod', [dnszone1], {'idnsallowquery': u'!10/8;any'}), + expected={ + 'value': dnszone1, + 'summary': None, + 'result': { + 'idnsname': [dnszone1], + 'idnszoneactive': [u'TRUE'], + 'nsrecord': [dnszone1_mname], + 'mxrecord': [u'0 ns1.dnszone.test.'], + 'locrecord': [u"49 11 42.400 N 16 36 29.600 E 227.64"], + 'idnssoamname': [dnszone1_mname], + 'idnssoarname': [dnszone1_rname], + 'idnssoaserial': [fuzzy_digits], + 'idnssoarefresh': [u'5478'], + 'idnssoaretry': [fuzzy_digits], + 'idnssoaexpire': [fuzzy_digits], + 'idnssoaminimum': [fuzzy_digits], + 'idnsallowquery': [u'!10.0.0.0/8;any;'], + 'idnsallowtransfer': [u'none;'], + }, + }, + ), + + + dict( + desc='Try to add invalid allow-transfer to zone %r' % dnszone1, + command=('dnszone_mod', [dnszone1], {'idnsallowtransfer': u'10.'}), + expected=errors.ValidationError(name='idnsallowtransfer', error=''), + ), + + dict( + desc='Add allow-transer ACL to zone %r' % dnszone1, + command=('dnszone_mod', [dnszone1], {'idnsallowtransfer': u'80.142.15.80'}), + expected={ + 'value': dnszone1, + 'summary': None, + 'result': { + 'idnsname': [dnszone1], + 'idnszoneactive': [u'TRUE'], + 'nsrecord': [dnszone1_mname], + 'mxrecord': [u'0 ns1.dnszone.test.'], + 'locrecord': [u"49 11 42.400 N 16 36 29.600 E 227.64"], + 'idnssoamname': [dnszone1_mname], + 'idnssoarname': [dnszone1_rname], + 'idnssoaserial': [fuzzy_digits], + 'idnssoarefresh': [u'5478'], + 'idnssoaretry': [fuzzy_digits], + 'idnssoaexpire': [fuzzy_digits], + 'idnssoaminimum': [fuzzy_digits], + 'idnsallowquery': [u'!10.0.0.0/8;any;'], + 'idnsallowtransfer': [u'80.142.15.80;'], + }, + }, + ), + + + dict( desc='Delete zone %r' % dnszone1, command=('dnszone_del', [dnszone1], {}), expected={ |