From c2f119c43a998cb1af1b898eb513052a7d668857 Mon Sep 17 00:00:00 2001 From: Andrew Walker Date: Wed, 13 Nov 2024 08:13:19 -0600 Subject: [PATCH] Improve handling for legacy IPA bind with kerberos principal Some users may have manually configured IPA integration by generating a keytab in IPA and uploading to our UI in 24.04 and earlier. Since there's no guarantee these keytabs were generated via proper IPA join, we default to legacy LDAP configuration, which is same as pre-24.10 and add an alert that we're basically running with a degraded capacity from what we could have. --- src/middlewared/middlewared/plugins/ldap.py | 27 +++++++++--- tests/api2/test_ipa_join.py | 46 +++++++++++++++++++++ 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/middlewared/middlewared/plugins/ldap.py b/src/middlewared/middlewared/plugins/ldap.py index 3aa5e3b89b78d..4e643838ea9c1 100644 --- a/src/middlewared/middlewared/plugins/ldap.py +++ b/src/middlewared/middlewared/plugins/ldap.py @@ -872,12 +872,25 @@ async def has_ipa_host_keytab(self): )) @private - async def ipa_kinit(self, ipa_conf, bindpw): + async def ipa_kinit(self, ipa_conf, ldap_conf): + if not ldap_conf['bindpw'] and ldap_conf['kerberos_principal']: + # If we already have a kerberos principal then we shouldn't perform + # an IPA join because it will potentially muck up our account in IPA. + # In this case we'll trigger the "Legacy IPA Configuration" alert and + # generate a warning message in logs. + errmsg = ( + 'LDAP kerberos principal is already populated, but was not generated ' + 'through the IPA join process. Domain functionality may be reduced and ' + 'is undefined from the perspective of the TrueNAS backend.' + ) + self.logger.warning(errmsg) + raise CallError(errmsg, errno.EEXIST) + princ = f'{ipa_conf["username"]}@{ipa_conf["realm"]}' await self.middleware.call('kerberos.do_kinit', { 'krb5_cred': { 'username': princ, - 'password': bindpw + 'password': ldap_conf['bindpw'] }, 'kinit-options': { 'kdc_override': { @@ -909,7 +922,7 @@ async def __start(self, job, ds_type): if ds_type is DSType.IPA and not await self.has_ipa_host_keytab(): ipa_config = await self.ipa_config(ldap) try: - await self.ipa_kinit(ipa_config, ldap['bindpw']) + await self.ipa_kinit(ipa_config, ldap) dom_join_resp = await job.wrap(await self.middleware.call( 'directoryservices.connection.join_domain', 'IPA', ipa_config['domain'] )) @@ -930,7 +943,7 @@ async def __start(self, job, ds_type): # We may have a kerberos error encapsulated in CallError due to translation from job results # In this case we also want to fall back to using legacy LDAP client compatibility. # We will expand this whitelist as we determine there are more somewhat-recoverable KRB5 errors. - if not err.err_msg.startswith('[KRB5_REALM_UNKNOWN]'): + if not err.errmsg.startswith('[KRB5_REALM_UNKNOWN]') and err.errno != errno.EEXIST: raise err await self.middleware.call( @@ -956,7 +969,11 @@ async def __start(self, job, ds_type): ) case DomainJoinResponse.ALREADY_JOINED.value: cache_job_id = await self.middleware.call('directoryservices.connection.activate') - await job.wrap(await self.middleware.call('core.job_wait', cache_job_id)) + try: + await job.wrap(await self.middleware.call('core.job_wait', cache_job_id)) + except Exception: + self.logger.warning('Failed to build user/group cache', exc_info=True) + # Change state to HEALTHY before performing final health check await self.middleware.call('directoryservices.health.set_state', ds_type.value, DSStatus.HEALTHY.name) # Force health check so that user gets immediate feedback if something diff --git a/tests/api2/test_ipa_join.py b/tests/api2/test_ipa_join.py index 44026cf75eeb6..98ac354ae1151 100644 --- a/tests/api2/test_ipa_join.py +++ b/tests/api2/test_ipa_join.py @@ -1,5 +1,6 @@ import pytest +from contextlib import contextmanager from middlewared.test.integration.assets.directory_service import ipa, FREEIPA_ADMIN_BINDPW from middlewared.test.integration.assets.product import product_type from middlewared.test.integration.utils import call, client @@ -30,6 +31,34 @@ def enable_ds_auth(override_product): call('system.general.update', {'ds_auth': False}) +@contextmanager +def switch_to_legacy_bind(): + kt_id = call('kerberos.keytab.query', [['name', '=', 'IPA_MACHINE_ACCOUNT']], {'get': True})['id'] + call('kerberos.keytab.update', kt_id, {'name': 'TMP_IPA_MACHINE_ACCOUNT'}) + + try: + yield + finally: + call('kerberos.keytab.update', kt_id, {'name': 'IPA_MACHINE_ACCOUNT'}) + + +@contextmanager +def toggle_ldap(ldap_conf, enable): + payload = { + 'hostname': ldap_conf['hostname'], + 'validate_certificates': ldap_conf['validate_certificates'], + 'enable': enable + } + + call('ldap.update', payload, job=True) + + try: + yield + finally: + payload['enable'] = not enable + call('ldap.update', payload, job=True) + + def test_setup_and_enabling_freeipa(do_freeipa_connection): config = do_freeipa_connection @@ -114,3 +143,20 @@ def test_dns_resolution(do_freeipa_connection): addresses = call('dnsclient.forward_lookup', {'names': [ipa_config['host']]}) assert len(addresses) != 0 + + +def test_ldap_bind_legacy_kerberos_principal(do_freeipa_connection): + """ + Do proper IPA join to get kerberos principal, + Disable LDAP + Rename keytab entry so that we switch to legacy bind type + Re-enable LDAP and verify dstype is LDAP and not IPA + Then roll back changes so that we can cleanly leave the IPA domain + """ + config = do_freeipa_connection + with toggle_ldap(config, False): + with switch_to_legacy_bind(): + with toggle_ldap(config, True): + ds = call('directoryservices.status') + assert ds['type'] == 'LDAP' + assert ds['status'] == 'HEALTHY'