Generate CNAMEs for TXT+URI location krb records

The IPA location system relies on DNS record priorities in order to give
higher precedence to servers from the same location. For Kerberos, this
is done by redirecting generic SRV records (e.g.
_kerberos._udp.[domain].) to location-aware records (e.g.
_kerberos._udp.[location]._locations.[domain].) using CNAMEs.

This commit applies the same logic for URI records. URI location-aware
record were created, but there were no redirection from generic URI
records. It was causing them to be ignored in practice.

Kerberos URI and TXT records have the same name: "_kerberos". However,
CNAME records cannot coexist with any other record type. To avoid this
conflict, the generic TXT realm record was replaced by location-aware
records, even if the content of these records is the same for all
locations.

Fixes: https://pagure.io/freeipa/issue/9257
Signed-off-by: Julien Rische <jrische@redhat.com>
Reviewed-By: Rob Crittenden <rcritten@redhat.com>
This commit is contained in:
Julien Rische 2022-10-04 15:03:28 +02:00 committed by Francisco Trivino
parent 3d6d7e9fdf
commit 673d2b82d0
4 changed files with 257 additions and 9 deletions

View File

@ -42,3 +42,4 @@ plugin: update_dnsserver_configuration_into_ldap
plugin: update_ldap_server_list
plugin: update_dna_shared_config
plugin: update_unhashed_password
plugin: update_krb_uri_txt_records_for_locations

View File

