From 6d931f50758693eaf6f157070c6a43b650a278db Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Mon, 16 Dec 2024 16:43:12 +0100 Subject: [PATCH 1/5] Define HOME_BASEDIR node variable - Customize the base directory where user's home is created by useradd. - The directory must exist, otherwise the custom setting is ignored. - On SELinux the directory must be equivalent of /home. --- .../var/lib/nethserver/node/actions/add-module/50update | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/imageroot/var/lib/nethserver/node/actions/add-module/50update b/core/imageroot/var/lib/nethserver/node/actions/add-module/50update index 09fe56c0b..e0834fa70 100755 --- a/core/imageroot/var/lib/nethserver/node/actions/add-module/50update +++ b/core/imageroot/var/lib/nethserver/node/actions/add-module/50update @@ -90,7 +90,13 @@ if is_rootfull: agent.run_helper('systemctl', 'enable', '--now', f'agent@{module_id}.service').check_returncode() else: # rootless # Create the module dirs structure - agent.run_helper('useradd', '-m', '-k', '/etc/nethserver/skel', '-s', '/bin/bash', module_id).check_returncode() + if 'HOME_BASEDIR' in os.environ and os.path.isdir(os.environ["HOME_BASEDIR"]): + # create the home directory with a custom base path + module_basedir_args = ["-b", os.environ["HOME_BASEDIR"]] + else: + # use default base path (see useradd -D) + module_basedir_args = [] + agent.run_helper('useradd', *module_basedir_args, '-m', '-k', '/etc/nethserver/skel', '-s', '/bin/bash', module_id).check_returncode() agent.run_helper('chmod', '-c', '0700', os.path.expanduser("~" + module_id)).check_returncode() os.chdir(os.path.expanduser("~" + module_id) + '/.config') save_environment(module_environment) From a52d756f554f026e6c118b4580b2f8f3bf8a00ef Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Mon, 13 Jan 2025 15:27:18 +0100 Subject: [PATCH 2/5] docs. Document HOME_BASEDIR variable --- docs/core/filesystem.md | 33 +++++++++++++++++++++++++++++++++ docs/development_process.md | 3 --- 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/docs/core/filesystem.md b/docs/core/filesystem.md index d6e731821..89980c6ac 100644 --- a/docs/core/filesystem.md +++ b/docs/core/filesystem.md @@ -22,3 +22,36 @@ See `/var/lib/nethserver/node/state/coreimage.lst` for a complete list. Rootless modules are totally contained inside UNIX user home directory, like `/home/trafeik1`. Rootfull modules are homed under `/var/lib/nethserver/samba1`. + +## Custom base path for home directories + +NS8 core uses the common `/home` path for users' home directories, but you +can configure a different path. Follow these steps to configure a node's +agent for this purpose: + +1. Create the alternative base path: + + mkdir -m 0755 /home1 + +2. For systems with SELinux, make it equivalent to `/home`: + + semanage fcontext -a -e /home /home1 + +3. Fix SELinux labels for the new path: + + restorecon -R -v /home1 + +4. Edit the node's environment file: + + runagent -m node vi environment + + Add or modify the `HOME_BASEDIR` line in the environment file: + + HOME_BASEDIR=/home1 + +From now on, new module instances will use `/home1` as their base +directory. Existing modules are not affected and will retain their current +home directory. + +Refer to the `semanage-fcontext` man page for additional SELinux-related +information. diff --git a/docs/development_process.md b/docs/development_process.md index 4c0ee6c2c..f0a0d2894 100644 --- a/docs/development_process.md +++ b/docs/development_process.md @@ -6,9 +6,6 @@ nav_order: 3 # Development process -* TOC -{:toc} - All NethServer projects follow a shared development process. See [NethServer development handbook](https://handbook.nethserver.org/) for a detailed guide on how to contribute to NethServer. From c2b9b1eb9f260378b3a0f647e95870fb61662858 Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Tue, 14 Jan 2025 19:10:36 +0100 Subject: [PATCH 3/5] Add configure-home-basedir node helper command --- .../node/bin/configure-home-basedir | 122 ++++++++++++++++++ docs/core/filesystem.md | 28 ++-- 2 files changed, 137 insertions(+), 13 deletions(-) create mode 100755 core/imageroot/var/lib/nethserver/node/bin/configure-home-basedir diff --git a/core/imageroot/var/lib/nethserver/node/bin/configure-home-basedir b/core/imageroot/var/lib/nethserver/node/bin/configure-home-basedir new file mode 100755 index 000000000..07e472353 --- /dev/null +++ b/core/imageroot/var/lib/nethserver/node/bin/configure-home-basedir @@ -0,0 +1,122 @@ +#!/usr/bin/env python3 + +# +# Copyright (C) 2025 Nethesis S.r.l. +# SPDX-License-Identifier: GPL-3.0-or-later +# + +import agent +import sys +import os +import stat +import argparse +import subprocess +import json + +def main(): + parser = argparse.ArgumentParser( + description="Validate and configure the base path for home directories of NS8 modules.", + epilog="DIR is the base path for new home directories." + ) + parser.add_argument( + "-c", + "--check-only", + dest="check_only", + action="store_true", + help="Check only, do not save the configuration.", + ) + parser.add_argument( + "DIR", + metavar="DIR", + help="Base path for new home directories.", + ) + + args = parser.parse_args() + if args.DIR == "" and not args.check_only: + agent.unset_env('HOME_BASEDIR') + print("The base path for home directories has been reset to OS default.") + else: + home_basedir = validate_home_basedir(args.DIR) + if not args.check_only: + store_configuration(home_basedir) + print("The base path for home directories has been updated.") + +def store_configuration(path): + if os.path.isdir("/sys/fs/selinux"): + # On systems with SELinux, configure path as /home equivalent + update_selinux_customization(path) + agent.set_env('HOME_BASEDIR', path) + print("HOME_BASEDIR=" + path) + +def update_selinux_customization(path): + ocurrent = subprocess.check_output(['semanage', 'fcontext', '-l', '-C'], text=True) + if not f"\n{path} = /home\n" in ocurrent: + subprocess.check_call(['semanage', 'fcontext', '-a', '-e', '/home', path]) + subprocess.check_call(['restorecon', '-v', path]) + update_parentdir_selinux_type(path) + +def update_parentdir_selinux_type(path): + """If needed, set home_root_t on parent dir as required by semanage-fcontext manpage.""" + parentdir = os.path.dirname(path) + if parentdir != "/": + ols = subprocess.check_output(['ls', '-Zd', parentdir], text=True) + if ":default_t:" in ols: + subprocess.check_call(['semanage', 'fcontext', '-a', '-t', 'home_root_t', parentdir]) + subprocess.check_call(['restorecon', '-v', parentdir]) + +def validate_home_basedir(path): + """Validate the given path and return it in canonicalized form.""" + if os.path.islink(path) or not os.path.isdir(path): + print(f"Error: {path} is not a directory.", file=sys.stderr) + sys.exit(2) + # Canonicalize the path value: + home_basedir = os.path.abspath(path) + if os.path.realpath(home_basedir) != home_basedir: + print(f"Error: {path} contains one or more symlink components.", file=sys.stderr) + sys.exit(2) + check_permissions(home_basedir) + check_unique_mountpoint(home_basedir) + return home_basedir + +def get_mountpoint_device(path): + """Check if path is a mountpoint and return its source device.""" + try: + joutput = subprocess.check_output(['findmnt', '--json', '--mountpoint', path]) + ofindmnt = json.loads(joutput) + return ofindmnt["filesystems"][0]["source"] # source device path + except subprocess.CalledProcessError: + print(f"Error: {path} is not a filesystem mountpoint.", file=sys.stderr) + sys.exit(2) + +def check_unique_mountpoint(path): + """Check if path is a mountpoint, and its source device is not + mounted elsewhere.""" + device = get_mountpoint_device(path) + joutput = subprocess.check_output(['findmnt', '-o', 'TARGET', '--json', '--source', device]) + ofindmnt = json.loads(joutput) + for ofs in ofindmnt["filesystems"]: + if ofs['target'] != path: + print(f"Error: device {device} has multiple mountpoints. It is mounted also on {ofs['target']}. Unmount it, persist the change, and retry.", file=sys.stderr) + sys.exit(2) + +def check_permissions(dir_path): + """Checks if the given directory and its ancestors have octal 5 + (world-readable and executable) or higher permissions.""" + current_path = dir_path + while current_path != "/": + try: + st = os.stat(current_path) + # Extract other permissions (last 3 bits) + other_perms = stat.S_IMODE(st.st_mode) & 0o007 + # Check if permissions are at least 5 (r-x) + if other_perms < 0o5: + print(f"Error: invalid permissions on {current_path}. Fix with chmod -c +rx {current_path}, or higher.", file=sys.stderr) + sys.exit(2) + # Move to the parent directory + current_path = os.path.dirname(current_path) + except Exception as ex: + print(f"Error: {current_path}:", ex, file=sys.stderr) + sys.exit(2) + +if __name__ == "__main__": + main() diff --git a/docs/core/filesystem.md b/docs/core/filesystem.md index 89980c6ac..597442c49 100644 --- a/docs/core/filesystem.md +++ b/docs/core/filesystem.md @@ -26,32 +26,34 @@ Rootfull modules are homed under `/var/lib/nethserver/samba1`. ## Custom base path for home directories NS8 core uses the common `/home` path for users' home directories, but you -can configure a different path. Follow these steps to configure a node's -agent for this purpose: +can configure a different path if it is a device mount point. Follow these +steps to configure a node's agent for this purpose: 1. Create the alternative base path: mkdir -m 0755 /home1 -2. For systems with SELinux, make it equivalent to `/home`: +2. Mount the device on the new path: - semanage fcontext -a -e /home /home1 + mount /dev/some /home1 -3. Fix SELinux labels for the new path: + To make the mount persistent, either edit `/etc/fstab` or create a + systemd `.mount` unit. Ensure the device is correctly mounted after a + system reboot. - restorecon -R -v /home1 +3. Configure the node agent to use `/home1` as base directory for new + modules: -4. Edit the node's environment file: - - runagent -m node vi environment - - Add or modify the `HOME_BASEDIR` line in the environment file: - - HOME_BASEDIR=/home1 + runagent -m node configure-home-basedir /home1 From now on, new module instances will use `/home1` as their base directory. Existing modules are not affected and will retain their current home directory. +The `configure-home-basedir` command modifies SELinux configuration. Run +the following command to inspect the current customizations: + + semanage fcontext -l -C + Refer to the `semanage-fcontext` man page for additional SELinux-related information. From bc5ec76c91412135261c78e9572c145e31cf4d5d Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Tue, 14 Jan 2025 19:40:34 +0100 Subject: [PATCH 4/5] add-user fails, if HOME_BASEDIR is not a dir --- .../lib/nethserver/node/actions/add-module/50update | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/core/imageroot/var/lib/nethserver/node/actions/add-module/50update b/core/imageroot/var/lib/nethserver/node/actions/add-module/50update index e0834fa70..0f8d3e189 100755 --- a/core/imageroot/var/lib/nethserver/node/actions/add-module/50update +++ b/core/imageroot/var/lib/nethserver/node/actions/add-module/50update @@ -54,6 +54,11 @@ is_rootfull = request['is_rootfull'] module_environment = request['environment'] image_url = module_environment['IMAGE_URL'] +# Valdate the base homedir path: it must exist. +if 'HOME_BASEDIR' in os.environ and not os.path.isdir(os.environ['HOME_BASEDIR']): + print(agent.SD_ERR + f"[ERROR] The HOME_BASEDIR path, {os.environ['HOME_BASEDIR']} does not exist or is not a directory.", file=sys.stderr) + sys.exit(2) + # Allocate TCP ports if request['tcp_ports_demand'] > 0: tcp_ports_range=node.ports_manager.allocate_ports(request['tcp_ports_demand'], module_id, 'tcp') @@ -89,13 +94,13 @@ if is_rootfull: # Start the module agent agent.run_helper('systemctl', 'enable', '--now', f'agent@{module_id}.service').check_returncode() else: # rootless - # Create the module dirs structure - if 'HOME_BASEDIR' in os.environ and os.path.isdir(os.environ["HOME_BASEDIR"]): - # create the home directory with a custom base path + if 'HOME_BASEDIR' in os.environ: + # create the home directory with the custom base path module_basedir_args = ["-b", os.environ["HOME_BASEDIR"]] else: # use default base path (see useradd -D) module_basedir_args = [] + # Create the module dirs structure agent.run_helper('useradd', *module_basedir_args, '-m', '-k', '/etc/nethserver/skel', '-s', '/bin/bash', module_id).check_returncode() agent.run_helper('chmod', '-c', '0700', os.path.expanduser("~" + module_id)).check_returncode() os.chdir(os.path.expanduser("~" + module_id) + '/.config') From 2f80502c01c6c7cb7a28800c21eb70a66adf861b Mon Sep 17 00:00:00 2001 From: Davide Principi Date: Fri, 17 Jan 2025 16:49:12 +0100 Subject: [PATCH 5/5] Improve helper argument parsing --- .../node/bin/configure-home-basedir | 42 ++++++++++++------- docs/core/filesystem.md | 2 +- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/core/imageroot/var/lib/nethserver/node/bin/configure-home-basedir b/core/imageroot/var/lib/nethserver/node/bin/configure-home-basedir index 07e472353..3e5ce7973 100755 --- a/core/imageroot/var/lib/nethserver/node/bin/configure-home-basedir +++ b/core/imageroot/var/lib/nethserver/node/bin/configure-home-basedir @@ -18,35 +18,49 @@ def main(): description="Validate and configure the base path for home directories of NS8 modules.", epilog="DIR is the base path for new home directories." ) - parser.add_argument( + exgroup = parser.add_mutually_exclusive_group() + exgroup.add_argument( + "-s", + "--set", + type=str, + dest="set_dir", + metavar="DIR", + help="Set base path for new home directories.", + ) + exgroup.add_argument( "-c", - "--check-only", - dest="check_only", - action="store_true", + "--check", + type=str, + dest="check_dir", + metavar="DIR", help="Check only, do not save the configuration.", ) - parser.add_argument( - "DIR", - metavar="DIR", - help="Base path for new home directories.", + exgroup.add_argument( + "-r", + "--reset", + action="store_true", + help="Reset base path for home directories to OS default." ) args = parser.parse_args() - if args.DIR == "" and not args.check_only: + if args.reset: agent.unset_env('HOME_BASEDIR') print("The base path for home directories has been reset to OS default.") + elif args.check_dir: + validate_home_basedir(args.check_dir) + elif args.set_dir: + home_basedir = validate_home_basedir(args.set_dir) + store_configuration(home_basedir) + print(f"The base path for home directories has been set to {args.set_dir}.") else: - home_basedir = validate_home_basedir(args.DIR) - if not args.check_only: - store_configuration(home_basedir) - print("The base path for home directories has been updated.") + args.print_usage() + sys.exit(1) def store_configuration(path): if os.path.isdir("/sys/fs/selinux"): # On systems with SELinux, configure path as /home equivalent update_selinux_customization(path) agent.set_env('HOME_BASEDIR', path) - print("HOME_BASEDIR=" + path) def update_selinux_customization(path): ocurrent = subprocess.check_output(['semanage', 'fcontext', '-l', '-C'], text=True) diff --git a/docs/core/filesystem.md b/docs/core/filesystem.md index 597442c49..b85c3124b 100644 --- a/docs/core/filesystem.md +++ b/docs/core/filesystem.md @@ -44,7 +44,7 @@ steps to configure a node's agent for this purpose: 3. Configure the node agent to use `/home1` as base directory for new modules: - runagent -m node configure-home-basedir /home1 + runagent -m node configure-home-basedir --set /home1 From now on, new module instances will use `/home1` as their base directory. Existing modules are not affected and will retain their current