mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2024-12-23 07:33:27 -06:00
f1c58fb646
MIT KRB5 1.15 introduced KDC service discovery with URI records. _kerberos and _kpasswd URI records can provide TCP, UDP, and Kerberos KDC-Proxy references. URI lookups take precedence over SRV lookups, falling back to SRV lookups if no URI records are found. Also reduce TTL for system records from one day to one hour. It allows users to remove or update discovery entries in a timely fashion. See: https://web.mit.edu/kerberos/krb5-latest/doc/admin/realm_config.html#kdc-discovery Fixes: https://pagure.io/freeipa/issue/8968 Signed-off-by: Christian Heimes <cheimes@redhat.com> Reviewed-By: Rob Crittenden <rcritten@redhat.com>
558 lines
19 KiB
Python
558 lines
19 KiB
Python
#
|
|
# Copyright (C) 2016 FreeIPA Contributors see COPYING for license
|
|
#
|
|
|
|
from __future__ import absolute_import
|
|
|
|
import logging
|
|
|
|
import six
|
|
|
|
from collections import defaultdict, OrderedDict
|
|
from dns import (
|
|
rdata,
|
|
rdataclass,
|
|
rdatatype,
|
|
zone,
|
|
)
|
|
|
|
from time import sleep, time
|
|
|
|
from ipalib import errors
|
|
from ipalib.dns import record_name_format
|
|
from ipapython.dnsutil import DNSName
|
|
from ipaserver.install import installutils
|
|
|
|
if six.PY3:
|
|
unicode=str
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
IPA_DEFAULT_MASTER_SRV_REC = (
|
|
# srv record name, port
|
|
(DNSName('_ldap._tcp'), 389),
|
|
# Kerberos records are provided for MIT KRB5 < 1.15 and AD
|
|
(DNSName('_kerberos._tcp'), 88),
|
|
(DNSName('_kerberos._udp'), 88),
|
|
(DNSName('_kerberos-master._tcp'), 88),
|
|
(DNSName('_kerberos-master._udp'), 88),
|
|
(DNSName('_kpasswd._tcp'), 464),
|
|
(DNSName('_kpasswd._udp'), 464),
|
|
)
|
|
|
|
IPA_DEFAULT_MASTER_URI_REC = (
|
|
# URI record name, URI template
|
|
|
|
# MIT KRB5 1.15+ prefers URI records for service discovery
|
|
# scheme: always krb5srv
|
|
# flags: empty or 'm' for primary server
|
|
# transport: 'tcp', 'udp', or 'kkdcp')
|
|
# residual: 'hostname', 'hostname:port', or 'https://' URL
|
|
(DNSName('_kerberos'), "krb5srv:m:tcp:{hostname}"),
|
|
(DNSName('_kerberos'), "krb5srv:m:udp:{hostname}"),
|
|
(DNSName('_kpasswd'), "krb5srv:m:tcp:{hostname}"),
|
|
(DNSName('_kpasswd'), "krb5srv:m:udp:{hostname}"),
|
|
)
|
|
|
|
IPA_DEFAULT_ADTRUST_SRV_REC = (
|
|
# srv record name, port
|
|
(DNSName('_ldap._tcp.Default-First-Site-Name._sites.dc._msdcs'), 389),
|
|
(DNSName('_ldap._tcp.dc._msdcs'), 389),
|
|
(DNSName('_kerberos._tcp.Default-First-Site-Name._sites.dc._msdcs'), 88),
|
|
(DNSName('_kerberos._udp.Default-First-Site-Name._sites.dc._msdcs'), 88),
|
|
(DNSName('_kerberos._tcp.dc._msdcs'), 88),
|
|
(DNSName('_kerberos._udp.dc._msdcs'), 88),
|
|
)
|
|
|
|
IPA_DEFAULT_NTP_SRV_REC = (
|
|
# srv record name, port
|
|
(DNSName("_ntp._udp"), 123),
|
|
)
|
|
|
|
CA_RECORDS_DNS_TIMEOUT = 15 # timeout in seconds
|
|
|
|
|
|
class IPADomainIsNotManagedByIPAError(Exception):
|
|
pass
|
|
|
|
|
|
class IPASystemRecords:
|
|
|
|
# fixme do it configurable
|
|
PRIORITY_HIGH = 0
|
|
PRIORITY_LOW = 50
|
|
# FIXME: use TTL from config
|
|
TTL = 3600
|
|
|
|
def __init__(self, api_instance, all_servers=False):
|
|
self.api_instance = api_instance
|
|
self.domain_abs = DNSName(self.api_instance.env.domain).make_absolute()
|
|
self.servers_data = OrderedDict()
|
|
self.__init_data(all_servers=all_servers)
|
|
|
|
def reload_data(self):
|
|
"""
|
|
After any change made to IPA servers, this method must be called to
|
|
update data in the object, otherwise invalid records may be
|
|
created/updated
|
|
"""
|
|
self.__init_data()
|
|
|
|
def __get_server_attrs(self, server_result):
|
|
weight = int(server_result.get('ipaserviceweight', ['100'])[0])
|
|
location = server_result.get('ipalocation_location', [None])[0]
|
|
roles = set(server_result.get('enabled_role_servrole', ()))
|
|
|
|
return weight, location, roles
|
|
|
|
def __get_location_suffix(self, location):
|
|
return location + DNSName('_locations') + self.domain_abs
|
|
|
|
def __init_data(self, all_servers=False):
|
|
self.servers_data.clear()
|
|
|
|
kwargs = dict(no_members=False)
|
|
if not all_servers:
|
|
# only active, fully installed masters]
|
|
kwargs["servrole"] = "IPA master"
|
|
servers = self.api_instance.Command.server_find(**kwargs)
|
|
|
|
for s in servers['result']:
|
|
weight, location, roles = self.__get_server_attrs(s)
|
|
self.servers_data[s['cn'][0]] = {
|
|
'weight': weight,
|
|
'location': location,
|
|
'roles': roles,
|
|
}
|
|
|
|
def __add_srv_records(
|
|
self, zone_obj, hostname, rname_port_map,
|
|
weight=100, priority=0, location=None
|
|
):
|
|
assert isinstance(hostname, DNSName)
|
|
assert isinstance(priority, int)
|
|
assert isinstance(weight, int)
|
|
|
|
if location:
|
|
suffix = self.__get_location_suffix(location)
|
|
else:
|
|
suffix = self.domain_abs
|
|
|
|
for name, port in rname_port_map:
|
|
rd = rdata.from_text(
|
|
rdataclass.IN, rdatatype.SRV,
|
|
'{0} {1} {2} {3}'.format(
|
|
priority, weight, port, hostname.make_absolute()
|
|
)
|
|
)
|
|
|
|
r_name = name.derelativize(suffix)
|
|
|
|
rdataset = zone_obj.get_rdataset(
|
|
r_name, rdatatype.SRV, create=True)
|
|
rdataset.add(rd, ttl=self.TTL)
|
|
|
|
def __add_uri_records(
|
|
self, zone_obj, hostname, rname_uri_map,
|
|
weight=100, priority=0, location=None
|
|
):
|
|
assert isinstance(hostname, DNSName)
|
|
assert isinstance(priority, int)
|
|
assert isinstance(weight, int)
|
|
|
|
if location:
|
|
suffix = self.__get_location_suffix(location)
|
|
else:
|
|
suffix = self.domain_abs
|
|
|
|
for name, uri_template in rname_uri_map:
|
|
uri = uri_template.format(hostname=hostname.make_absolute())
|
|
rd = rdata.from_text(
|
|
rdataclass.IN, rdatatype.URI,
|
|
'{0} {1} {2}'.format(
|
|
priority, weight, uri
|
|
)
|
|
)
|
|
|
|
r_name = name.derelativize(suffix)
|
|
|
|
rdataset = zone_obj.get_rdataset(
|
|
r_name, rdatatype.URI, create=True)
|
|
rdataset.add(rd, ttl=self.TTL)
|
|
|
|
def __add_ca_records_from_hostname(self, zone_obj, hostname):
|
|
assert isinstance(hostname, DNSName) and hostname.is_absolute()
|
|
r_name = DNSName('ipa-ca') + self.domain_abs
|
|
rrsets = None
|
|
end_time = time() + CA_RECORDS_DNS_TIMEOUT
|
|
while True:
|
|
try:
|
|
# function logs errors
|
|
rrsets = installutils.resolve_rrsets_nss(hostname)
|
|
except OSError:
|
|
# also retry on EAI_AGAIN, EAI_FAIL
|
|
pass
|
|
if rrsets:
|
|
break
|
|
if time() >= end_time:
|
|
break
|
|
sleep(3)
|
|
|
|
if not rrsets:
|
|
logger.error('unable to resolve host name %s to IP address, '
|
|
'ipa-ca DNS record will be incomplete', hostname)
|
|
return
|
|
|
|
for rrset in rrsets:
|
|
for rd in rrset:
|
|
rdataset = zone_obj.get_rdataset(
|
|
r_name, rd.rdtype, create=True)
|
|
rdataset.add(rd, ttl=self.TTL)
|
|
|
|
def __add_kerberos_txt_rec(self, zone_obj):
|
|
# FIXME: with external DNS, this should generate records for all
|
|
# realmdomains
|
|
r_name = DNSName('_kerberos') + self.domain_abs
|
|
rd = rdata.from_text(rdataclass.IN, rdatatype.TXT,
|
|
self.api_instance.env.realm)
|
|
rdataset = zone_obj.get_rdataset(
|
|
r_name, rdatatype.TXT, create=True
|
|
)
|
|
rdataset.add(rd, ttl=self.TTL)
|
|
|
|
def _add_base_dns_records_for_server(
|
|
self, zone_obj, hostname, roles=None, include_master_role=True,
|
|
include_kerberos_realm=True,
|
|
):
|
|
server = self.servers_data[hostname]
|
|
if roles:
|
|
eff_roles = server['roles'] & set(roles)
|
|
else:
|
|
eff_roles = server['roles']
|
|
hostname_abs = DNSName(hostname).make_absolute()
|
|
|
|
if include_kerberos_realm:
|
|
self.__add_kerberos_txt_rec(zone_obj)
|
|
|
|
# get master records
|
|
if include_master_role:
|
|
self.__add_srv_records(
|
|
zone_obj,
|
|
hostname_abs,
|
|
IPA_DEFAULT_MASTER_SRV_REC,
|
|
weight=server['weight']
|
|
)
|
|
self.__add_uri_records(
|
|
zone_obj,
|
|
hostname_abs,
|
|
IPA_DEFAULT_MASTER_URI_REC,
|
|
weight=server['weight']
|
|
)
|
|
|
|
if 'CA server' in eff_roles:
|
|
self.__add_ca_records_from_hostname(zone_obj, hostname_abs)
|
|
|
|
if 'AD trust controller' in eff_roles:
|
|
self.__add_srv_records(
|
|
zone_obj,
|
|
hostname_abs,
|
|
IPA_DEFAULT_ADTRUST_SRV_REC,
|
|
weight=server['weight']
|
|
)
|
|
|
|
if 'NTP server' in eff_roles:
|
|
self.__add_srv_records(
|
|
zone_obj,
|
|
hostname_abs,
|
|
IPA_DEFAULT_NTP_SRV_REC,
|
|
weight=server['weight']
|
|
)
|
|
|
|
def _get_location_dns_records_for_server(
|
|
self, zone_obj, hostname, locations,
|
|
roles=None, include_master_role=True):
|
|
server = self.servers_data[hostname]
|
|
if roles:
|
|
eff_roles = server['roles'] & roles
|
|
else:
|
|
eff_roles = server['roles']
|
|
hostname_abs = DNSName(hostname).make_absolute()
|
|
|
|
# generate locations specific records
|
|
for location in locations:
|
|
if location == self.servers_data[hostname]['location']:
|
|
priority = self.PRIORITY_HIGH
|
|
else:
|
|
priority = self.PRIORITY_LOW
|
|
|
|
if include_master_role:
|
|
self.__add_srv_records(
|
|
zone_obj,
|
|
hostname_abs,
|
|
IPA_DEFAULT_MASTER_SRV_REC,
|
|
weight=server['weight'],
|
|
priority=priority,
|
|
location=location
|
|
)
|
|
self.__add_uri_records(
|
|
zone_obj,
|
|
hostname_abs,
|
|
IPA_DEFAULT_MASTER_URI_REC,
|
|
weight=server['weight'],
|
|
priority=priority,
|
|
location=location
|
|
)
|
|
|
|
if 'AD trust controller' in eff_roles:
|
|
self.__add_srv_records(
|
|
zone_obj,
|
|
hostname_abs,
|
|
IPA_DEFAULT_ADTRUST_SRV_REC,
|
|
weight=server['weight'],
|
|
priority=priority,
|
|
location=location
|
|
)
|
|
|
|
if 'NTP server' in eff_roles:
|
|
self.__add_srv_records(
|
|
zone_obj,
|
|
hostname_abs,
|
|
IPA_DEFAULT_NTP_SRV_REC,
|
|
weight=server['weight'],
|
|
priority=priority,
|
|
location=location
|
|
)
|
|
|
|
return zone_obj
|
|
|
|
def __prepare_records_update_dict(self, node):
|
|
update_dict = defaultdict(list)
|
|
for rdataset in node:
|
|
for rdata in rdataset:
|
|
option_name = (record_name_format % rdatatype.to_text(
|
|
rdata.rdtype).lower())
|
|
update_dict[option_name].append(unicode(rdata.to_text()))
|
|
return update_dict
|
|
|
|
def __update_dns_records(
|
|
self, record_name, nodes, set_cname_template=True
|
|
):
|
|
update_dict = self.__prepare_records_update_dict(nodes)
|
|
cname_template = {
|
|
'addattr': ['objectclass=idnsTemplateObject'],
|
|
'setattr': [
|
|
r'idnsTemplateAttribute;cnamerecord=%s'
|
|
r'.\{substitutionvariable_ipalocation\}._locations' %
|
|
record_name.relativize(self.domain_abs)
|
|
]
|
|
}
|
|
try:
|
|
if set_cname_template:
|
|
# only srv records should have configured cname templates
|
|
update_dict.update(cname_template)
|
|
self.api_instance.Command.dnsrecord_mod(
|
|
self.domain_abs, record_name,
|
|
**update_dict
|
|
)
|
|
except errors.NotFound:
|
|
# because internal API magic, addattr and setattr doesn't work with
|
|
# dnsrecord-add well, use dnsrecord-mod instead later
|
|
update_dict.pop('addattr', None)
|
|
update_dict.pop('setattr', None)
|
|
|
|
self.api_instance.Command.dnsrecord_add(
|
|
self.domain_abs, record_name, **update_dict)
|
|
|
|
if set_cname_template:
|
|
try:
|
|
self.api_instance.Command.dnsrecord_mod(
|
|
self.domain_abs,
|
|
record_name, **cname_template)
|
|
except errors.EmptyModlist:
|
|
pass
|
|
except errors.EmptyModlist:
|
|
pass
|
|
|
|
def get_base_records(
|
|
self, servers=None, roles=None, include_master_role=True,
|
|
include_kerberos_realm=True
|
|
):
|
|
"""
|
|
Generate IPA service records for specific servers and roles
|
|
:param servers: list of server which will be used in records,
|
|
if None all IPA servers will be used
|
|
:param roles: roles for which DNS records will be generated,
|
|
if None all roles will be used
|
|
:param include_master_role: generate records required by IPA master
|
|
role
|
|
:return: dns.zone.Zone object that contains base DNS records
|
|
"""
|
|
|
|
zone_obj = zone.Zone(self.domain_abs, relativize=False)
|
|
if servers is None:
|
|
servers = list(self.servers_data)
|
|
|
|
for server in servers:
|
|
self._add_base_dns_records_for_server(zone_obj, server,
|
|
roles=roles, include_master_role=include_master_role,
|
|
include_kerberos_realm=include_kerberos_realm
|
|
)
|
|
return zone_obj
|
|
|
|
def get_locations_records(
|
|
self, servers=None, roles=None, include_master_role=True):
|
|
"""
|
|
Generate IPA location records for specific servers and roles.
|
|
:param servers: list of server which will be used in records,
|
|
if None all IPA servers will be used
|
|
:param roles: roles for which DNS records will be generated,
|
|
if None all roles will be used
|
|
:param include_master_role: generate records required by IPA master
|
|
role
|
|
:return: dns.zone.Zone object that contains location DNS records
|
|
"""
|
|
zone_obj = zone.Zone(self.domain_abs, relativize=False)
|
|
if servers is None:
|
|
servers = list(self.servers_data)
|
|
|
|
locations_result = self.api_instance.Command.location_find()['result']
|
|
locations = [l['idnsname'][0] for l in locations_result]
|
|
|
|
for server in servers:
|
|
self._get_location_dns_records_for_server(
|
|
zone_obj, server,
|
|
locations, roles=roles,
|
|
include_master_role=include_master_role)
|
|
return zone_obj
|
|
|
|
def update_base_records(self):
|
|
"""
|
|
Update base DNS records for IPA services
|
|
:return: [(record_name, node), ...], [(record_name, node, error), ...]
|
|
where the first list contains successfully updated records, and the
|
|
second list contains failed updates with particular exceptions
|
|
"""
|
|
fail = []
|
|
success = []
|
|
names_requiring_cname_templates = set(
|
|
rec[0].derelativize(self.domain_abs) for rec in (
|
|
IPA_DEFAULT_MASTER_SRV_REC +
|
|
IPA_DEFAULT_ADTRUST_SRV_REC +
|
|
IPA_DEFAULT_NTP_SRV_REC
|
|
)
|
|
)
|
|
|
|
base_zone = self.get_base_records()
|
|
for record_name, node in base_zone.items():
|
|
set_cname_template = record_name in names_requiring_cname_templates
|
|
try:
|
|
self.__update_dns_records(
|
|
record_name, node, set_cname_template)
|
|
except errors.PublicError as e:
|
|
fail.append((record_name, node, e))
|
|
else:
|
|
success.append((record_name, node))
|
|
return success, fail
|
|
|
|
def update_locations_records(self):
|
|
"""
|
|
Update locations DNS records for IPA services
|
|
:return: [(record_name, node), ...], [(record_name, node, error), ...]
|
|
where the first list contains successfully updated records, and the
|
|
second list contains failed updates with particular exceptions
|
|
"""
|
|
fail = []
|
|
success = []
|
|
|
|
location_zone = self.get_locations_records()
|
|
for record_name, nodes in location_zone.items():
|
|
try:
|
|
self.__update_dns_records(
|
|
record_name, nodes,
|
|
set_cname_template=False)
|
|
except errors.PublicError as e:
|
|
fail.append((record_name, nodes, e))
|
|
else:
|
|
success.append((record_name, nodes))
|
|
return success, fail
|
|
|
|
def update_dns_records(self):
|
|
"""
|
|
Update all IPA DNS records
|
|
:return: (sucessfully_updated_base_records, failed_base_records,
|
|
sucessfully_updated_locations_records, failed_locations_records)
|
|
For format see update_base_records or update_locations_method
|
|
:raise IPADomainIsNotManagedByIPAError: if IPA domain is not managed by
|
|
IPA DNS
|
|
"""
|
|
try:
|
|
self.api_instance.Command.dnszone_show(self.domain_abs)
|
|
except errors.NotFound:
|
|
raise IPADomainIsNotManagedByIPAError()
|
|
|
|
return (
|
|
self.update_base_records(),
|
|
self.update_locations_records()
|
|
)
|
|
|
|
def remove_location_records(self, location):
|
|
"""
|
|
Remove all location records
|
|
:param location: DNSName object
|
|
:return: list of successfuly removed record names, list of record
|
|
names that cannot be removed and returned exception in tuples
|
|
[rname1, ...], [(rname2, exc), ...]
|
|
"""
|
|
success = []
|
|
failed = []
|
|
|
|
location = DNSName(location)
|
|
loc_records = []
|
|
for records in (
|
|
IPA_DEFAULT_MASTER_SRV_REC,
|
|
IPA_DEFAULT_ADTRUST_SRV_REC,
|
|
IPA_DEFAULT_NTP_SRV_REC
|
|
):
|
|
for name, _port in records:
|
|
loc_records.append(
|
|
name + self.__get_location_suffix(location))
|
|
|
|
for rname in loc_records:
|
|
try:
|
|
self.api_instance.Command.dnsrecord_del(
|
|
self.domain_abs, rname, del_all=True)
|
|
except errors.NotFound:
|
|
pass
|
|
except errors.PublicError as e:
|
|
failed.append((rname, e))
|
|
else:
|
|
success.append(rname)
|
|
return success, failed
|
|
|
|
|
|
@classmethod
|
|
def records_list_from_node(cls, name, node):
|
|
records = []
|
|
for rdataset in node:
|
|
for rd in rdataset:
|
|
records.append(
|
|
'{name} {ttl} {rdclass} {rdtype} {rdata}'.format(
|
|
name=name.ToASCII(),
|
|
ttl=rdataset.ttl,
|
|
rdclass=rdataclass.to_text(rd.rdclass),
|
|
rdtype=rdatatype.to_text(rd.rdtype),
|
|
rdata=rd.to_text()
|
|
)
|
|
)
|
|
return records
|
|
|
|
@classmethod
|
|
def records_list_from_zone(cls, zone_obj, sort=True):
|
|
records = []
|
|
for name, node in zone_obj.items():
|
|
records.extend(IPASystemRecords.records_list_from_node(name, node))
|
|
if sort:
|
|
records.sort()
|
|
return records
|