@ -70,6 +70,10 @@ IPA_DEFAULT_NTP_SRV_REC = (
(DNSName("_ntp._udp"), 123),
)
IPA_DEFAULT_KRB_TXT_REC = (
(DNSName('_kerberos'), "\"{realm}\""),
)
CA_RECORDS_DNS_TIMEOUT = 15 # timeout in seconds
@ -210,10 +214,15 @@ class IPASystemRecords:
r_name, rd.rdtype, create=True)
rdataset.add(rd, ttl=self.TTL)
def __add_kerberos_txt_rec(self, zone_obj):
def __add_kerberos_txt_rec(self, zone_obj, location=None):
# FIXME: with external DNS, this should generate records for all
# realmdomains
r_name = DNSName('_kerberos') + self.domain_abs
if location:
suffix = self.__get_location_suffix(location)
else:
suffix = self.domain_abs
r_name = DNSName('_kerberos') + suffix
rd = rdata.from_text(rdataclass.IN, rdatatype.TXT,
self.api_instance.env.realm)
rdataset = zone_obj.get_rdataset(
@ -233,7 +242,7 @@ class IPASystemRecords:
hostname_abs = DNSName(hostname).make_absolute()
if include_kerberos_realm:
self.__add_kerberos_txt_rec(zone_obj)
self.__add_kerberos_txt_rec(zone_obj, location=None)
# get master records
if include_master_role:
@ -271,7 +280,8 @@ class IPASystemRecords:
def _get_location_dns_records_for_server(
self, zone_obj, hostname, locations,
roles=None, include_master_role=True):
roles=None, include_master_role=True,
include_kerberos_realm=True):
server = self.servers_data[hostname]
if roles:
eff_roles = server['roles'] & roles
@ -286,6 +296,9 @@ class IPASystemRecords:
else:
priority = self.PRIORITY_LOW
if include_kerberos_realm:
self.__add_kerberos_txt_rec(zone_obj, location)
if include_master_role:
self.__add_srv_records(
zone_obj,
@ -401,7 +414,9 @@ class IPASystemRecords:
return zone_obj
def get_locations_records(
self, servers=None, roles=None, include_master_role=True):
self, servers=None, roles=None, include_master_role=True,
include_kerberos_realm=True
):
"""
Generate IPA location records for specific servers and roles.
:param servers: list of server which will be used in records,
@ -423,7 +438,8 @@ class IPASystemRecords:
self._get_location_dns_records_for_server(
zone_obj, server,
locations, roles=roles,
include_master_role=include_master_role)
include_master_role=include_master_role,
include_kerberos_realm=include_kerberos_realm)
return zone_obj
def update_base_records(self):
@ -437,9 +453,11 @@ class IPASystemRecords:
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
IPA_DEFAULT_MASTER_SRV_REC
+ IPA_DEFAULT_MASTER_URI_REC
+ IPA_DEFAULT_KRB_TXT_REC
+ IPA_DEFAULT_ADTRUST_SRV_REC
+ IPA_DEFAULT_NTP_SRV_REC
)
)

View File

@ -551,3 +551,111 @@ class update_dnsserver_configuration_into_ldap(DNSUpdater):
"created in LDAP database")
sysupgrade.set_upgrade_state('dns', 'server_config_to_ldap', True)
return False, []
@register()
class update_krb_uri_txt_records_for_locations(DNSUpdater):
backup_filename = u'dns-krb-uri-txt-records-for-locations-%Y-%m-%d-%H-%M-'\
u'%S.ldif'
def execute(self, **options):
ldap = self.api.Backend.ldap2
if not dns_container_exists(ldap):
return False, []
locations = []
for location in self.api.Command.location_find()['result']:
locations.append(str(location['idnsname'][0]))
if not locations:
return False, []
tmpl_class_attr = 'objectClass'
tmpl_class_value = 'idnsTemplateObject'
cname_tmpl_attr = 'idnsTemplateAttribute;cnamerecord'
cname_tmpl_value = '_kerberos.\\{substitutionvariable_ipalocation\\}.'\
'_locations'
domain = self.env.domain
realm = self.env.realm
dns_updates = []
main_krb_rec = '_kerberos.' + domain + '.'
main_krb_rec_dn = DN(('idnsname', '_kerberos'),
('idnsname', domain + '.'),
self.env.container_dns, self.env.basedn)
try:
entry = ldap.get_entry(main_krb_rec_dn, [
tmpl_class_attr, cname_tmpl_attr,
])
except errors.NotFound:
logger.debug('DNS: %s does not exist, there is no %s record to '
'convert into CNAME', main_krb_rec_dn, main_krb_rec)
else:
cname_updates = []
if tmpl_class_value not in entry.get(tmpl_class_attr, []):
logger.debug('DNS: convert %s into a CNAME record',
main_krb_rec)
cname_updates.append({
'action': 'add',
'attr': tmpl_class_attr,
'value': tmpl_class_value,
})
if cname_tmpl_value not in entry.get(cname_tmpl_attr, []):
logger.debug('DNS: update %s CNAME record to point to location '
'records', main_krb_rec)
cname_updates.append({
'action': 'add',
'attr': cname_tmpl_attr,
'value': cname_tmpl_value,
})
if cname_updates:
dns_updates.append({
'dn': main_krb_rec_dn,
'updates': cname_updates,
})
else:
logger.debug('DNS: %s is already a valid CNAME record',
main_krb_rec)
for location in locations:
location_krb_rec = '_kerberos.' + location + '._locations.' \
+ domain + '.'
location_krb_rec_dn = DN(('idnsname',
'_kerberos.' + location + '._locations'),
('idnsname', domain + '.'),
self.env.container_dns, self.env.basedn)
try:
entry = ldap.get_entry(location_krb_rec_dn, ['tXTRecord'])
except errors.NotFound:
logger.debug('DNS: %s does not exist, there is no %s URI '
'record to rely on to create a TXT record',
location_krb_rec_dn, location_krb_rec)
else:
if 'tXTRecord' in entry:
logger.debug('DNS: there already is a %s TXT record',
location_krb_rec)
else:
logger.debug('DNS: add %s location-aware TXT record',
location_krb_rec)
dns_updates.append({
'dn': location_krb_rec_dn,
'updates': [
{
'action': 'addifnew',
'attr': 'tXTRecord',
'value': f'"{realm}"',
},
],
})
return False, dns_updates

View File

@ -20,6 +20,7 @@ from ipapython.dn import DN
from ipapython.ipautil import template_str
from ipaserver.install import bindinstance
from ipaserver.install.sysupgrade import STATEFILE_FILE
from ipalib.constants import DEFAULT_CONFIG
from ipatests.test_integration.base import IntegrationTest
from ipatests.pytest_ipa.integration import tasks
@ -98,6 +99,24 @@ def clear_sysupgrade(host, *sections):
host.put_file_contents(statefile, out.getvalue())
def get_main_krb_rec_dn(domain):
return DN(
('idnsname', '_kerberos'),
('idnsname', domain.name + '.'),
dict(DEFAULT_CONFIG)['container_dns'],
domain.basedn,
)
def get_location_krb_rec_dn(domain, location):
return DN(
('idnsname', '_kerberos.' + location + '._locations'),
('idnsname', domain.name + '.'),
dict(DEFAULT_CONFIG)['container_dns'],
domain.basedn,
)
class TestUpgrade(IntegrationTest):
"""
Test ipa-server-upgrade.
@ -111,6 +130,73 @@ class TestUpgrade(IntegrationTest):
tasks.install_master(cls.master)
tasks.install_dns(cls.master)
@pytest.fixture
def setup_locations(self):
realm = self.master.domain.realm
_locations = []
def _setup_locations(locations):
_locations = locations
ldap = self.master.ldap_connect()
for location in locations:
self.master.run_command(['ipa', 'location-add', location])
self.master.run_command([
'ipa',
'server-mod',
'--location=' + locations[0],
self.master.hostname,
])
main_krb_rec = ldap.get_entry(
get_main_krb_rec_dn(self.master.domain),
)
main_krb_rec['objectClass'].remove('idnsTemplateObject')
del main_krb_rec['idnsTemplateAttribute;cnamerecord']
ldap.update_entry(main_krb_rec)
for location in locations:
location_krb_rec = ldap.get_entry(
get_location_krb_rec_dn(self.master.domain, location),
)
del location_krb_rec['tXTRecord']
ldap.update_entry(location_krb_rec)
yield _setup_locations
ldap = self.master.ldap_connect()
modified = False
main_krb_rec = ldap.get_entry(get_main_krb_rec_dn(self.master.domain))
if 'idnsTemplateObject' not in main_krb_rec['objectClass']:
main_krb_rec['objectClass'].append('idnsTemplateObject')
modified = True
if 'idnsTemplateAttribute;cnamerecord' not in main_krb_rec:
main_krb_rec['idnsTemplateAttribute;cnamerecord'] = \
'_kerberos.\\{substitutionvariable_ipalocation\\}._locations'
modified = True
if modified:
ldap.update_entry(main_krb_rec)
for location in _locations:
location_krb_rec = ldap.get_entry(
get_location_krb_rec_dn(self.master.domain, location),
)
if 'tXTRecord' not in location_krb_rec:
location_krb_rec['tXTRecord'] = f'"{realm}"'
ldap.update_entry(location_krb_rec)
self.master.run_command([
'ipa',
'server-mod',
'--location=',
self.master.hostname,
])
for location in _locations:
self.master.run_command(['ipa', 'location-del', location])
def test_invoke_upgrader(self):
cmd = self.master.run_command(['ipa-server-upgrade'],
raiseonerr=False)
@ -334,3 +420,38 @@ class TestUpgrade(IntegrationTest):
assert "False" in result.stdout_text
finally:
self.master.run_command(["rmdir", kra_path])
def test_krb_uri_txt_to_cname(self, setup_locations):
"""Test that ipa-server-upgrade correctly updates Kerberos DNS records
Test for https://pagure.io/freeipa/issue/9257
Kerberos URI and TXT DNS records should be location-aware in case the
server is part of a location, in order for DNS discovery to prioritize
servers from the same location. This means that for such servers the
_kerberos record should be a CNAME one pointing to the appropriate set
of location-aware records.
"""
realm = self.master.domain.realm
locations = ['a', 'b']
setup_locations(locations)
self.master.run_command(['ipa-server-upgrade'])
ldap = self.master.ldap_connect()
main_krb_rec = ldap.get_entry(
get_main_krb_rec_dn(self.master.domain),
)
assert 'idnsTemplateObject' in main_krb_rec['objectClass']
assert len(main_krb_rec['idnsTemplateAttribute;cnamerecord']) == 1
assert main_krb_rec['idnsTemplateAttribute;cnamerecord'][0] \
== '_kerberos.\\{substitutionvariable_ipalocation\\}._locations'
for location in locations:
location_krb_rec = ldap.get_entry(
get_location_krb_rec_dn(self.master.domain, location),
)
assert 'tXTRecord' in location_krb_rec
assert len(location_krb_rec['tXTRecord']) == 1
assert location_krb_rec['tXTRecord'][0] == f'"{realm}"'