Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NAS-132212 / 25.04 / Improve handling for legacy IPA bind with kerberos principal #14996

Merged
merged 1 commit into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions src/middlewared/middlewared/plugins/ldap.py
Original file line number Diff line number Diff line change
Expand Up @@ -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': {
Expand Down Expand Up @@ -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']
))
Expand All @@ -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(
Expand All @@ -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
Expand Down
46 changes: 46 additions & 0 deletions tests/api2/test_ipa_join.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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'
Loading