summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--API.txt12
-rw-r--r--ipalib/plugins/dns.py87
-rw-r--r--ipapython/ipautil.py19
-rw-r--r--ipaserver/install/bindinstance.py8
-rw-r--r--ipaserver/install/plugins/Makefile.am1
-rw-r--r--ipaserver/install/plugins/dns.py65
-rw-r--r--tests/test_xmlrpc/test_dns_plugin.py86
7 files changed, 264 insertions, 14 deletions
diff --git a/API.txt b/API.txt
index 9942e630..d57e1ba1 100644
--- a/API.txt
+++ b/API.txt
@@ -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={