mirror of
https://salsa.debian.org/freeipa-team/freeipa.git
synced 2025-02-25 18:55:28 -06:00
ipa-client-samba: a tool to configure Samba domain member on IPA client
Introduces new utility to configure Samba on an IPA domain member. The tool sets up Samba configuration and internal databases, creates cifs/... Kerberos service and makes sure that a keytab for this service contains the key with the same randomly generated password that is set in the internal Samba databases. Samba configuration is created by querying an IPA master about details of trust to Active Directory configuration. All known identity ranges added to the configuration to allow Samba to properly handle them (read-only) via idmap_sss. Resulting configuration allows connection with both NTLMSSP and Kerberos authentication for IPA users. Access controls for the shared content should be set by utilizing POSIX ACLs on the file system under a specific share. The utility is packaged as freeipa-client-samba package to allow pulling in all required dependencies for Samba and cifs.ko (smb3.ko) kernel module. This allows an IPA client to become both an SMB server and an SMB client. Fixes: https://pagure.io/freeipa/issue/3999 Signed-off-by: Alexander Bokovoy <abokovoy@redhat.com> Reviewed-By: Rob Crittenden <rcritten@redhat.com> Reviewed-By: Christian Heimes <cheimes@redhat.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -127,6 +127,7 @@ makeapi
|
||||
client/ipa-certupdate
|
||||
client/ipa-client-automount
|
||||
client/ipa-client-install
|
||||
client/ipa-client-samba
|
||||
daemons/dnssec/ipa-dnskeysyncd
|
||||
daemons/dnssec/ipa-dnskeysync-replica
|
||||
daemons/dnssec/ipa-ods-exporter
|
||||
|
@@ -43,6 +43,7 @@ sbin_SCRIPTS = \
|
||||
ipa-certupdate \
|
||||
ipa-client-automount \
|
||||
ipa-client-install \
|
||||
ipa-client-samba \
|
||||
$(NULL)
|
||||
|
||||
ipa_getkeytab_SOURCES = \
|
||||
@@ -102,6 +103,7 @@ EXTRA_DIST = \
|
||||
ipa-certupdate.in \
|
||||
ipa-client-automount.in \
|
||||
ipa-client-install.in \
|
||||
ipa-client-samba.in \
|
||||
$(NULL)
|
||||
|
||||
install-data-hook:
|
||||
|
21
client/ipa-client-samba.in
Executable file
21
client/ipa-client-samba.in
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/usr/bin/python3
|
||||
#
|
||||
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
# Configure the Samba suite to operate as domain member in IPA domain
|
||||
|
||||
import os
|
||||
import sys
|
||||
from ipaclient.install import ipa_client_samba
|
||||
|
||||
try:
|
||||
if not os.geteuid() == 0:
|
||||
sys.exit("\nMust be run as root\n")
|
||||
|
||||
sys.exit(ipa_client_samba.run())
|
||||
except SystemExit as e:
|
||||
sys.exit(e)
|
||||
except RuntimeError as e:
|
||||
sys.exit(e)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
sys.exit(1)
|
@@ -7,6 +7,7 @@ dist_man1_MANS = \
|
||||
ipa-rmkeytab.1 \
|
||||
ipa-client-install.1 \
|
||||
ipa-client-automount.1 \
|
||||
ipa-client-samba.1 \
|
||||
ipa-certupdate.1 \
|
||||
ipa-join.1 \
|
||||
ipa.1
|
||||
|
88
client/man/ipa-client-samba.1
Normal file
88
client/man/ipa-client-samba.1
Normal file
@@ -0,0 +1,88 @@
|
||||
.\" A man page for ipa-client-samba
|
||||
.\" Copyright (C) 2008-2016 FreeIPA Contributors see COPYING for license
|
||||
.\"
|
||||
.TH "ipa-client-samba" "1" "Jun 10 2019" "FreeIPA" "FreeIPA Manual Pages"
|
||||
.SH "NAME"
|
||||
ipa\-client\-samba \- Configure Samba file server on an IPA client
|
||||
.SH "SYNOPSIS"
|
||||
ipa\-client\-samba [\fIOPTION\fR]...
|
||||
.SH "DESCRIPTION"
|
||||
Configures a Samba file server on the client machine to use IPA domain controller for authentication and identity services.
|
||||
|
||||
The tool configures Samba file server to be a domain member of IPA domain. Samba file server will use SSSD to resolve information about users and groups, and will use IPA master it is enrolled against as its domain controller.
|
||||
|
||||
It is not possible to reconciliate original Samba environment if that was pre-existing on the client with new configuration. Samba databases will be updated to follow IPA domain details and \fBsmb.conf\fR configuration will will be overwritten. It is recommended to enable Samba suite on a freshly deployed IPA client.
|
||||
|
||||
.TP
|
||||
During the configuration process, the tool will perform following steps:
|
||||
|
||||
1. Discover details of IPA domain: realm, domain SID, domain ID range
|
||||
|
||||
2. Discover details of trusted Actvide Directory domains: domain name, domain SID, domain ID range
|
||||
|
||||
3. Create Samba configuration file using the details discovered above.
|
||||
|
||||
4. Create Samba Kerberos service using host credentials and fetch its keytab into /etc/samba/samba.keytab. The Kerberos service key is pre-set to a randomly generated value that is shared with Samba.
|
||||
|
||||
5. Populate Samba databases by setting the domain details and the randomly generated machine account password from the previous step.
|
||||
|
||||
6. Create a default [homes] share to allow users to log in to their home directories unless \-\-no\-homes option was specified.
|
||||
|
||||
.TP
|
||||
The tool does not start nor does it enable Samba file services after the configuration. In order to enable and start Samba file services, one needs to enable both \fBsmb.service\fR and \fBwinbind.service\fR system services. Please check that \fB/etc/samba/smb.conf\fR contains all settings for your use case as starting Samba service will make identity mapping details written into the Samba databases. To enable and start Samba file services at the same time one can use \fBsystemctl enable \-\-now\fR command:
|
||||
|
||||
systemctl enable --now smb winbind
|
||||
|
||||
.SS "Assumptions"
|
||||
The ipa\-client\-samba script assumes that the machine has alreaby been enrolled into IPA.
|
||||
|
||||
.SS "IPA Master Requirements"
|
||||
At least one IPA master must hold a \fBTrust Controller\fR role. This can be achieved by running ipa\-adtrust\-install on the IPA master. The utility will configure IPA master to be a domain controller for IPA domain.
|
||||
|
||||
IPA master holding a \fBTrust Controller\fR role has also to have support for a special service command to create SMB service, \fBipa service-add-smb\fR. This command is available with FreeIPA 4.8.0 or later release.
|
||||
|
||||
.SH "OPTIONS"
|
||||
.SS "BASIC OPTIONS"
|
||||
.TP
|
||||
\fB\-\-server\fR=\fISERVER\fR
|
||||
Set the FQDN of the IPA server to connect to. Under normal circumstances, this option is not needed as the server to use is discovered automatically.
|
||||
.TP
|
||||
\fB\-\-no\-homes\fR
|
||||
Do not configure a \fB[homes]\fR share by default to allow users to access their home directories.
|
||||
.TP
|
||||
\fB\-\-no\-nfs\fR
|
||||
Do not enable SELinux booleans to allow Samba to re-share NFS shares.
|
||||
.TP
|
||||
\fB\-\-netbios-name\fR=\fINETBIOS_NAME\fR
|
||||
NetBIOS name of this machine. If not provided then this is determined based on the leading component of the hostname.
|
||||
.TP
|
||||
\fB\-d\fR, \fB\-\-debug\fR
|
||||
Print debugging information to stdout
|
||||
.TP
|
||||
\fB\-U\fR, \fB\-\-unattended\fR
|
||||
Unattended installation. The user will not be prompted.
|
||||
.TP
|
||||
\fB\-\-uninstall\fR
|
||||
Revert Samba suite configuration changes and remove SMB service principal. It is not possible to preserve original Samba configuration: while \fBsmb.conf\fR configuration file will be restored, various Samba databases would not be restored. In general, it is not possible to restore full original Samba environment.
|
||||
.TP
|
||||
\fB\-\-force\fR
|
||||
Force through the installation steps even if they were done before
|
||||
|
||||
.SH "FILES"
|
||||
.TP
|
||||
Files that will be replaced if Samba is configured:
|
||||
|
||||
/etc/samba/smb.conf
|
||||
.br
|
||||
/etc/samba/samba.keytab
|
||||
|
||||
.SH "EXIT STATUS"
|
||||
0 if the installation was successful
|
||||
|
||||
1 if an error occurred
|
||||
|
||||
.SH "SEE ALSO"
|
||||
.BR smb.conf(5),
|
||||
.BR krb5.conf(5),
|
||||
.BR sssd.conf(5),
|
||||
.BR systemctl(1)
|
@@ -520,6 +520,22 @@ If your network uses IPA for authentication, this package should be
|
||||
installed on every client machine.
|
||||
This package provides command-line tools for IPA administrators.
|
||||
|
||||
%package client-samba
|
||||
Summary: Tools to configure Samba on IPA client
|
||||
Group: System Environment/Base
|
||||
Requires: %{name}-client = %{version}-%{release}
|
||||
Requires: python3-samba
|
||||
Requires: samba-client
|
||||
Requires: samba-winbind
|
||||
Requires: samba-common-tools
|
||||
Requires: samba
|
||||
Requires: sssd-winbind-idmap
|
||||
Requires: tdb-tools
|
||||
Requires: cifs-utils
|
||||
|
||||
%description client-samba
|
||||
This package provides command-line tools to deploy Samba domain member
|
||||
on the machine enrolled into a FreeIPA environment
|
||||
|
||||
%package -n python3-ipaclient
|
||||
Summary: Python libraries used by IPA client
|
||||
@@ -1207,6 +1223,11 @@ fi
|
||||
%{_mandir}/man1/ipa-certupdate.1*
|
||||
%{_mandir}/man1/ipa-join.1*
|
||||
|
||||
%files client-samba
|
||||
%doc README.md Contributors.txt
|
||||
%license COPYING
|
||||
%{_sbindir}/ipa-client-samba
|
||||
%{_mandir}/man1/ipa-client-samba.1*
|
||||
|
||||
%files -n python3-ipaclient
|
||||
%doc README.md Contributors.txt
|
||||
|
745
ipaclient/install/ipa_client_samba.py
Executable file
745
ipaclient/install/ipa_client_samba.py
Executable file
@@ -0,0 +1,745 @@
|
||||
#
|
||||
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
# Configure the Samba suite to operate as domain member in IPA domain
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import logging
|
||||
import os
|
||||
import gssapi
|
||||
from urllib.parse import urlsplit
|
||||
from optparse import OptionParser # pylint: disable=deprecated-module
|
||||
from contextlib import contextmanager
|
||||
|
||||
from ipaclient import discovery
|
||||
from ipaclient.install.client import (
|
||||
CLIENT_NOT_CONFIGURED,
|
||||
CLIENT_ALREADY_CONFIGURED,
|
||||
)
|
||||
from ipalib import api, errors
|
||||
from ipalib.install import sysrestore
|
||||
from ipalib.util import check_client_configuration
|
||||
from ipalib.request import context
|
||||
from ipapython import ipautil
|
||||
from ipapython.errors import SetseboolError
|
||||
from ipapython.ipa_log_manager import standard_logging_setup
|
||||
from ipapython.dnsutil import DNSName
|
||||
from ipaplatform.tasks import tasks
|
||||
from ipaplatform.paths import paths
|
||||
from ipaplatform.constants import constants
|
||||
from ipaplatform import services
|
||||
from ipapython.admintool import ScriptError
|
||||
from samba import generate_random_password
|
||||
|
||||
logger = logging.getLogger(os.path.basename(__file__))
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def use_api_as_principal(principal, keytab):
|
||||
with ipautil.private_ccache() as ccache_file:
|
||||
try:
|
||||
old_principal = getattr(context, "principal", None)
|
||||
name = gssapi.Name(principal, gssapi.NameType.kerberos_principal)
|
||||
store = {"ccache": ccache_file, "client_keytab": keytab}
|
||||
gssapi.Credentials(name=name, usage="initiate", store=store)
|
||||
# Finalize API when TGT obtained using host keytab exists
|
||||
if not api.isdone("finalize"):
|
||||
api.finalize()
|
||||
|
||||
# Now we have a TGT, connect to IPA
|
||||
try:
|
||||
if api.Backend.rpcclient.isconnected():
|
||||
api.Backend.rpcclient.disconnect()
|
||||
api.Backend.rpcclient.connect()
|
||||
|
||||
yield
|
||||
except gssapi.exceptions.GSSError as e:
|
||||
raise Exception(
|
||||
"Unable to bind to IPA server. Error initializing "
|
||||
"principal %s in %s: %s" % (principal, keytab, str(e))
|
||||
)
|
||||
finally:
|
||||
if api.Backend.rpcclient.isconnected():
|
||||
api.Backend.rpcclient.disconnect()
|
||||
setattr(context, "principal", old_principal)
|
||||
|
||||
|
||||
def parse_options():
|
||||
usage = "%prog [options]\n"
|
||||
parser = OptionParser(usage=usage)
|
||||
parser.add_option(
|
||||
"--server",
|
||||
dest="server",
|
||||
help="FQDN of IPA server to connect to",
|
||||
)
|
||||
parser.add_option(
|
||||
"--netbios-name",
|
||||
dest="netbiosname",
|
||||
help="NetBIOS name of this machine",
|
||||
default=None,
|
||||
)
|
||||
parser.add_option(
|
||||
"--no-homes",
|
||||
dest="no_homes",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Do not add [homes] share to the generated Samba configuration",
|
||||
)
|
||||
parser.add_option(
|
||||
"--no-nfs",
|
||||
dest="no_nfs",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Do not allow NFS integration (SELinux booleans)",
|
||||
)
|
||||
parser.add_option(
|
||||
"--force",
|
||||
dest="force",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="force installation by redoing all steps",
|
||||
)
|
||||
parser.add_option(
|
||||
"--debug",
|
||||
dest="debug",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="print debugging information",
|
||||
)
|
||||
parser.add_option(
|
||||
"-U",
|
||||
"--unattended",
|
||||
dest="unattended",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="unattended installation never prompts the user",
|
||||
)
|
||||
parser.add_option(
|
||||
"--uninstall",
|
||||
dest="uninstall",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Revert configuration and remove SMB service",
|
||||
)
|
||||
|
||||
options, args = parser.parse_args()
|
||||
return options, args
|
||||
|
||||
|
||||
domain_information_template = """
|
||||
Domain name: {domain_name}
|
||||
NetBIOS name: {netbios_name}
|
||||
SID: {domain_sid}
|
||||
ID range: {range_id_min} - {range_id_max}
|
||||
"""
|
||||
|
||||
|
||||
def pretty_print_domain_information(info):
|
||||
result = []
|
||||
for domain in info:
|
||||
result.append(domain_information_template.format(**domain))
|
||||
return "\n".join(result)
|
||||
|
||||
|
||||
trust_keymap = {
|
||||
"netbios_name": "ipantflatname",
|
||||
"domain_sid": "ipantsecurityidentifier",
|
||||
"domain_name": "cn",
|
||||
}
|
||||
|
||||
|
||||
def retrieve_domain_information(api):
|
||||
# Pull down default domain configuration
|
||||
# IPA master might be missing freeipa-server-trust-ad package
|
||||
# or `ipa-adtrust-install` was never run. In such case return
|
||||
# empty list to report an error
|
||||
try:
|
||||
tc_command = api.Command.trustconfig_show
|
||||
except AttributeError:
|
||||
return []
|
||||
try:
|
||||
result = tc_command()["result"]
|
||||
except errors.PublicError:
|
||||
return []
|
||||
|
||||
l_domain = dict()
|
||||
for key in trust_keymap:
|
||||
l_domain[key] = result.get(trust_keymap[key], [None])[0]
|
||||
|
||||
# Pull down ID range and other details of our domain
|
||||
#
|
||||
# TODO: make clear how to handle multiple ID ranges for ipa-local range
|
||||
# In Samba only one range can belong to the same idmap domain,
|
||||
# otherwise winbindd's _wbint_Sids2UnixIDs function will not be able
|
||||
# to accept that a mapped Unix ID belongs to the specified domain
|
||||
idrange_local = "{realm}_id_range".format(realm=api.env.realm)
|
||||
result = api.Command.idrange_show(idrange_local)["result"]
|
||||
l_domain["range_id_min"] = int(result["ipabaseid"][0])
|
||||
l_domain["range_id_max"] = (
|
||||
int(result["ipabaseid"][0]) + int(result["ipaidrangesize"][0]) - 1
|
||||
)
|
||||
|
||||
domains = [l_domain]
|
||||
|
||||
# Retrieve list of trusted domains, if they exist
|
||||
#
|
||||
# We flatten the whole trust list because it should be non-overlapping
|
||||
result = api.Command.trust_find()["result"]
|
||||
for forest in result:
|
||||
r = api.Command.trustdomain_find(forest["cn"][0], all=True, raw=True)[
|
||||
"result"
|
||||
]
|
||||
# We don't need to process forest root info separately
|
||||
# as trustdomain_find() returns it as well
|
||||
for dom in r:
|
||||
r_dom = dict()
|
||||
for key in trust_keymap:
|
||||
r_dom[key] = dom.get(trust_keymap[key], [None])[0]
|
||||
|
||||
r_idrange_name = "{realm}_id_range".format(
|
||||
realm=r_dom["domain_name"].upper()
|
||||
)
|
||||
|
||||
# TODO: support ipa-ad-trust-posix range as well
|
||||
r_idrange = api.Command.idrange_show(r_idrange_name)["result"]
|
||||
r_dom["range_id_min"] = int(r_idrange["ipabaseid"][0])
|
||||
r_dom["range_id_max"] = (
|
||||
int(r_idrange["ipabaseid"][0]) +
|
||||
int(r_idrange["ipaidrangesize"][0]) - 1
|
||||
)
|
||||
domains.append(r_dom)
|
||||
return domains
|
||||
|
||||
|
||||
smb_conf_template = """
|
||||
[global]
|
||||
# Limit number of forked processes to avoid SMBLoris attack
|
||||
max smbd processes = 1000
|
||||
# Use dedicated Samba keytab. The key there must be synchronized
|
||||
# with Samba tdb databases or nothing will work
|
||||
dedicated keytab file = FILE:${samba_keytab}
|
||||
kerberos method = dedicated keytab
|
||||
# Set up logging per machine and Samba process
|
||||
log file = /var/log/samba/log.%m
|
||||
log level = 1
|
||||
# We force 'member server' role to allow winbind automatically
|
||||
# discover what is supported by the domain controller side
|
||||
server role = member server
|
||||
realm = ${realm}
|
||||
netbios name = ${machine_name}
|
||||
workgroup = ${netbios_name}
|
||||
# Local writable range for IDs not coming from IPA or trusted domains
|
||||
idmap config * : range = 0 - 0
|
||||
idmap config * : backend = tdb
|
||||
"""
|
||||
|
||||
idmap_conf_domain_snippet = """
|
||||
idmap config ${netbios_name} : range = ${range_id_min} - ${range_id_max}
|
||||
idmap config ${netbios_name} : backend = sss
|
||||
"""
|
||||
|
||||
homes_conf_snippet = """
|
||||
# Default homes share
|
||||
[homes]
|
||||
read only = no
|
||||
"""
|
||||
|
||||
|
||||
def configure_smb_conf(fstore, statestore, options, domains):
|
||||
sub_dict = {
|
||||
"samba_keytab": paths.SAMBA_KEYTAB,
|
||||
"realm": api.env.realm,
|
||||
"machine_name": options.netbiosname,
|
||||
}
|
||||
|
||||
# First domain in the list is ours, pull our domain name from there
|
||||
sub_dict["netbios_name"] = domains[0]["netbios_name"]
|
||||
|
||||
# Construct elements of smb.conf by pre-rendering idmap configuration
|
||||
template = [smb_conf_template]
|
||||
for dom in domains:
|
||||
template.extend([ipautil.template_str(idmap_conf_domain_snippet, dom)])
|
||||
|
||||
# Add default homes share so that users can log into Samba
|
||||
if not options.no_homes:
|
||||
template.extend([homes_conf_snippet])
|
||||
|
||||
fstore.backup_file(paths.SMB_CONF)
|
||||
with open(paths.SMB_CONF, "w") as f:
|
||||
f.write(ipautil.template_str("\n".join(template), sub_dict))
|
||||
tasks.restore_context(paths.SMB_CONF)
|
||||
|
||||
|
||||
def generate_smb_machine_account(fstore, statestore, options, domain):
|
||||
# Ideally, we should be using generate_random_machine_password()
|
||||
# from samba but it uses munged UTF-16 which is not decodable
|
||||
# by the code called from 'net changesecretpw -f'. Thus, we'd limit
|
||||
# password to ASCII only.
|
||||
return generate_random_password(128, 255)
|
||||
|
||||
|
||||
def retrieve_service_principal(
|
||||
fstore, statestore, options, domain, principal, password
|
||||
):
|
||||
# Use explicit encryption types. SMB service must have arcfour-hmac
|
||||
# generated to allow domain member to authenticate to the domain controller
|
||||
args = [
|
||||
paths.IPA_GETKEYTAB,
|
||||
"-p",
|
||||
principal,
|
||||
"-k",
|
||||
paths.SAMBA_KEYTAB,
|
||||
"-P",
|
||||
"-e",
|
||||
"aes128-cts-hmac-sha1-96,aes256-cts-hmac-sha1-96,arcfour-hmac",
|
||||
]
|
||||
try:
|
||||
ipautil.run(args, stdin=password + "\n" + password, encoding="utf-8")
|
||||
except ipautil.CalledProcessError as e:
|
||||
logger.error(
|
||||
"Cannot set machine account password at IPA DC. Error: %s",
|
||||
e,
|
||||
)
|
||||
raise
|
||||
|
||||
# Once we fetched the keytab, we also need to set ipaNTHash attribute
|
||||
# Use ipa-pwd-extop plugin to regenerate it from the Kerberos key
|
||||
value = "ipaNTHash=MagicRegen"
|
||||
try:
|
||||
api.Command.service_mod(principal, addattr=value)
|
||||
except errors.PublicError as e:
|
||||
logger.error(
|
||||
"Cannot update %s principal NT hash value due to an error: %s",
|
||||
principal,
|
||||
e,
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
def populate_samba_databases(fstore, statestore, options, domain, password):
|
||||
# First, set domain SID in Samba
|
||||
args = [paths.NET, "setdomainsid", domain["domain_sid"]]
|
||||
try:
|
||||
ipautil.run(args)
|
||||
except ipautil.CalledProcessError as e:
|
||||
logger.error("Cannot set domain SID in Samba. Error: %s", e)
|
||||
raise
|
||||
|
||||
# Next, make sure we can set machine account credentials
|
||||
# the workaround with tdbtool is temporary until 'net' utility
|
||||
# will not provide us a way to perform 'offline join' procedure
|
||||
secrets_key = "SECRETS/MACHINE_LAST_CHANGE_TIME/{}".format(
|
||||
domain["netbios_name"]
|
||||
)
|
||||
args = [paths.TDBTOOL, paths.SECRETS_TDB, "store", secrets_key, "2\\00"]
|
||||
try:
|
||||
ipautil.run(args)
|
||||
except ipautil.CalledProcessError as e:
|
||||
logger.error(
|
||||
"Cannot prepare machine account creds in Samba. Error: %s", e,
|
||||
)
|
||||
raise
|
||||
|
||||
secrets_key = "SECRETS/MACHINE_PASSWORD/{}".format(domain["netbios_name"])
|
||||
args = [paths.TDBTOOL, paths.SECRETS_TDB, "store", secrets_key, "2\\00"]
|
||||
try:
|
||||
ipautil.run(args)
|
||||
except ipautil.CalledProcessError as e:
|
||||
logger.error(
|
||||
"Cannot prepare machine account creds in Samba. Error: %s", e,
|
||||
)
|
||||
raise
|
||||
|
||||
# Finally, set actual machine account's password
|
||||
args = [paths.NET, "changesecretpw", "-f"]
|
||||
try:
|
||||
ipautil.run(args, stdin=password, encoding="utf-8")
|
||||
except ipautil.CalledProcessError as e:
|
||||
logger.error(
|
||||
"Cannot set machine account creds in Samba. Error: %s", e,
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
def configure_default_groupmap(fstore, statestore, options, domain):
|
||||
args = [
|
||||
paths.NET,
|
||||
"groupmap",
|
||||
"add",
|
||||
"sid=S-1-5-32-546",
|
||||
"unixgroup=nobody",
|
||||
"type=builtin",
|
||||
]
|
||||
|
||||
logger.info("Map BUILTIN\\Guests to a group 'nobody'")
|
||||
try:
|
||||
ipautil.run(args)
|
||||
except ipautil.CalledProcessError as e:
|
||||
if "already mapped to SID S-1-5-32-546" not in e.stdout:
|
||||
logger.error(
|
||||
'Cannot map BUILTIN\\Guests to a group "nobody". Error: %s',
|
||||
e
|
||||
)
|
||||
raise
|
||||
|
||||
|
||||
def set_selinux_booleans(booleans, statestore, backup=True):
|
||||
def default_backup_func(name, value):
|
||||
statestore.backup_state("selinux", name, value)
|
||||
|
||||
backup_func = default_backup_func if backup else None
|
||||
try:
|
||||
tasks.set_selinux_booleans(booleans, backup_func=backup_func)
|
||||
except SetseboolError as e:
|
||||
print("WARNING: " + str(e))
|
||||
logger.info("WARNING: %s", e)
|
||||
|
||||
|
||||
def harden_configuration(fstore, statestore, options, domain):
|
||||
# Add default homes share so that users can log into Samba
|
||||
if not options.no_homes:
|
||||
set_selinux_booleans(
|
||||
constants.SELINUX_BOOLEAN_SMBSERVICE["share_home_dirs"], statestore
|
||||
)
|
||||
# Allow Samba to access NFS-shared content
|
||||
if not options.no_nfs:
|
||||
set_selinux_booleans(
|
||||
constants.SELINUX_BOOLEAN_SMBSERVICE["reshare_nfs_with_samba"],
|
||||
statestore,
|
||||
)
|
||||
|
||||
|
||||
def uninstall(fstore, statestore, options):
|
||||
# Shut down Samba services and disable them
|
||||
smb = services.service("smb", api)
|
||||
winbind = services.service("winbind", api)
|
||||
for svc in (smb, winbind):
|
||||
if svc.is_running():
|
||||
svc.stop()
|
||||
svc.disable()
|
||||
|
||||
# Restore the state of affected selinux booleans
|
||||
boolean_states = {}
|
||||
for usecase in constants.SELINUX_BOOLEAN_SMBSERVICE:
|
||||
for name in usecase:
|
||||
boolean_states[name] = statestore.restore_state("selinux", name)
|
||||
|
||||
if boolean_states:
|
||||
set_selinux_booleans(boolean_states, statestore, backup=False)
|
||||
|
||||
# Remove samba's credentials cache
|
||||
ipautil.remove_ccache(ccache_path=paths.KRB5CC_SAMBA)
|
||||
|
||||
# Remove samba's configuration file
|
||||
ipautil.remove_file(paths.SMB_CONF)
|
||||
fstore.restore_file(paths.SMB_CONF)
|
||||
|
||||
# Remove samba's persistent and temporary tdb files
|
||||
tdb_files = [
|
||||
tdb_file
|
||||
for tdb_file in os.listdir(paths.SAMBA_DIR)
|
||||
if tdb_file.endswith(".tdb")
|
||||
]
|
||||
for tdb_file in tdb_files:
|
||||
ipautil.remove_file(tdb_file)
|
||||
|
||||
# Remove our keys from samba's keytab
|
||||
if os.path.exists(paths.SAMBA_KEYTAB):
|
||||
try:
|
||||
ipautil.run(
|
||||
[
|
||||
paths.IPA_RMKEYTAB,
|
||||
"--principal",
|
||||
api.env.smb_princ,
|
||||
"-k",
|
||||
paths.SAMBA_KEYTAB,
|
||||
]
|
||||
)
|
||||
except ipautil.CalledProcessError as e:
|
||||
if e.returncode != 5:
|
||||
logger.critical("Failed to remove old key for %s",
|
||||
api.env.smb_princ)
|
||||
|
||||
with use_api_as_principal(api.env.host_princ, paths.KRB5_KEYTAB):
|
||||
try:
|
||||
api.Command.service_del(api.env.smb_princ)
|
||||
except errors.VersionError as e:
|
||||
print("This client is incompatible: " + str(e))
|
||||
except errors.NotFound:
|
||||
logger.debug("No SMB service principal exists, OK to proceed")
|
||||
except errors.PublicError as e:
|
||||
logger.error(
|
||||
"Cannot connect to the server due to "
|
||||
"a generic error: %s", e,
|
||||
)
|
||||
|
||||
|
||||
def run():
|
||||
try:
|
||||
check_client_configuration()
|
||||
except ScriptError as e:
|
||||
print(e.msg)
|
||||
return e.rval
|
||||
|
||||
fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
|
||||
statestore = sysrestore.StateFile(paths.IPA_CLIENT_SYSRESTORE)
|
||||
|
||||
options, _args = parse_options()
|
||||
|
||||
logfile = paths.IPACLIENTSAMBA_INSTALL_LOG
|
||||
if options.uninstall:
|
||||
logfile = paths.IPACLIENTSAMBA_UNINSTALL_LOG
|
||||
|
||||
standard_logging_setup(
|
||||
logfile,
|
||||
verbose=False,
|
||||
debug=options.debug,
|
||||
filemode="a",
|
||||
console_format="%(message)s",
|
||||
)
|
||||
|
||||
cfg = dict(
|
||||
context="cli_installer",
|
||||
confdir=paths.ETC_IPA,
|
||||
in_server=False,
|
||||
debug=options.debug,
|
||||
verbose=0,
|
||||
)
|
||||
|
||||
# Bootstrap API early so that env object is available
|
||||
api.bootstrap(**cfg)
|
||||
|
||||
local_config = dict(
|
||||
host_princ=str("host/%s@%s" % (api.env.host, api.env.realm)),
|
||||
smb_princ=str("cifs/%s@%s" % (api.env.host, api.env.realm)),
|
||||
)
|
||||
|
||||
# Until api.finalize() is called, we can add our own configuration
|
||||
api.env._merge(**local_config)
|
||||
|
||||
if options.uninstall:
|
||||
if statestore.has_state("domain_member"):
|
||||
uninstall(fstore, statestore, options)
|
||||
print(
|
||||
"Samba configuration is reverted. "
|
||||
"However, Samba databases were fully cleaned and "
|
||||
"old configuration file will not be usable anymore."
|
||||
)
|
||||
else:
|
||||
print("Samba domain member is not configured yet")
|
||||
return 0
|
||||
|
||||
ca_cert_path = None
|
||||
if os.path.exists(paths.IPA_CA_CRT):
|
||||
ca_cert_path = paths.IPA_CA_CRT
|
||||
|
||||
if statestore.has_state("domain_member") and not options.force:
|
||||
print("Samba domain member is already configured")
|
||||
return CLIENT_ALREADY_CONFIGURED
|
||||
|
||||
if not os.path.exists(paths.SMBD):
|
||||
print("Samba suite is not installed")
|
||||
return CLIENT_NOT_CONFIGURED
|
||||
|
||||
autodiscover = False
|
||||
ds = discovery.IPADiscovery()
|
||||
if not options.server:
|
||||
print("Searching for IPA server...")
|
||||
ret = ds.search(ca_cert_path=ca_cert_path)
|
||||
logger.debug("Executing DNS discovery")
|
||||
if ret == discovery.NO_LDAP_SERVER:
|
||||
logger.debug("Autodiscovery did not find LDAP server")
|
||||
s = urlsplit(api.env.xmlrpc_uri)
|
||||
server = [s.netloc]
|
||||
logger.debug("Setting server to %s", s.netloc)
|
||||
else:
|
||||
autodiscover = True
|
||||
if not ds.servers:
|
||||
print(
|
||||
"Autodiscovery was successful but didn't return a server"
|
||||
)
|
||||
return 1
|
||||
logger.debug(
|
||||
"Autodiscovery success, possible servers %s",
|
||||
",".join(ds.servers),
|
||||
)
|
||||
server = ds.servers[0]
|
||||
else:
|
||||
server = options.server
|
||||
logger.debug("Verifying that %s is an IPA server", server)
|
||||
ldapret = ds.ipacheckldap(server, api.env.realm, ca_cert_path)
|
||||
if ldapret[0] == discovery.NO_ACCESS_TO_LDAP:
|
||||
print("Anonymous access to the LDAP server is disabled.")
|
||||
print("Proceeding without strict verification.")
|
||||
print(
|
||||
"Note: This is not an error if anonymous access has been "
|
||||
"explicitly restricted."
|
||||
)
|
||||
elif ldapret[0] == discovery.NO_TLS_LDAP:
|
||||
logger.warning("Unencrypted access to LDAP is not supported.")
|
||||
elif ldapret[0] != 0:
|
||||
print("Unable to confirm that %s is an IPA server" % server)
|
||||
return 1
|
||||
|
||||
if not autodiscover:
|
||||
print("IPA server: %s" % server)
|
||||
logger.debug("Using fixed server %s", server)
|
||||
else:
|
||||
print("IPA server: DNS discovery")
|
||||
logger.info("Configured to use DNS discovery")
|
||||
|
||||
if api.env.host == server:
|
||||
logger.error(
|
||||
"Cannot run on IPA master. "
|
||||
"Cannot configure Samba as a domain member on a domain "
|
||||
"controller. Please use ipa-adtrust-install for that!"
|
||||
)
|
||||
return 1
|
||||
|
||||
if not options.netbiosname:
|
||||
options.netbiosname = DNSName.from_text(api.env.host)[0].decode()
|
||||
options.netbiosname = options.netbiosname.upper()
|
||||
|
||||
with use_api_as_principal(api.env.host_princ, paths.KRB5_KEYTAB):
|
||||
try:
|
||||
# Try to access 'service_add_smb' command, if it throws
|
||||
# AttributeError exception, the IPA server doesn't support
|
||||
# setting up Samba as a domain member.
|
||||
service_add_smb = api.Command.service_add_smb
|
||||
|
||||
# Now try to see if SMB principal already exists
|
||||
api.Command.service_show(api.env.smb_princ)
|
||||
|
||||
# If no exception was raised, the object exists.
|
||||
# We cannot continue because we would break existing configuration
|
||||
print(
|
||||
"WARNING: SMB service principal %s already exists. "
|
||||
"Please remove it before proceeding." % (api.env.smb_princ)
|
||||
)
|
||||
if not options.force:
|
||||
return 1
|
||||
# For --force, we should then delete cifs/.. service object
|
||||
api.Command.service_del(api.env.smb_princ)
|
||||
except AttributeError:
|
||||
logger.error(
|
||||
"Chosen IPA master %s does not have support to"
|
||||
"set up Samba domain members", server,
|
||||
)
|
||||
return 1
|
||||
except errors.VersionError as e:
|
||||
print("This client is incompatible: " + str(e))
|
||||
return 1
|
||||
except errors.NotFound:
|
||||
logger.debug("No SMB service principal exists, OK to proceed")
|
||||
except errors.PublicError as e:
|
||||
logger.error(
|
||||
"Cannot connect to the server due to "
|
||||
"a generic error: %s", e,
|
||||
)
|
||||
return 1
|
||||
|
||||
# At this point we have proper setup:
|
||||
# - we connected to IPA API end-point as a host principal
|
||||
# - no cifs/... principal exists so we can create it
|
||||
print("Chosen IPA master: %s" % server)
|
||||
print("SMB principal to be created: %s" % api.env.smb_princ)
|
||||
print("NetBIOS name to be used: %s" % options.netbiosname)
|
||||
logger.info("Chosen IPA master: %s", server)
|
||||
logger.info("SMB principal to be created: %s", api.env.smb_princ)
|
||||
logger.info("NetBIOS name to be used: %s", options.netbiosname)
|
||||
|
||||
# 1. Pull down ID range and other details of known domains
|
||||
domains = retrieve_domain_information(api)
|
||||
if len(domains) == 0:
|
||||
# logger.error() produces both log file and stderr output
|
||||
logger.error("No configured trust controller detected "
|
||||
"on IPA masters. Use ipa-adtrust-install on an IPA "
|
||||
"master to configure trust controller role.")
|
||||
return 1
|
||||
|
||||
str_info = pretty_print_domain_information(domains)
|
||||
logger.info("Discovered domains to use:\n%s", str_info)
|
||||
print("Discovered domains to use:\n%s" % str_info)
|
||||
|
||||
if not options.unattended and not ipautil.user_input(
|
||||
"Continue to configure the system with these values?", False
|
||||
):
|
||||
print("Installation aborted")
|
||||
return 1
|
||||
|
||||
# 2. Create SMB service principal, if we are here, the command exists
|
||||
if (
|
||||
not statestore.get_state("domain_member", "service.principal") or
|
||||
options.force
|
||||
):
|
||||
service_add_smb(api.env.host, options.netbiosname)
|
||||
statestore.backup_state(
|
||||
"domain_member", "service.principal", "configured"
|
||||
)
|
||||
|
||||
# 3. Generate machine account password for reuse
|
||||
password = generate_smb_machine_account(
|
||||
fstore, statestore, options, domains[0]
|
||||
)
|
||||
|
||||
# 4. Now that we have all domains retrieved, we can generate smb.conf
|
||||
if (
|
||||
not statestore.get_state("domain_member", "smb.conf") or
|
||||
options.force
|
||||
):
|
||||
configure_smb_conf(fstore, statestore, options, domains)
|
||||
statestore.backup_state("domain_member", "smb.conf", "configured")
|
||||
|
||||
# 5. Create SMB service
|
||||
if statestore.get_state("domain_member",
|
||||
"service.principal") == "configured":
|
||||
retrieve_service_principal(
|
||||
fstore, statestore, options, domains[0],
|
||||
api.env.smb_princ, password
|
||||
)
|
||||
statestore.backup_state(
|
||||
"domain_member", "service.principal", "configured"
|
||||
)
|
||||
|
||||
# 6. Configure databases to contain proper details
|
||||
if not statestore.get_state("domain_member", "tdb") or options.force:
|
||||
populate_samba_databases(
|
||||
fstore, statestore, options, domains[0], password
|
||||
)
|
||||
statestore.backup_state("domain_member", "tdb", "configured")
|
||||
|
||||
# 7. Configure default group mapping
|
||||
if (
|
||||
not statestore.get_state("domain_member", "groupmap") or
|
||||
options.force
|
||||
):
|
||||
configure_default_groupmap(fstore, statestore, options, domains[0])
|
||||
statestore.backup_state("domain_member", "groupmap", "configured")
|
||||
|
||||
# 8. Enable SELinux policies
|
||||
if (
|
||||
not statestore.get_state("domain_member", "hardening") or
|
||||
options.force
|
||||
):
|
||||
harden_configuration(fstore, statestore, options, domains[0])
|
||||
statestore.backup_state("domain_member", "hardening", "configured")
|
||||
|
||||
# 9. Finally, store the state of upgrade
|
||||
statestore.backup_state("domain_member", "configured", True)
|
||||
|
||||
# Suggest service start only after validating smb.conf
|
||||
print(
|
||||
"Samba domain member is configured. "
|
||||
"Please check configuration at %s and "
|
||||
"start smb and winbind services" % paths.SMB_CONF
|
||||
)
|
||||
logger.info(
|
||||
"Samba domain member is configured. "
|
||||
"Please check configuration at %s and "
|
||||
"start smb and winbind services",
|
||||
paths.SMB_CONF,
|
||||
)
|
||||
|
||||
return 0
|
@@ -406,6 +406,8 @@ class BasePathNamespace:
|
||||
SSHD = '/usr/sbin/sshd'
|
||||
SSSCTL = '/usr/sbin/sssctl'
|
||||
LIBARCH = "64"
|
||||
TDBTOOL = '/usr/bin/tdbtool'
|
||||
SECRETS_TDB = '/var/lib/samba/private/secrets.tdb'
|
||||
|
||||
def check_paths(self):
|
||||
"""Check paths for missing files
|
||||
|
@@ -740,7 +740,7 @@ class update_host_cifs_keytabs(Updater):
|
||||
|
||||
def extract_key_refs(self, keytab):
|
||||
host_princ = self.host_princ_template.format(
|
||||
master=self.api.host, realm=self.api.realm)
|
||||
master=self.api.env.host, realm=self.api.env.realm)
|
||||
result = ipautil.run([paths.KLIST, "-etK", "-k", keytab],
|
||||
capture_output=True, raiseonerr=False,
|
||||
nolog_output=True)
|
||||
|
152
ipatests/test_integration/test_smb.py
Normal file
152
ipatests/test_integration/test_smb.py
Normal file
@@ -0,0 +1,152 @@
|
||||
#
|
||||
# Copyright (C) 2019 FreeIPA Contributors see COPYING for license
|
||||
#
|
||||
|
||||
"""This module provides tests for SMB-related features like
|
||||
configuring Samba file server and mounting SMB file system
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import time
|
||||
import os
|
||||
|
||||
from ipatests.test_integration.base import IntegrationTest
|
||||
from ipatests.pytest_ipa.integration import tasks
|
||||
from ipaplatform.paths import paths
|
||||
|
||||
# give some time for units to stabilize
|
||||
# otherwise we get transient errors
|
||||
WAIT_AFTER_INSTALL = 5
|
||||
WAIT_AFTER_UNINSTALL = WAIT_AFTER_INSTALL
|
||||
user_password = "Secret123"
|
||||
users = {
|
||||
"athena": "p",
|
||||
"euripides": "s"
|
||||
}
|
||||
|
||||
|
||||
class TestSMB(IntegrationTest):
|
||||
|
||||
num_replicas = 1
|
||||
num_clients = 1
|
||||
|
||||
@classmethod
|
||||
def fix_resolv_conf(cls, client, server):
|
||||
|
||||
contents = client.get_file_contents(paths.RESOLV_CONF,
|
||||
encoding='utf-8')
|
||||
nameserver = 'nameserver %s\n' % server.ip
|
||||
if not contents.startswith(nameserver):
|
||||
contents = nameserver + contents.replace(nameserver, '')
|
||||
client.run_command([
|
||||
'/usr/bin/cp', paths.RESOLV_CONF,
|
||||
'%s.sav' % paths.RESOLV_CONF
|
||||
])
|
||||
client.put_file_contents(paths.RESOLV_CONF, contents)
|
||||
|
||||
@classmethod
|
||||
def restore_resolv_conf(cls, client):
|
||||
client.run_command([
|
||||
'/usr/bin/cp',
|
||||
'%s.sav' % paths.RESOLV_CONF,
|
||||
paths.RESOLV_CONF
|
||||
])
|
||||
|
||||
@classmethod
|
||||
def install(cls, mh):
|
||||
tasks.install_master(cls.master, setup_dns=True)
|
||||
tasks.install_adtrust(cls.master)
|
||||
|
||||
for client in cls.replicas + cls.clients:
|
||||
cls.fix_resolv_conf(client, cls.master)
|
||||
tasks.install_client(cls.master, client,
|
||||
extra_args=['--mkhomedir'])
|
||||
|
||||
cls.replicas[0].collect_log('/var/log/samba/')
|
||||
cls.master.collect_log('/var/log/samba/')
|
||||
|
||||
@classmethod
|
||||
def uninstall(cls, mh):
|
||||
for client in cls.clients + cls.replicas:
|
||||
tasks.uninstall_client(client)
|
||||
cls.restore_resolv_conf(client)
|
||||
tasks.uninstall_master(cls.master)
|
||||
|
||||
def test_prepare_users(self):
|
||||
smbsrv = self.replicas[0]
|
||||
|
||||
temp_pass = "t3mp!p4ss"
|
||||
user_kinit = "%s\n%s\n%s\n" % (temp_pass,
|
||||
user_password, user_password)
|
||||
user_addpass = "%s\n%s\n" % (temp_pass, temp_pass)
|
||||
for user in users:
|
||||
self.master.run_command([
|
||||
"ipa", "user-add",
|
||||
"%s" % user, "--first", "%s" % user,
|
||||
"--last", "%s" % users[user],
|
||||
'--password'], stdin_text=user_addpass
|
||||
)
|
||||
self.master.run_command(['kdestroy', '-A'])
|
||||
self.master.run_command(
|
||||
['kinit', user], stdin_text=user_kinit
|
||||
)
|
||||
# Force creation of home directories on the SMB server
|
||||
smbsrv.run_command(['su', '-l', '-', user, '-c', 'stat .'])
|
||||
|
||||
# Switch back to admin
|
||||
self.master.run_command(['kdestroy', '-A'])
|
||||
tasks.kinit_admin(self.master)
|
||||
|
||||
def test_install_samba(self):
|
||||
|
||||
smbsrv = self.replicas[0]
|
||||
|
||||
smbsrv.run_command([
|
||||
"ipa-client-samba", "-U"
|
||||
])
|
||||
|
||||
smbsrv.run_command([
|
||||
"systemctl", "enable", "--now", "smb", "winbind"
|
||||
])
|
||||
time.sleep(WAIT_AFTER_INSTALL)
|
||||
|
||||
smbsrv.run_command(['smbstatus'])
|
||||
|
||||
def test_access_homes_smbclient(self):
|
||||
"""Access user home directory via smb3.ko and smbclient
|
||||
Test checks that both kernel SMB3 driver and userspace
|
||||
smbclient utility work against IPA-enrolled Samba server
|
||||
"""
|
||||
smbsrv = self.replicas[0]
|
||||
smbclt = self.clients[0]
|
||||
|
||||
remote_uri = '//{smbsrv}/homes'.format(smbsrv=smbsrv.hostname)
|
||||
|
||||
for user in users:
|
||||
smbclt.run_command(['kinit', user], stdin_text=user_password)
|
||||
mntpoint = '/mnt/{user}'.format(user=user)
|
||||
userfile = '{user}.dat'.format(user=user)
|
||||
|
||||
smbclt.run_command(['mkdir', '-p', mntpoint])
|
||||
smbclt.run_command(['mount', '-t', 'cifs',
|
||||
remote_uri, mntpoint, '-o',
|
||||
'user={user},sec=krb5i'.format(user=user)])
|
||||
smbclt.run_command(['dd', 'count=1024', 'bs=1K', 'if=/dev/zero',
|
||||
'of={path}'.format(
|
||||
path=os.path.join(mntpoint, userfile))])
|
||||
smbclt.run_command(['findmnt', '-t', 'cifs'])
|
||||
smbclt.run_command(['ls', '-laZ',
|
||||
os.path.join(mntpoint, userfile)])
|
||||
smbsrv.run_command(['smbstatus'])
|
||||
smbclt.run_command(['umount', '-a', '-t', 'cifs'])
|
||||
smbclt.run_command(['smbclient', '-k', remote_uri,
|
||||
'-c', 'allinfo {path}'.format(path=userfile)])
|
||||
smbclt.run_command(['kdestroy', '-A'])
|
||||
|
||||
def test_uninstall_samba(self):
|
||||
for user in users:
|
||||
self.master.run_command(['ipa', 'user-del', user])
|
||||
|
||||
smbsrv = self.replicas[0]
|
||||
smbsrv.run_command(['ipa-client-samba', '--uninstall', '-U'])
|
Reference in New Issue
Block a user