# # Copyright (C) 2019 Red Hat, Inc. # # This copyrighted material is made available to anyone wishing to use, # modify, copy, or redistribute it subject to the terms and conditions of # the GNU General Public License v.2, or (at your option) any later version. # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY expressed or implied, including the implied warranties of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General # Public License for more details. You should have received a copy of the # GNU General Public License along with this program; if not, write to the # Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301, USA. Any Red Hat trademarks that are incorporated in the # source code or documentation are not subject to the GNU General Public # License and may only be used or replicated with the express permission of # Red Hat, Inc. # import os import copy import shutil from pyanaconda.anaconda_loggers import get_module_logger from pyanaconda.core import util from pyanaconda.core.configuration.anaconda import conf from pyanaconda.core.constants import PAYLOAD_TYPE_DNF from pyanaconda.core.path import make_directories, join_paths from pyanaconda.modules.common.errors.installation import SecurityInstallationError from pyanaconda.modules.common.task import Task from pyanaconda.modules.security.constants import SELinuxMode log = get_module_logger(__name__) REALM_TOOL_NAME = "realm" AUTHSELECT_TOOL_PATH = "/usr/bin/authselect" PAM_SO_PATH = "/lib/security/pam_fprintd.so" PAM_SO_64_PATH = "/lib64/security/pam_fprintd.so" class PreconfigureFIPSTask(Task): """Installation task that sets up FIPS for the payload installation.""" def __init__(self, fips_enabled, payload_type, sysroot): """Create a new task. :param fips_enabled: True if FIPS is enabled, otherwise False :param payload_type: a type of the payload :param sysroot: a path to the system root """ super().__init__() self._fips_enabled = fips_enabled self._payload_type = payload_type self._sysroot = sysroot @property def name(self): return "Set up FIPS for the payload installation" def run(self): """Set up FIPS for the payload installation. Copy the crypto policy from the installation environment to the target system before package installation. The RPM scriptlets need to be executed in the FIPS mode if there is fips=1 on the kernel cmdline. """ if not self._fips_enabled: log.debug("FIPS is not enabled. Skipping.") return if self._payload_type != PAYLOAD_TYPE_DNF: log.debug("Don't set up FIPS for the %s payload.", self._payload_type) return if not self._check_fips(): raise SecurityInstallationError( "FIPS is not correctly set up " "in the installation environment." ) self._set_up_fips() def _check_fips(self): """Check FIPS in the installation environment.""" # Check the config file. config_path = "/etc/crypto-policies/config" if not os.path.exists(config_path): log.error("File '%s' doesn't exist.", config_path) return False with open(config_path) as f: if f.read().strip() != "FIPS": log.error("The crypto policy is not set to FIPS.") return False # Check one of the back-end symlinks. symlink_path = "/etc/crypto-policies/back-ends/opensshserver.config" if "FIPS" not in os.path.realpath(symlink_path): log.error("The back ends are not set to FIPS.") return False return True def _set_up_fips(self): """Set up FIPS in the target system.""" log.debug("Copying the crypto policy.") # Create /etc/crypto-policies. src = "/etc/crypto-policies/" dst = join_paths(self._sysroot, src) make_directories(dst) # Copy the config file. src = "/etc/crypto-policies/config" dst = join_paths(self._sysroot, src) shutil.copyfile(src, dst) # Log the file content on the target system. util.execWithRedirect("/bin/cat", [dst]) # Copy the back-ends. src = "/etc/crypto-policies/back-ends/" dst = join_paths(self._sysroot, src) shutil.copytree(src, dst, symlinks=True) # Log the directory content on the target system. util.execWithRedirect("/bin/ls", ["-l", dst]) class ConfigureFIPSTask(Task): """Installation task that configures FIPS on the installed system.""" def __init__(self, fips_enabled, sysroot): """Create a new task. :param fips_enabled: True if FIPS is enabled, otherwise False :param sysroot: a path to the system root """ super().__init__() self._fips_enabled = fips_enabled self._sysroot = sysroot @property def name(self): return "Configure FIPS" def run(self): """Configure FIPS on the installed system. If the installation is running in fips mode then make sure fips is also correctly enabled in the installed system. """ if not self._fips_enabled: log.debug("FIPS is not enabled. Skipping.") return if not conf.target.is_hardware: log.debug("Don't set up FIPS on %s.", conf.target.type.value) return # We use the --no-bootcfg option as we don't want fips-mode-setup # to modify the bootloader configuration. Anaconda already does # everything needed & it would require grubby to be available on # the system. util.execWithRedirect( "fips-mode-setup", ["--enable", "--no-bootcfg"], root=self._sysroot ) class ConfigureSELinuxTask(Task): """Installation task for Initial Setup configuration.""" SELINUX_CONFIG_PATH = "/etc/selinux/config" SELINUX_STATES = { SELinuxMode.DISABLED: "disabled", SELinuxMode.ENFORCING: "enforcing", SELinuxMode.PERMISSIVE: "permissive" } def __init__(self, sysroot, selinux_mode): """Create a new task. :param str sysroot: a path to the root of the target system :param SELinuxMode selinux_mode: a SELinux mode States are defined by the SELinuxMode enum as distinct integers. """ super().__init__() self._sysroot = sysroot self._selinux_mode = selinux_mode @property def name(self): return "Configure SELinux" def run(self): """Run the task.""" if self._selinux_mode == SELinuxMode.DEFAULT: log.debug("Use SELinux default configuration.") return if self._selinux_mode not in self.SELINUX_STATES: log.error("Unknown SELinux state for %s.", self._selinux_mode) return try: # Read the SELinux configuration file. path = join_paths(self._sysroot, self.SELINUX_CONFIG_PATH) log.debug("Modifying the configuration at %s.", path) with open(path, "r") as f: lines = f.readlines() # Modify the SELinux configuration. lines = list(map(self._process_line, lines)) # Write the modified configuration. with open(path, "w") as f: f.writelines(lines) except OSError as msg: log.error("SELinux configuration failed: %s", msg) @property def _selinux_state(self): """The string representation of the SELinux mode.""" return self.SELINUX_STATES[self._selinux_mode] def _process_line(self, line): """Process a line from the SELinux configuration file.""" if line.strip().startswith("SELINUX="): log.debug("Found '%s'.", line.strip()) line = "SELINUX={}\n".format(self._selinux_state) log.debug("Setting '%s'.", line.strip()) return line return line class RealmDiscoverTask(Task): """Task for discovering information about a realm we intend to join (if any). Based on results from this task we might attempt to join the realm via a separate task later on. """ def __init__(self, sysroot, realm_data): """Create a new realm discovery task. :param str sysroot: a path to the root of the target system :param realm_data: realm data holder """ super().__init__() self._sysroot = sysroot self._realm_data = realm_data @property def name(self): return "Discover information about a realm" def _parse_realm_data(self, output): """Parse data from realm tool output. First line is the realm name, and following lines are data formatted as a simple key value store: "name: value" We only care about the keys called "required-package" and ignore the rest. :param str output: output of the realm tool :return: a tuple reporting discovery success and package requirements :rtype: (bool, list(str)) """ required_packages = ["realmd"] discovered_realm_data = None lines = output.split("\n") if lines: discovered_realm_data = lines.pop(0).strip() log.info("Realm discovered: %s", discovered_realm_data) for line in lines: parts = line.split(":", 1) if len(parts) == 2 and parts[0].strip() == "required-package": package_spec = parts[1].strip() # "" is not a valid package specification if package_spec: required_packages.append(package_spec) log.info("Realm %s needs packages %s", discovered_realm_data, ", ".join(required_packages)) return bool(discovered_realm_data), required_packages def run(self): if not self._realm_data.name: log.debug("No realm name set, skipping realm discovery.") return self._realm_data output = "" try: argv = ["discover", "--verbose"] + self._realm_data.discover_options \ + [self._realm_data.name] output = util.execWithCapture(REALM_TOOL_NAME, argv, filter_stderr=True) except OSError: # TODO: A lousy way of propagating what will usually be # 'no such realm' # The error message is logged by util return self._realm_data realm_discovered, required_packages = self._parse_realm_data(output) # set the result in the realm data holder and return it self._realm_data.discovered = realm_discovered self._realm_data.required_packages = required_packages return self._realm_data class RealmJoinTask(Task): """Task for joining a realm we have discovered (if any).""" def __init__(self, sysroot, realm_data): """Create a new realm discovery task. :param str sysroot: a path to the root of the target system :param realm_data: realm data holder """ super().__init__() self._sysroot = sysroot # We do a deep copy of the realm data holder to avoid # changes to the data in the Task from changing the backing # data structure in the module, without triggering the changed signal. # This also works the other way around, preventing changes # to the data structure in the module influencing the task after # it has been instantiated. self._realm_data = copy.deepcopy(realm_data) @property def name(self): return "Join a realm" def set_realm_data(self, realm_data): """Set a new version of realm data to the task.""" log.debug("Setting new realm data for realm join task: %s", realm_data) self._realm_data = realm_data def run(self): if not self._realm_data.discovered: log.debug("No realm has been discovered, so not joining any realm.") return for arg in self._realm_data.join_options: if arg.startswith("--no-password") or arg.startswith("--one-time-password"): pw_args = [] break else: # no explicit password argument, using implicit --no-password pw_args = ["--no-password"] argv = ["join", "--install", self._sysroot, "--verbose"] \ + pw_args + self._realm_data.join_options rc = -1 try: rc = util.execWithRedirect(REALM_TOOL_NAME, argv) except OSError: log.exception("Realm %s join failed with exception.", self._realm_data.name) pass if rc == 0: log.info("Joined realm %s", self._realm_data.name) else: log.info("Joining realm %s failed", self._realm_data.name) def run_auth_tool(cmd, args, root, required=True): """Run an authentication related tool. This generally means authselect. :param str cmd: path to the tool to be run :param list(str) args: list of arguments passed to the tool :param str root: a path to the root in which the tool should be run :param bool required: require the tool to be present and run (False makes the function pass if the tool is not available) :raises: SecurityInstallationError if the tool which is required is not found :raises: RuntimeError if the run of the tool fails """ if not os.path.lexists(root + cmd): msg = "{} is missing. Cannot setup authentication.".format(cmd) if required: raise SecurityInstallationError(msg) else: log.error(msg) return try: log.debug("Configuring authentication: %s %s", cmd, args) util.execWithRedirect(cmd, args, root=root) except RuntimeError as msg: log.error("Error running %s %s: %s", cmd, args, msg) class ConfigureFingerprintAuthTask(Task): """Installation task for fingerprint authentication setup.""" def __init__(self, sysroot, fingerprint_auth_enabled): """Create a new Authselect configuration task. :param str sysroot: a path to the root of the target system :param bool fingerprint_auth_enabled: True if fingerprint authentication should be enabled if possible, False otherwise """ super().__init__() self._sysroot = sysroot self._fingerprint_auth_enabled = fingerprint_auth_enabled @property def name(self): return "Configure fingerprint authentication" def _is_fingerprint_configuration_supported(self): """Is the fingerprint configuration supported?""" return (os.path.exists(self._sysroot + PAM_SO_64_PATH) or os.path.exists(self._sysroot + PAM_SO_PATH)) def run(self): """Run the task.""" if not self._fingerprint_auth_enabled: log.debug("Fingerprint configuration is not enabled. Skipping.") return if not self._is_fingerprint_configuration_supported(): log.debug("Fingerprint configuration is not supported on target system. Skipping.") return log.debug("Enabling fingerprint authentication.") run_auth_tool( AUTHSELECT_TOOL_PATH, ["enable-feature", "with-fingerprint"], self._sysroot, required=False ) class ConfigureAuthselectTask(Task): """Installation task for Authselect configuration.""" def __init__(self, sysroot, authselect_options): """Create a new Authselect configuration task. :param str sysroot: a path to the root of the target system :param list authselect_options: options for authselect """ super().__init__() self._sysroot = sysroot self._authselect_options = authselect_options @property def name(self): return "Authselect configuration" def run(self): """Run the task.""" if not self._authselect_options: log.debug("Authselect is not configured. Skipping.") return run_auth_tool( AUTHSELECT_TOOL_PATH, self._authselect_options + ["--force"], self._sysroot )