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-133586 / 25.04 / Fix domain join in Samba 4.21 #15402

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
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
65 changes: 4 additions & 61 deletions src/middlewared/middlewared/etc_files/krb5.keytab.py
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be renamed to _ad_set_nfs_spns ?

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
Expand Down Expand Up @@ -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')
Expand All @@ -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',
]
Expand All @@ -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
Expand Down
12 changes: 8 additions & 4 deletions src/middlewared/middlewared/plugins/kerberos.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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:
Expand Down
9 changes: 8 additions & 1 deletion src/middlewared/middlewared/plugins/smb_/util_smbconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The obligatory flake8 comment: Need an extra blank line between AD_KEYTABS_PARAMS and class TrueNASVfsObjects

# Ordering here determines order in which objects entered into
Expand Down Expand Up @@ -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,
Expand Down
31 changes: 31 additions & 0 deletions src/middlewared/middlewared/utils/directoryservices/krb5.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -108,6 +109,15 @@ def __tmp_krb5_keytab() -> str:
return tmpfile.name


@contextmanager
def temporary_keytab() -> str:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Flake8 doesn't like the str as a return type:
Return type of generator function must be compatible with "Generator[Any, Any, Any]"   "Generator[Any, Any, Any]" is not assignable to "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`
Expand Down Expand Up @@ -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()
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
Loading