From 082702eb7fcbc2ecb2e716a83d5efa5a15e15112 Mon Sep 17 00:00:00 2001 From: Andrew Walker Date: Wed, 15 Jan 2025 13:05:12 -0600 Subject: [PATCH] Fix domain join in Samba 4.21 Various winbind / libads parameters and commands changed in samba 4.21 and so we need to adjust. --- .../middlewared/etc_files/krb5.keytab.py | 65 ++----------------- .../activedirectory_join_mixin.py | 39 +++++------ .../middlewared/plugins/kerberos.py | 12 ++-- .../middlewared/plugins/smb_/util_smbconf.py | 9 ++- .../utils/directoryservices/krb5.py | 31 +++++++++ .../utils/directoryservices/krb5_constants.py | 1 + 6 files changed, 72 insertions(+), 85 deletions(-) diff --git a/src/middlewared/middlewared/etc_files/krb5.keytab.py b/src/middlewared/middlewared/etc_files/krb5.keytab.py index b084cc810ea8e..6ea8a2f61b696 100644 --- a/src/middlewared/middlewared/etc_files/krb5.keytab.py +++ b/src/middlewared/middlewared/etc_files/krb5.keytab.py @@ -1,67 +1,10 @@ -import logging -import os -import base64 -import subprocess -import stat - -from contextlib import suppress - -logger = logging.getLogger(__name__) -kdir = "/etc/kerberos" -keytabfile = "/etc/krb5.keytab" -unified_keytab = os.path.join(kdir, 'tmp_keytab') - - -def mit_copy(temp_keytab): - kt_copy = subprocess.run( - ['ktutil'], - input=f'rkt {temp_keytab}\nwkt {unified_keytab}'.encode(), - capture_output=True - ) - if kt_copy.stderr: - logger.error("%s: failed to add to uinified keytab: %s", - temp_keytab, kt_copy.stderr.decode()) - - -def write_keytab(db_keytabname, db_keytabfile): - dirfd = None - - def opener(path, flags): - return os.open(path, flags, mode=0o600, dir_fd=dirfd) - - with suppress(FileExistsError): - os.mkdir(kdir, mode=0o700) - - try: - dirfd = os.open(kdir, os.O_DIRECTORY) - st = os.fstat(dirfd) - if stat.S_IMODE(st.st_mode) != 0o700: - os.fchmod(dirfd, 0o700) - - with open(db_keytabname, "wb", opener=opener) as f: - f.write(db_keytabfile) - kt_name = os.readlink(f'/proc/self/fd/{f.fileno()}') - - mit_copy(kt_name) - os.remove(db_keytabname, dir_fd=dirfd) - - finally: - os.close(dirfd) +from base64 import b64decode +from middlewared.utils.directoryservices import krb5 def render(service, middleware, render_ctx): - keytabs = middleware.call_sync('kerberos.keytab.query') + keytabs = [b64decode(x['file']) for x in middleware.call_sync('kerberos.keytab.query')] if not keytabs: - logger.trace('No keytabs in configuration database, skipping keytab generation') return - for keytab in keytabs: - db_keytabfile = base64.b64decode(keytab['file'].encode()) - db_keytabname = f'keytab_{keytab["id"]}' - write_keytab(db_keytabname, db_keytabfile) - - with open(unified_keytab, 'rb') as f: - keytab_bytes = f.read() - - os.unlink(unified_keytab) - return keytab_bytes + return krb5.concatenate_keytab_data(keytabs) diff --git a/src/middlewared/middlewared/plugins/directoryservices_/activedirectory_join_mixin.py b/src/middlewared/middlewared/plugins/directoryservices_/activedirectory_join_mixin.py index 66f97261dd3a6..544e9c30c2ed7 100644 --- a/src/middlewared/middlewared/plugins/directoryservices_/activedirectory_join_mixin.py +++ b/src/middlewared/middlewared/plugins/directoryservices_/activedirectory_join_mixin.py @@ -174,22 +174,24 @@ def _ad_leave(self, job: Job, ds_type: DSType, domain: str): ) @kerberos_ticket - def _ad_set_spn(self): - cmd = [ - SMBCmd.NET.value, - '--use-kerberos', 'required', - '--use-krb5-ccache', krb5ccache.SYSTEM.value, - 'ads', 'keytab', - 'add_update_ads', 'nfs' - ] - - netads = subprocess.run(cmd, check=False, capture_output=True) - if netads.returncode != 0: - raise CallError( - 'Failed to set spn entry: ' - f'{netads.stdout.decode().strip()}' - ) + def _ad_set_spn(self, netbiosname, domainname): + def setspn(spn): + cmd = [ + SMBCmd.NET.value, + '--use-kerberos', 'required', + '--use-krb5-ccache', krb5ccache.SYSTEM.value, + 'ads', 'setspn', 'add', spn + ] + + netads = subprocess.run(cmd, check=False, capture_output=True) + if netads.returncode != 0: + raise CallError( + 'Failed to set spn entry: ' + f'{netads.stdout.decode().strip()}' + ) + setspn(f'nfs/{netbiosname.upper()}') + setspn(f'nfs/{netbiosname.upper()}.{domainname.lower()}') self.middleware.call_sync('kerberos.keytab.store_ad_keytab') @kerberos_ticket @@ -262,8 +264,8 @@ def _ad_grant_privileges(self) -> None: 'TrueNAS API.', exc_info=True ) - def _ad_post_join_actions(self, job: Job): - self._ad_set_spn() + def _ad_post_join_actions(self, job: Job, conf: dict): + self._ad_set_spn(conf['netbiosname'], conf['domainname']) # The password in secrets.tdb has been replaced so make # sure we have it backed up in our config. self.middleware.call_sync('directoryservices.secrets.backup') @@ -282,7 +284,6 @@ def _ad_join_impl(self, job: Job, conf: dict): SMBCmd.NET.value, '--use-kerberos', 'required', '--use-krb5-ccache', krb5ccache.SYSTEM.value, - '-U', conf['bindname'], '-d', '5', 'ads', 'join', ] @@ -303,7 +304,7 @@ def _ad_join_impl(self, job: Job, conf: dict): # operations try: job.set_progress(60, 'Performing post-join actions') - return self._ad_post_join_actions(job) + return self._ad_post_join_actions(job, conf) except KRB5Error: # if there's an actual unrecoverable kerberos error # in our post-join actions then leaving AD will also fail diff --git a/src/middlewared/middlewared/plugins/kerberos.py b/src/middlewared/middlewared/plugins/kerberos.py index c465230daeed0..a00428862e42f 100644 --- a/src/middlewared/middlewared/plugins/kerberos.py +++ b/src/middlewared/middlewared/plugins/kerberos.py @@ -20,8 +20,10 @@ KRB_ETYPE, KRB_TKT_CHECK_INTERVAL, PERSISTENT_KEYRING_PREFIX, + SAMBA_KEYTAB_DIR, ) from middlewared.utils.directoryservices.krb5 import ( + concatenate_keytab_data, gss_get_current_cred, gss_acquire_cred_principal, gss_acquire_cred_user, @@ -928,13 +930,15 @@ def store_ad_keytab(self): libads automatically generates a system keytab during domain join process. This method parses the system keytab and inserts as the AD_MACHINE_ACCOUNT keytab. """ - if not os.path.exists(KRB_Keytab.SYSTEM.value): - self.logger.warning('System keytab is missing. Unable to extract AD machine account keytab.') + samba_keytabs = [] + for file in os.listdir(SAMBA_KEYTAB_DIR): + samba_keytabs.append(extract_from_keytab(os.path.join(SAMBA_KEYTAB_DIR, file), [])) + + if not samba_keytabs: return ad = self.middleware.call_sync('activedirectory.config') - ad_kt_bytes = extract_from_keytab(KRB_Keytab.SYSTEM.value, [['principal', 'Crin', ad['netbiosname']]]) - keytab_file = base64.b64encode(ad_kt_bytes).decode() + keytab_file = base64.b64encode(concatenate_keytab_data(samba_keytabs)).decode() entry = self.middleware.call_sync('kerberos.keytab.query', [('name', '=', 'AD_MACHINE_ACCOUNT')]) if not entry: diff --git a/src/middlewared/middlewared/plugins/smb_/util_smbconf.py b/src/middlewared/middlewared/plugins/smb_/util_smbconf.py index 2dd7812e11769..9e6327e7675d9 100644 --- a/src/middlewared/middlewared/plugins/smb_/util_smbconf.py +++ b/src/middlewared/middlewared/plugins/smb_/util_smbconf.py @@ -4,6 +4,7 @@ from logging import getLogger from middlewared.utils import filter_list from middlewared.utils.directoryservices.constants import DSType +from middlewared.utils.directoryservices.krb5_constants import SAMBA_KEYTAB_DIR from middlewared.utils.filesystem.acl import FS_ACL_Type, path_get_acltype from middlewared.utils.io import get_io_uring_enabled from middlewared.utils.path import FSLocation, path_location @@ -28,6 +29,11 @@ "0x3e:0xf024,0x3f:0xf025,0x5c:0xf026,0x7c:0xf027" ) +AD_KEYTAB_PARAMS = ( + f"{SAMBA_KEYTAB_DIR}/krb5.keytab0:account_name:sync_kvno:machine_password", + f"{SAMBA_KEYTAB_DIR}/krb5.keytab1:sync_spns:sync_kvno:machine_password", + f"{SAMBA_KEYTAB_DIR}/krb5.keytab2:spn_prefixes=nfs:sync_kvno:machine_password" +) class TrueNASVfsObjects(enum.StrEnum): # Ordering here determines order in which objects entered into @@ -457,7 +463,8 @@ def generate_smb_conf_dict( ac = ds_config smbconf.update({ 'server role': 'member server', - 'kerberos method': 'secrets and keytab', + 'kerberos method': 'secrets only', + 'sync machine password to keytab': ' '.join(AD_KEYTAB_PARAMS), 'security': 'ADS', 'local master': False, 'domain master': False, diff --git a/src/middlewared/middlewared/utils/directoryservices/krb5.py b/src/middlewared/middlewared/utils/directoryservices/krb5.py index d10ee1f2955eb..848f2921320d1 100644 --- a/src/middlewared/middlewared/utils/directoryservices/krb5.py +++ b/src/middlewared/middlewared/utils/directoryservices/krb5.py @@ -13,6 +13,7 @@ import subprocess import time +from contextlib import contextmanager from .krb5_constants import krb_tkt_flag, krb5ccache, KRB_ETYPE, KRB_Keytab from middlewared.service_exception import CallError from middlewared.utils import filter_list @@ -108,6 +109,15 @@ def __tmp_krb5_keytab() -> str: return tmpfile.name +@contextmanager +def temporary_keytab() -> str: + kt = __tmp_krb5_keytab() + try: + yield kt + finally: + os.remove(kt) + + def parse_klist_output(klistbuf: str) -> list: """ This is an internal method that parses the output of `klist -ef` @@ -436,3 +446,24 @@ def extract_from_keytab( os.remove(tmp_keytab) return kt_bytes + + +def concatenate_keytab_data(keytab_data: list[bytes]) -> bytes: + with temporary_keytab() as unified: + for data in keytab_data: + with temporary_keytab() as kt: + with open(kt, 'wb') as f: + f.write(data) + f.flush() + + kt_copy = subprocess.run( + ['ktutil'], + input=f'rkt {kt}\nwkt {unified}'.encode(), + capture_output=True + ) + if kt_copy.stderr: + raise RuntimeError('Failed to concatenate keytabs: %s', + kt_copy.stderr.decode()) + + with open(unified, 'rb') as f: + return f.read() diff --git a/src/middlewared/middlewared/utils/directoryservices/krb5_constants.py b/src/middlewared/middlewared/utils/directoryservices/krb5_constants.py index 8d9a8c19715b4..9e72770fcb7ab 100644 --- a/src/middlewared/middlewared/utils/directoryservices/krb5_constants.py +++ b/src/middlewared/middlewared/utils/directoryservices/krb5_constants.py @@ -4,6 +4,7 @@ KRB_TKT_CHECK_INTERVAL = 1800 PERSISTENT_KEYRING_PREFIX = 'KEYRING:persistent:' +SAMBA_KEYTAB_DIR = '/etc/samba/keytabs' class KRB_Keytab(enum.Enum):