Server IP : 184.154.167.98 / Your IP : 18.224.54.118 Web Server : Apache System : Linux pink.dnsnetservice.com 4.18.0-553.22.1.lve.1.el8.x86_64 #1 SMP Tue Oct 8 15:52:54 UTC 2024 x86_64 User : puertode ( 1767) PHP Version : 8.2.26 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : ON | Sudo : ON | Pkexec : ON Directory : /usr/share/cagefs/ |
Upload File : |
#!/opt/cloudlinux/venv/bin/python3 -Ibb # -*- coding: utf-8 -*- # # Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2019 All Rights Reserved # # Licensed under CLOUD LINUX LICENSE AGREEMENT # http://cloudlinux.com/docs/LICENSE.TXT from __future__ import print_function from __future__ import absolute_import from __future__ import division from __future__ import unicode_literals from future import standard_library from typing import Dict, Optional, List standard_library.install_aliases() from builtins import * from future.utils import native_str import copy import errno import os import locale import psutil import grp import sys import configparser import getopt import string import shutil import subprocess import secureio import stat import time import fcntl import random import struct import signal import pickle import cagefs_da_lib import re import yaml import glob from collections import defaultdict from enum import Enum from clcagefslib.const import BASEDIR, SYMLINKS from clcagefslib.fs import get_linksafe_gid, get_user_prefix from clcagefslib.io import read_file from clcagefslib.selector.configure import configure_alt_php, is_ea4_enabled, read_cpanel_ea4_php_conf, switch_symlink from clcagefslib.selector.paths import get_alt_paths from cldetectlib import is_plesk, is_cpanel from clcommon.utils import ( ExternalProgramFailed, create_symlink, is_socket_file, is_may_detach_mounts_enabled, mod_makedirs, ) from clcommon.clproc import ProcLve from clcommon import ClPwd, reload_processes, clconfpars, clcaptain from clcommon.clfunc import unicodeify, byteify from cagefslib import ( stripslash, CageFSException, SYSTEMD_JOURNAL_SOCKET, is_new_syslog_socket_used, relative_symlink, is_running_without_lve, ) from logs import logger import cagefshooks import tempfile import unshare import cagefs_without_lve_lib import cagefs_universal_hook_lib LVECTL = '/usr/sbin/lvectl' if is_running_without_lve(): # mock lvectl, because it will not be used in containers probably LVECTL = '/bin/true' UMOUNT = '/bin/umount' MOUNT = '/bin/mount' LVE_UMOUNT = "/bin/lve_umount" BASEDIR_UID = '/var/cagefs.uid' SKELETON = '/usr/share/cagefs-skeleton' SKELETON_NAME = '/cagefs-skeleton/' LIBDIR = '/usr/share/cagefs' INIPREFIX = '/etc/cagefs/' MP_PREFIX = '/usr/share/cagefs/' CONFIG_DIR = '/etc/cagefs/conf.d/' ETC_MPFILE = INIPREFIX + 'cagefs.mp' PREV_MPFILE = MP_PREFIX + 'cagefs.mp.prev' LOCKNAME = '/usr/share/cagefs/.lock' FUSE_WHITE_LIST = '/etc/cagefs/etc.safe/etc.system' FUSE_SAFE_LIST = '/etc/cagefs/etc.safe/etc.safe' FUSE_DIR = '/etc/cagefs/etc.safe' FILES_LIST = '/usr/share/cagefs/skeleton.files.list' LIBS_LIST = '/usr/share/cagefs/skeleton.libs.list' PASSWD_CACHE = '/usr/share/cagefs/passwd.cache' WORK_CONFIG_DIR = "/usr/share/cagefs/conf.d" EXCLUDE_PATH = '/etc/cagefs/exclude' EXCLUDE_SAVE_PATH = '/usr/share/cagefs/exclude' MIN_UID = 500 MIN_UID_FILENAME = "/etc/cagefs/cagefs.min.uid" SERVICE_CAGEFS_LOCK = "/var/lock/subsys/cagefs" DISABLE_ETCFS = "/etc/cagefs/etc.safe/disable.etcfs" DIFF = "/usr/bin/diff" PROXYEXEC_SOCKET_DIR_OLD = "/var/run/proxyexec/cagefs.sock" PROXYEXEC_SOCKET_DIR = "/var/lib/proxyexec/cagefs.sock" BLACK_LIST_FILE = "/etc/cagefs/black.list" PLUGIN_STATE = "/usr/share/cagefs-plugins/install-cagefs-plugin.py" EMPTY_DIR = '/usr/share/cagefs/.cagefs.empty' STD_PACKAGES_FILE = "/usr/share/cagefs/exclude.packages" PROXY_COMMANDS = "/etc/cagefs/proxy.commands" REMOUNT_FLAG = '/usr/share/cagefs/need.remount' INFO_LOG_FILE = "/var/log/cagefs.log" LICENSE_TIMESTAMP_FILE = '/var/lve/lveinfo.ver' SELECTOR_CONF_DIR_TEMPLATE = '/usr/share/l.v.e-manager/cl.{}' DEV_SHM_OPTIONS = "/etc/cagefs/dev.shm.options" DEBUG_CAGEFS_MARKER = '/etc/cagefs/enabled_debug' disabled_dir = INIPREFIX + 'users.disabled' enabled_dir = INIPREFIX + 'users.enabled' SKELETON_INITIALIZED = 'Initialized' SKELETON_NOT_INITIALIZED = 'Not initialized' kernel_header = os.uname().release # Default RPM packages class DefaultPackages(Enum): """Default packages, used in cagefs --init""" common_packages = [ 'tcl', 'cpp', 'gcc', 'automake', 'autoconf', 'm4', 'mc', 'ghostscript', 'fontconfig', 'aspell', 'aspell-en', 'hunspell', 'coreutils', 'python3-virtualenv', 'libxml2', 'recode', 'crypto-policies', 'snmptrapd', 'unixodbc', 'openssl', 'alt-libicu', 'enchant', 'curl', 'cpanel-git', # git + 'git', ] ubuntu = [ 'imagemagick', 'libmagick++-dev', 'perlmagick', 'expat', 'libexpat1-dev', 'libltdl7', 'libnss3', 'build-essential', f'linux-headers-{kernel_header}', 'gfortran', 'lib32gcc-10-dev', 'g++', 'libtext-pdf-perl', 'libedit2', 'hunspell-en-us', 'libcogl-pango-dev', 'python3.8', 'libc-client2007e', 'libodbc1', 'libmhash2', 'libmcrypt4', 'libxslt1.1', 'libtidy5deb1', 'libicu66', 'libicu-dev', 'tmpreaper', 'libgpg-error0', 'postgresql', 'postgresql-contrib', 'libpng-dev', 'libgmp3-dev', 'libpam-modules', 'bzip2', 'libpam-cracklib', 'ncdu', 'libidn11', 'libc-client2007e', 'db5.3-util', 'libncurses6', 'slapd', 'libxpm4', 'libgcrypt20', 'libsasl2-2', 'zlib1g', 'snmpd', 'snmp', 'libsnmp-dev', 'libmm-dev', 'libfreetype6', 'libfreetype6-dev', 'libssh2-1', 'geoip-database', 'ffmpeg', 'dnsutils', 'libgs9', 'libgs-dev', 'libgs9-common', ] centos = [ 'ImageMagick', 'ImageMagick-c++', 'ImageMagick-c++-devel', 'ImageMagick-devel', 'ImageMagick-perl', 'cloudlinux-ImageMagick', 'cloudlinux-ImageMagick-c++', 'cloudlinux-ImageMagick-c++-devel', 'cloudlinux-ImageMagick-devel', 'expat', 'expat-devel', 'libtool-ltdl', 'nss', 'nss-softokn', # - 'compat-glibc-headers', # not exist in centos 'glibc-headers', 'kernel-headers', 'compat-libgcc-296', # - 'gcc-gfortran', 'compat-gcc-34-c++', # - 'compat-gcc-34-g77', # - 'libgcc', 'gcc-c++', 'compat-gcc-34', # - 'redhat-rpm-config', 'fontpackages-filesystem', # - 'perl-Text-PDF', 'pdf-tools', # - 'perl-PDF-Reuse', # - 'libedit', 'hunspell-en', 'git-core', 'pango', 'mktemp', 'scl-utils', # - 'python36', # + python3.8 'libc-client-2007e', 'unixODBC-libs', 'mhash', 'tcp_wrappers', # - 'compat-libstdc++', # - 'libmcrypt', 'libxslt', 'libtidy', 'libicu', 'libicu-devel', 'tmpwatch', 'net-snmp', 'libgpg-error', 'postgresql-libs', 'libpng', 'gmp', 'pam', 'bzip2-libs', 'cracklib', 'ncurses', 'libidn', 'libc-client-2004g', 'db4', 'ncurses-libs', 'openldap', 'libXpm', 'libgcrypt', 'cyrus-sasl-lib', 'zlib', 'net-snmp-libs', 'libmm', 'freetype', 'freetype-devel', 'curl-devel', # - 'libssh2', 'GeoIP', 'cyrus-sasl', 'ffmpeg-libs', 'termcap', # - 'bind-utils', 'libgs', 'libgs-devel' ] if 'ubuntu' in os.uname().version.lower(): STD_PACKAGES = DefaultPackages.common_packages.value + DefaultPackages.ubuntu.value else: STD_PACKAGES = DefaultPackages.common_packages.value + DefaultPackages.centos.value # This line is changed in %install section of securelve.spec cagefs_version = '7.6.23-1.el8.cloudlinux' sys.path.append(LIBDIR) import cagefslib import repair_homes from signals_handlers import sigterm_check cagefslib.FUSE_WHITE_LIST = FUSE_WHITE_LIST cagefslib.FUSE_SAFE_LIST = FUSE_SAFE_LIST cagefslib.SKELETON = SKELETON # List of spamassasin dirs for add to Cagefs SPAMASSASSIN_DIRS_FOR_CAGEFS = ['/usr/local/cpanel/3rdparty/bin', '/var/lib/spamassassin'] def save_passwd_cache(pw=None): if pw == None: pw = secureio.get_pwd_dict() try: umask_saved = os.umask(0o77) pf = open(PASSWD_CACHE, 'wb') # byteify because python2 uses bytes # and we in python3 use unicode pickle.dump(byteify(pw), pf, protocol=2) pf.close() os.umask(umask_saved) os.chmod(PASSWD_CACHE, 0o600) except Exception as err: secureio.print_error("saving", PASSWD_CACHE, '-', err) def load_passwd_cache(): pw = {} if os.path.isfile(PASSWD_CACHE): try: pf = open(PASSWD_CACHE, 'rb') # unicodeify because python2 saves bytes # into this file and we use unicode in python3 pw = unicodeify(pickle.load(pf, encoding=locale.getpreferredencoding())) pf.close() except Exception as err: secureio.print_error("loading", PASSWD_CACHE, '-', err) return pw # Returns list of users whose passwd entry has been changed def get_modified_users(pw_old=None, pw_new=None): if pw_old == None: pw_old = load_passwd_cache() if pw_new == None: pw_new = secureio.get_pwd_dict() users = [] for user in pw_new: try: if pw_new[user] != pw_old[user]: users.append(user) except KeyError: users.append(user) continue save_passwd_cache(pw_new) return users def create_empty_dir(): sigterm_check() if not os.path.lexists(EMPTY_DIR): try: mod_makedirs(EMPTY_DIR, 0o755) except (IOError, OSError): secureio.logging('Error: failed to create ' + EMPTY_DIR, SILENT, 1) sys.exit(1) else: try: os.chmod(EMPTY_DIR, 0o755) except (IOError, OSError): secureio.logging('Error: failed to set permissions to ' + EMPTY_DIR, SILENT, 1) sys.exit(1) def mount_empty_dir(path): dest = SKELETON+path # parent dir exists ? if os.path.isdir(dest): # mount empty dir over parent dir for cmd in ([MOUNT, "-n", "-o", "nosuid", "--bind", EMPTY_DIR, dest], [MOUNT, "-n", "-o", "remount,ro,nosuid,bind", EMPTY_DIR, dest]): ret = subprocess.call(cmd) if ret != 0: secureio.print_error("failed to mount", EMPTY_DIR, '->', dest) sys.exit(1) def etcfs_is_disabled(): return os.path.exists(DISABLE_ETCFS) def get_etc_version(path): fpath = path + cagefslib.ETC_VERSION if os.path.isfile(fpath): try: f = open(fpath, "r") ver = f.readline() f.close() except (IOError, OSError): return 0 ver = ver.rstrip() try: # return version return int(ver) except ValueError: # return 0 as version return 0 else: # return 0 as version return 0 def set_etc_version(path, ver): fpath = path + cagefslib.ETC_VERSION umask_saved = os.umask(0o77) try: f = open(fpath, "w") f.write("%d\n" % ver) f.close() except (IOError, OSError): secureio.logging('Error: failed to write ' + fpath, SILENT, 1) sys.exit(1) os.umask(umask_saved) def copy_etc_version(src, dst): srcpath = src + cagefslib.ETC_VERSION dstpath = dst + cagefslib.ETC_VERSION try: # copy file and metadata shutil.copy2(srcpath, dstpath) except (OSError, IOError, shutil.Error): pass LOGFILE = "/var/log/cagefs-update.log" SILENT = 0 VERBOSE = 0 def remove_log_file(): if os.path.isdir(LOGFILE): secureio.print_error(LOGFILE, "is a directory") sys.exit(1) elif os.path.isfile(LOGFILE): os.remove(LOGFILE) MPDIRS = ['/var/lib/mysql', '/var/lib/dav', '/var/www/cgi-bin', '/opt', '/var/www/php-bin', '/dev/shm', '/var/www/html', '/var/run/pgsql', '/var/passenger', '/dev/pts', '/usr/local/apache/domlogs', '/proc', PROXYEXEC_SOCKET_DIR, '/var/spool/at', '/var/run/dbus', '/usr/local/cpanel/var', '/var/run/nscd', SELECTOR_CONF_DIR_TEMPLATE.format('nodejs'), SELECTOR_CONF_DIR_TEMPLATE.format('python')] MYSQL_SOCK_DIR='/var/lib/mysql' MYSQL_SOCK='/var/lib/mysql/mysql.sock' LITESPEED='/usr/local/lsws' READ_ONLY_MOUNTS = [ '/lib', '/usr/lib', '/lib64', '/usr/lib64', '/usr/include', '/usr/share/locale', '/usr/share/terminfo', '/usr/share/zoneinfo', '/usr/share/vim', '/usr/local/lib/perl5', '/usr/local/lib/php', '/usr/local/cpanel/etc', '/usr/local/cpanel/Cpanel', '/usr/local/cpanel/3rdparty/perl', '/usr/local/cpanel/3rdparty/lib', '/usr/local/cpanel/3rdparty/lib64', '/usr/local/cpanel/3rdparty/share', '/usr/local/cpanel/3rdparty/php', '/usr/local/cpanel/install', '/usr/local/cpanel/lib', '/usr/local/cpanel/htdocs', '/usr/local/cpanel/shared', '/usr/local/cpanel/whostmgr', '/usr/local/cpanel/share', '/usr/local/cpanel/php', '/usr/local/cpanel/libexec', '/usr/local/cpanel/lang', '/usr/local/cpanel/cgi-priv', '/usr/local/cpanel/cpaddons', '/usr/local/cpanel/Whostmgr', '/usr/local/cpanel/img-sys', '/usr/local/cpanel/modules-install', '/usr/local/cpanel/locale', '/usr/local/cpanel/scripts', '/usr/local/cpanel/sbin', '/usr/local/cpanel/base', '/usr/local/cpanel/hooks', '/usr/java', '/usr/saase', '/usr/local/easy', '/var/cpanel/ea4', '/usr/share/man', ] SPLITTED_MOUNTS = ['/var/cpanel/userdata'] SPLITTED_UID_MOUNTS = ['/var/clwpos/uids', '/usr/share/alt-php-xray-tasks'] def is_dev_shm_isolated(): """ Return True when /dev/shm isolation is enabled see CAG-954 for details """ return os.path.isfile(DEV_SHM_OPTIONS) def is_outside_mp_path(path): path = cagefslib.addslash(path) for tmpdir in MPDIRS: if tmpdir[0] == '/' and path.startswith(tmpdir+'/'): return False return True def save_cagefs_mp_backup(): try: shutil.copyfile(ETC_MPFILE, PREV_MPFILE) os.chmod(PREV_MPFILE, 0o600) except: secureio.print_error('copying', ETC_MPFILE, 'to', PREV_MPFILE) def create_mp(force, exit_on_error=False): if not force and os.path.isfile(ETC_MPFILE): print(ETC_MPFILE, "exists") return umask_saved = os.umask(0o22) f = open(ETC_MPFILE, 'w') f.write('# Lines, which start with "/", specify mounts, that are common for all users:\n') for tmpdir in MPDIRS: if is_plesk() and tmpdir == "/var/www/cgi-bin": continue if tmpdir == '/dev/shm' and is_dev_shm_isolated(): # CAG-954: do not add /dev/shm mount to cagefs.mp when /dev/shm isolation is enabled continue if tmpdir[0] == '/' and os.path.isdir(tmpdir): f.write(tmpdir) f.write('\n') # /var/run/postgres should be used on CL5, CL6; /var/run/postgresql should be used on CL7 # for details plz see CAG-593, CAG-528 from cagefsreconfigure import POSTGRES_CL7_FOLDER, POSTGRES_CONF, DEFAULT_POSTGRES_FOLDER if os.path.isdir(POSTGRES_CONF): if os.path.isdir(DEFAULT_POSTGRES_FOLDER): # CL5, CL6 f.write('%s\n' % DEFAULT_POSTGRES_FOLDER) elif os.path.isdir(POSTGRES_CL7_FOLDER): # should never happen f.write('%s\n' % POSTGRES_CL7_FOLDER) else: if os.path.isdir(POSTGRES_CL7_FOLDER): # CL7 f.write('%s\n' % POSTGRES_CL7_FOLDER) elif os.path.isdir(DEFAULT_POSTGRES_FOLDER): # should never happen f.write('%s\n' % DEFAULT_POSTGRES_FOLDER) # detect if MYSQL socket is outside of cagefs if os.path.realpath(MYSQL_SOCK) != MYSQL_SOCK: rpath = os.path.realpath(MYSQL_SOCK) rdir = os.path.dirname(rpath) if rdir == '/tmp': print('Warning: MySQL socket is located in /tmp directory: path', MYSQL_SOCK, 'points to', rpath) print('This is not compatible with CageFS.') print('Please move socket outside of /tmp directory by changing socket= directive in /etc/my.cnf file and restart MySQL.') print('Then execute cagefsctl --create-mp') print('Default socket location -', MYSQL_SOCK) if exit_on_error: f.close() os.unlink(ETC_MPFILE) sys.exit(1) elif rdir != '/' and os.path.isdir(rdir) and is_outside_mp_path(rdir): f.write(rdir) f.write('\n') if os.path.isdir(LITESPEED): f.write(LITESPEED) f.write('\n') f.write('# You can add personal (individual) mounts for users, like below.\n') f.write('# Please, start line with "@" symbol, and then specify path and permissions (comma separated).\n') f.write('# These directories will be virtualized for each user.\n') f.write('@/var/spool/cron,700\n') f.write('@/var/run/screen,777\n') f.write('@'+cagefslib.VAR_RUN_CAGEFS+',700\n') f.write('@/var/cache/php-eaccelerator,777\n') f.write('@/var/php/apm/db,777\n') f.write('# Please add exclamation sign at the beginning of the line if you want to mount path read-only, like below.\n') for tmpdir in READ_ONLY_MOUNTS: if tmpdir[0] == '/' and os.path.isdir(tmpdir): f.write('!'+tmpdir+'\n') f.write('# Please add "%" sign at the beginning of the line if you want to "split" mount by username, like below.\n') for tmpdir in SPLITTED_MOUNTS: if tmpdir[0] == '/' and os.path.isdir(tmpdir): f.write('%'+tmpdir+'\n') f.write('# Please add "*" sign at the beginning of the line if you want to "split" mount by UID, like below.\n') for tmpdir in SPLITTED_UID_MOUNTS: if tmpdir[0] == '/' and os.path.isdir(tmpdir): f.write('*'+tmpdir+'\n') if is_cpanel(): # Setup the spamassasin directories to CageFs on cPanel f.write('\n') #SPAMASSASSIN_DIRS_FOR_CAGEFS = ['/usr/local/cpanel/3rdparty/bin', '/var/lib/spamassassin'] from cagefsreconfigure import BOX_TRAPPER_DIR for line in SPAMASSASSIN_DIRS_FOR_CAGEFS: if os.path.isdir(line): f.write('!'+line+'\n') if os.path.isdir(BOX_TRAPPER_DIR): f.write('!'+BOX_TRAPPER_DIR+'\n') f.write('\n') f.close() os.umask(umask_saved) os.chmod(ETC_MPFILE, 0o600) add_mounts_for_php_selector() add_mounts_for_ea_php_sessions() if is_plesk(): from cagefsreconfigure import add_php_session_dir_plesk add_php_session_dir_plesk() # copy cagefs.mp to cagefs.mp.prev (in order to detect changes of cagefs.mp file in the future) if not os.path.isfile(PREV_MPFILE): save_cagefs_mp_backup() def remove_mount_points(mounts, old_mounts, base_path = SKELETON): # Remove unused mount points for mount in old_mounts: if mount not in mounts: mount = mount.rstrip() try: os.removedirs(base_path + mount) except (OSError, IOError): pass def remove_unused_mount_points(): # Previous mpfile exists ? if os.path.isfile(PREV_MPFILE): # Read previous mount points mp_config_old = MountpointConfig(path=PREV_MPFILE, skip_errors=True, skip_cpanel_check=True) # Reading of old mpfile is successful ? if mp_config_old.common_mounts: # Read current mp-file mp_config = MountpointConfig() # Remove common mount points which are not used (from cagefs-skeleton) remove_mount_points(mp_config.common_mounts, mp_config_old.common_mounts) # Remove personal mount points which are not used (from cagefs-skeleton) remove_mount_points(mp_config.personal_mounts, mp_config_old.personal_mounts) # Remove splitted by name mount points which are not used (from cagefs-skeleton) remove_mount_points(mp_config.splitted_by_username_mounts, mp_config_old.splitted_by_username_mounts) # Remove splitted by UID mount points which are not used (from cagefs-skeleton) remove_mount_points(mp_config.splitted_by_uid_mounts, mp_config_old.splitted_by_uid_mounts) # Remove personal mount points (which are not used) from home dirs pw = secureio.clpwd.get_user_dict() for user in pw: line = pw[user] homepath = cagefslib.stripslash(line.pw_dir) cagefspath = homepath + '/.cagefs' secureio.set_user_perm(line.pw_uid, line.pw_gid) remove_mount_points(mp_config.personal_mounts, mp_config_old.personal_mounts, cagefspath) secureio.set_root_perm() save_cagefs_mp_backup() def dirinjail(testdir, jail): if (testdir[-1]!= '/'): testdir = testdir+'/' return (jail == testdir[:len(jail)]) def user_exists(user): return user in secureio.clpwd.get_user_dict() save_postfix = '.save' def get_user_mode(): if os.path.isdir(disabled_dir): if os.path.isdir(enabled_dir): return 'Error' else: return 'Enable All' elif os.path.isdir(enabled_dir): return 'Disable All' else: return 'Not Initialized' def check_mode_error(mode = None, raise_exception = False): if mode == None: mode = get_user_mode() if mode == 'Error': if raise_exception: raise CageFSException secureio.print_error('both directories', enabled_dir, 'and', disabled_dir, 'exist.\n', 'Please, run one of the following commands:\n', sys.argv[0], '--enable-all\n', 'to enable all users, except specified in', disabled_dir,'\n', 'or\n', sys.argv[0], '--disable-all\n', 'to disable all users, except specified in', enabled_dir) sys.exit(1) elif mode == 'Not Initialized': if raise_exception: raise CageFSException if os.path.isdir(SKELETON+'/bin'): secureio.print_error('mode has not been selected yet.\n', 'Please, run one of the following commands:\n', sys.argv[0], '--enable-all\n', 'to enable all users, except specified in', disabled_dir, '\nor\n', sys.argv[0], '--disable-all\n', 'to disable all users, except specified in', enabled_dir) else: secureio.print_error('CageFS is not initialized. Use "'+sys.argv[0]+' --init" to initialize CageFS') sys.exit(1) def cagefs_is_enabled(): return os.path.isdir(disabled_dir) or os.path.isdir(enabled_dir) def save_dir_exists(): return os.path.isdir(disabled_dir+save_postfix) or os.path.isdir(enabled_dir+save_postfix) def save_dir(_dir): if os.path.isdir(_dir): if os.path.isdir(_dir+save_postfix): # This should never happen secureio.logging('Error : directory %s already exists' % (_dir+save_postfix)) else: try: os.rename(_dir, _dir+save_postfix) except (OSError, IOError): secureio.print_error('failed to rename', _dir, 'to', _dir+save_postfix) def restore_dir(_dir): if os.path.isdir(_dir+save_postfix): if os.path.isdir(_dir): # This should never happen secureio.logging('Error : directory %s already exists' % _dir) else: try: os.rename(_dir+save_postfix, _dir) except (OSError, IOError): secureio.print_error('failed to rename', _dir+save_postfix, 'to', _dir) def disable_cagefs(): if not cagefs_is_enabled(): print('CageFS is disabled') return check_mode_error() save_dir(disabled_dir) save_dir(enabled_dir) if not cagefs_is_enabled(): print('CageFS has been disabled') def enable_cagefs(): if cagefs_is_enabled(): print('CageFS is enabled') return restore_dir(disabled_dir) restore_dir(enabled_dir) check_mode_error() if cagefs_is_enabled(): print('CageFS has been enabled') def check_save_dir(raise_exception=False): if save_dir_exists(): if cagefs_is_enabled(): # This should never happen if raise_exception: raise CageFSException secureio.print_error('CageFS is enabled, but "saved" lists of users exist\n', 'Please, remove '+INIPREFIX+'*'+save_postfix) sys.exit(1) else: if raise_exception: raise CageFSException # This message is parsed in CageFS plugins for control panels print('CageFS is disabled.') print('Please, run "cagefsctl --enable-cagefs" to enable CageFS.') sys.exit(1) def print_user_mode(): check_save_dir() mode = get_user_mode() print('Mode:', mode) def set_user_mode(enable_all = True): check_save_dir() # clear permissions (delete both enabled_dir and disabled_dir) try: shutil.rmtree(enabled_dir, False) except (OSError, IOError, shutil.Error): pass try: shutil.rmtree(disabled_dir, False) except (OSError, IOError, shutil.Error): pass if enable_all: # enable all users except specified in disabled_dir try: mod_makedirs(disabled_dir, 0o751) except OSError: pass else: # disable all users except specified in enabled_dir try: mod_makedirs(enabled_dir, 0o751) except OSError: pass # Exclude system users check_exclude() print_user_mode() exclude_user_list_cache = {} def get_exclude_user_list(exclude_path = EXCLUDE_PATH): global exclude_user_list_cache if exclude_path in exclude_user_list_cache: return exclude_user_list_cache[exclude_path] user_list = [] if os.path.isdir(exclude_path): for exclude_file_path in os.listdir(exclude_path): path = os.path.join(exclude_path, exclude_file_path) if os.path.isfile(path) and exclude_file_path != '.htaccess': try: f = open(path, "r") for line in f.readlines(): line = line.rstrip() if line == '': continue user_list.append(line) f.close() except IOError: secureio.print_error("reading", exclude_file_path) exclude_user_list_cache[exclude_path] = user_list return user_list def filter_users(user_list): return list(set(user_list) - set(get_exclude_user_list())) def toggle_file(_dir, username, enable, prefix=None): if prefix == None: prefix = get_user_prefix(username) fname = '/'+prefix+'/'+username if enable: try: os.remove(_dir + fname) except (IOError, OSError): pass remove_htaccess(_dir + '/'+prefix) try: os.rmdir(_dir + '/'+prefix) except (IOError, OSError): pass else: try: mod_makedirs(_dir+'/'+prefix, 0o751) except (IOError, OSError): pass try: open(_dir + fname, 'w').close() os.chmod(_dir + fname, 0o644) except (IOError, OSError): pass def toggle_user(username, enable): check_save_dir() mode = get_user_mode() check_mode_error(mode) try: pw = secureio.clpwd.get_pw_by_name(username) except ClPwd.NoSuchUserException: secureio.print_error('user', username, 'does not exist') return if pw.pw_uid < MIN_UID: secureio.print_error('user', username, 'should have UID >=', MIN_UID) return username_list = secureio.clpwd.get_names(pw.pw_uid) if mode == 'Enable All': for tmp_username in username_list: toggle_file(disabled_dir, tmp_username, enable) elif mode == 'Disable All': for tmp_username in username_list: toggle_file(enabled_dir, tmp_username, not enable) def print_users(users, users_per_line = 5, message = 'users'): users_count = len(users) if users_count != 0: users.sort() print(users_count, message) name = -1 for name in range(users_count // users_per_line): print('\t'.join(users[name*users_per_line:(name+1)*users_per_line])) print('\t'.join(users[(name+1)*users_per_line:])) # Returns list of users from configuration directory def get_list_of_users_from_config_dir(_dir): users = [] pw = secureio.clpwd.get_user_dict() for subdir in os.listdir(_dir): if os.path.isdir(os.path.join(_dir, subdir)): for _file in os.listdir(os.path.join(_dir, subdir)): if (_file in pw) and (subdir == get_user_prefix(_file)): users.append(_file) return users # _dir == configuration directory (users.enabled for DISABLE_ALL mode, and users.disabled for ENABLE_ALL mode) def get_list_of_users_from_passwd(_dir): # get all users from /etc/passwd users = set(secureio.clpwd.get_user_dict()) # exclude users specified in config dir exc_users = set(get_list_of_users_from_config_dir(_dir)) return list(users - exc_users) # enabled == True : return list of enabled users # enabled == False : return list of disabled users def get_list_of_users(enabled, raise_exception=False): check_save_dir(raise_exception) mode = get_user_mode() check_mode_error(mode, raise_exception) if mode == 'Enable All': if enabled: return get_list_of_users_from_passwd(disabled_dir) else: return get_list_of_users_from_config_dir(disabled_dir) elif mode == 'Disable All': if enabled: return get_list_of_users_from_config_dir(enabled_dir) else: return get_list_of_users_from_passwd(enabled_dir) def get_enabled_users(): if cagefs_is_enabled(): return get_list_of_users(True) return [] # enabled == True : list enabled users # enabled == False : list disabled users def list_users(enabled): check_save_dir() mode = get_user_mode() check_mode_error(mode) if mode == 'Enable All': if enabled: print_users(get_list_of_users_from_passwd(disabled_dir), 1, 'enabled user(s)') else: print_users(filter_users(get_list_of_users_from_config_dir(disabled_dir)), 1, 'disabled user(s)') elif mode == 'Disable All': if enabled: print_users(get_list_of_users_from_config_dir(enabled_dir), 1, 'enabled user(s)') else: print_users(filter_users(get_list_of_users_from_passwd(enabled_dir)), 1, 'disabled user(s)') def check_exclude(ex_list = None): # check if CageFS is disabled if save_dir_exists(): if cagefs_is_enabled(): # This should never happen secureio.print_error('CageFS is enabled, but "saved" lists of users exist\n', 'Please, remove '+INIPREFIX+'*'+save_postfix) sys.exit(1) else: # CageFS is disabled return mode = get_user_mode() # Read "new" exclude list if ex_list == None: ex_list = get_exclude_user_list() # get all users from /etc/passwd pw = secureio.clpwd.get_user_dict() # Disable users in "new" exclude list for username in ex_list: if username in pw: if mode == 'Enable All': toggle_file(disabled_dir, username, False) elif mode == 'Disable All': toggle_file(enabled_dir, username, True) # Read "old" saved copy of exclude list old_ex_list = get_exclude_user_list(EXCLUDE_SAVE_PATH) # Enable users from "old" list that do not exist in "new" exclude list for username in old_ex_list: if username not in ex_list and username in pw: if mode == 'Enable All': toggle_file(disabled_dir, username, True) elif mode == 'Disable All': toggle_file(enabled_dir, username, False) # Save "new" exclude list (with concurrency in mind) if os.path.isdir(EXCLUDE_PATH): if not os.path.isdir(EXCLUDE_SAVE_PATH): try: mod_makedirs(EXCLUDE_SAVE_PATH, 0o750) except OSError: pass tmp_dir = None try: for f in os.listdir(EXCLUDE_SAVE_PATH): path = os.path.join(EXCLUDE_SAVE_PATH, f) orig_path = os.path.join(EXCLUDE_PATH, f) if os.path.isfile(path) and not os.path.isfile(orig_path): try: os.unlink(path) except OSError: pass tmp_dir = tempfile.mkdtemp(dir=EXCLUDE_SAVE_PATH) for f in os.listdir(EXCLUDE_PATH): tmp_path = os.path.join(tmp_dir, f) shutil.copy(os.path.join(EXCLUDE_PATH, f), tmp_path) os.rename(tmp_path, os.path.join(EXCLUDE_SAVE_PATH, f)) except (OSError, IOError, shutil.Error): secureio.print_error("copying", EXCLUDE_PATH, "to", EXCLUDE_SAVE_PATH) finally: if tmp_dir: shutil.rmtree(tmp_dir, True) def clean_var_cagefs(): bdir = '/var/cagefs' pw_db = secureio.clpwd.get_user_dict() if os.path.isdir(bdir): for prefix in os.listdir(bdir): if os.path.isdir(os.path.join(bdir, prefix)): for username in os.listdir(os.path.join(bdir, prefix)): path = os.path.join(bdir, prefix, username) if os.path.islink(path) or ((not path.endswith('.lock')) and os.path.isfile(path)): try: os.remove(path) except (OSError, IOError): pass elif os.path.isfile(path): username = username[:-len('.lock')] if (username not in pw_db) or (prefix != get_user_prefix(username)): try: os.remove(path) except (OSError, IOError): pass elif os.path.isdir(path): if (username not in pw_db) or (prefix != get_user_prefix(username)): shutil.rmtree(path, True) try: os.rmdir(os.path.join(bdir, prefix)) except (IOError, OSError): pass def clean_config_dir(_dir): ex_list = get_exclude_user_list() pw_db = secureio.clpwd.get_user_dict() for subdir in os.listdir(_dir): if os.path.isdir(os.path.join(_dir, subdir)): for _file in os.listdir(os.path.join(_dir, subdir)): if (_file not in ex_list) and ( (_file not in pw_db) or (subdir != get_user_prefix(_file)) ): toggle_file(_dir, _file, True, subdir) # Remove files that correspond to non-existing users or users with UID < MIN_UID def clean_config_dirs(): if os.path.isdir(disabled_dir): clean_config_dir(disabled_dir) if os.path.isdir(enabled_dir): clean_config_dir(enabled_dir) # Returns random string def id_generator(size=6, chars=string.ascii_uppercase + string.digits): return ''.join(random.choice(chars) for _ in range(size)) def migrate_config_dir(_dir): temp_dir = _dir + '.' + id_generator() try: os.rename(_dir, temp_dir) except (OSError, IOError): secureio.print_error('failed to rename', _dir, 'to', temp_dir) sys.exit(1) try: mod_makedirs(_dir, 0o751) except (OSError, IOError): secureio.print_error('failed to create', _dir) sys.exit(1) pw_db = secureio.clpwd.get_user_dict() for subdir in os.listdir(temp_dir): if os.path.isdir(os.path.join(temp_dir, subdir)): for _file in os.listdir(os.path.join(temp_dir, subdir)): if _file in pw_db: # create prefix directory and _file for user toggle_file(_dir, _file, False) # remove temp dir shutil.rmtree(temp_dir, True) # Returns True if new prefixes are used def new_prefixes_are_used(): pw_db = secureio.clpwd.get_user_dict() for _dir in [enabled_dir, disabled_dir, enabled_dir+save_postfix, disabled_dir+save_postfix]: if os.path.isdir(_dir): for subdir in os.listdir(_dir): if os.path.isdir(os.path.join(_dir, subdir)): for _file in os.listdir(os.path.join(_dir, subdir)): if (_file in pw_db) and (subdir != get_user_prefix(_file)): return False return True def migrate_to_new_prefixes(): if not new_prefixes_are_used(): if os.path.isdir(disabled_dir): migrate_config_dir(disabled_dir) if os.path.isdir(enabled_dir): migrate_config_dir(enabled_dir) if os.path.isdir(disabled_dir+save_postfix): migrate_config_dir(disabled_dir+save_postfix) if os.path.isdir(enabled_dir+save_postfix): migrate_config_dir(enabled_dir+save_postfix) BASEDIRS_FILE = '/etc/cagefs/cagefs.base.home.dirs' basedirs = None def mount_base_dir_enabled(): global basedirs if os.path.isfile(BASEDIRS_FILE): if basedirs == None: basedirs = read_file(BASEDIRS_FILE) if basedirs[0].rstrip() == "mount_basedir=1": return True return False def get_base_dir(homepath): for reg_exp in basedirs: reg_exp = reg_exp.rstrip() if reg_exp == "mount_basedir=1" or reg_exp == "mount_basedir=0": continue m = re.search(reg_exp, homepath) if m != None: return m.group() return '' def get_mounted_users_old(fix_permissions=False): """ Returns list of users which are currently mounted in CageFS. Used when /proc/sys/fs/may_detach_mounts set to 0 (disabled) or does not exist :param fix_permissions: when True == fix permissions of directories (mount points) for users' home directories inside /var/cagefs :type fix_permissions: bool """ base_dir_flag = mount_base_dir_enabled() pw_db = secureio.clpwd.get_user_dict() res = set() # scan directories of users in /var/cagefs if os.path.isdir(BASEDIR): for prefix in os.listdir(BASEDIR): if os.path.isdir(os.path.join(BASEDIR, prefix)): for user in os.listdir(os.path.join(BASEDIR, prefix)): try: pw = pw_db[user] except KeyError: # user does not exist continue if base_dir_flag: base_dir = get_base_dir(pw.pw_dir) if base_dir == '': continue mount_point_path = os.path.join(BASEDIR, prefix, user) + base_dir else: mount_point_path = os.path.join(BASEDIR, prefix, user) + pw.pw_dir if os.path.isdir(mount_point_path): if fix_permissions: try: os.chmod(mount_point_path, 0o755) except OSError as e: print('Error: failed to set permissions to directory', mount_point_path, ':', str(e)) else: remove_htaccess(mount_point_path) try: os.rmdir(mount_point_path) except OSError: # mount point is busy - user is mounted for user2 in cagefslib.get_all_users_with_uid(pw.pw_uid): res.add(user2) continue # recreate mount point try: umask_saved = os.umask(0) os.mkdir(mount_point_path, 0o755) os.umask(umask_saved) except OSError: pass return list(res) def get_mounted_users_new(): """ Returns list of users which are currently mounted in CageFS. Used when /proc/sys/fs/may_detach_mounts set to 1 (enabled) """ users = get_enabled_users() pw_db = secureio.clpwd.get_user_dict() mounted_users = [] for user in users: res = subprocess.run(['/bin/lve_suwrapper', '-meck', str(pw_db[user].pw_uid), '/usr/bin/stat', '/var/.cagefs'], capture_output=True, cwd='/') if res.returncode == 0: mounted_users.append(user) return mounted_users def get_mounted_users(fix_permissions=False): """ Returns list of users which are currently mounted in CageFS. """ if is_may_detach_mounts_enabled(): return get_mounted_users_new() return get_mounted_users_old(fix_permissions) def get_logged_in_users(): try: pl = subprocess.Popen(['/bin/ps','aux'], stdout=subprocess.PIPE, text=True).communicate()[0] except OSError: secureio.print_error('failed to run', 'ps', "aux") sys.exit(1) pattern = re.compile(r'sshd:[a-z_][a-z0-9_-]*[$]?@pts',re.IGNORECASE) lst = [] for i in pl.split('\n'): line = i.split() #line format #USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND #0 1 2 3 4 5 6 7 8 9 10+ if len(line) > 0: command = ''.join(line[10:]) # looking where command start with sshd:USERNAME@pts if pattern.match(command): try: #check if name so long, and ps show UID uid = int(line[0]) lst.extend(secureio.clpwd.get_names(uid)) except ValueError: #nope.. ps show username lst.append(line[0]) pass except ClPwd.NoSuchUserException: secureio.print_error('Can`t get user name for UID ',uid ) sshd_set = set(lst) mounted_set = set(get_mounted_users()) result_set = sshd_set & mounted_set return list(result_set) def get_lve_list() -> List[int]: """ Return list of id's for existing LVEs """ lve_list = [] proc_lve = ProcLve() for lve_id in proc_lve.lve_id_list(): lve_list.append(lve_id) if proc_lve.resellers_supported(): for lvp_id in proc_lve.lvp_id_list(): for lve_id in proc_lve.lve_id_list(lvp_id=lvp_id): lve_list.append(lve_id) return lve_list # Returns True if error has occured def umount_list(_list): _list.sort() _list.reverse() error = False for line in _list: if len(line) > 0 and line[0] == '/': line = line.rstrip() try: # run the "umount" command and suppress it's output p = subprocess.Popen([UMOUNT, "-l", line],\ stdout=subprocess.PIPE, stderr=subprocess.PIPE) p.communicate() # check return code of the child if p.returncode != 0: error = True except OSError: secureio.print_error('failed to run', UMOUNT, "-l", line) error = True return error # Returns True if error has occured def umount_dir(path): try: ret = subprocess.call([UMOUNT, "-l", SKELETON+path]) if ret != 0: secureio.print_error("failed to unmount", SKELETON+path) return True except OSError: secureio.print_error('failed to run', UMOUNT, "-l", SKELETON+path) return True return False def destroy_and_recreate_all(): """ Run lvectl apply all Returns True if error has occured """ ATTEMPTS = 3 for _ in range(ATTEMPTS): error = False try: # run the command and suppress it's output p = subprocess.Popen( [LVECTL, "destroy-and-recreate-all",], stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) p.communicate() # check return code of the child if p.returncode != 0: error = True else: break except OSError: secureio.print_error('failed to run', LVECTL, "destroy-and-recreate-all") error = True if error: secureio.print_error(LVECTL, "destroy-and-recreate-all failed") return error def destroy_all(): """ Destroy all LVEs Returns True if error has occured """ ATTEMPTS = 3 for _ in range(ATTEMPTS): error = False try: # run the command and suppress it's output p = subprocess.Popen([LVECTL, "destroy", "all", "--force"],\ stdout=subprocess.PIPE, stderr=subprocess.PIPE) p.communicate() # check return code of the child if p.returncode != 0: error = True else: break except OSError: secureio.print_error('failed to run', LVECTL, "destroy all") error = True if error: secureio.print_error(LVECTL, "destroy all failed") return error def destroy_lve(uids): """ Run lvectl destroy for specified uids :param uids: list of integers (UIDs) :type uids: iterable Returns True if error has occured """ error = False # create input string for subprocess (lvectl) s = '' for uid in uids: s = s + str(uid) + '\n' try: # run the command and suppress it's output p = subprocess.Popen([LVECTL, "destroy-many"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) # send input string to suprocess p.communicate(s) except OSError: secureio.print_error('failed to run', LVECTL, "destroy-many") error = True return error def apply_lve(uids): """ Run lvectl apply for specified uids :param uids: list of integers (UIDs) :type uids: iterable Returns True if error has occured """ error = False # create input string for subprocess (lvectl) s = '' for uid in uids: s = s + str(uid) + '\n' try: # run the command and suppress it's output p = subprocess.Popen([LVECTL, "apply-many"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) # send input string to suprocess p.communicate(s) except OSError: secureio.print_error('failed to run', LVECTL, "apply-many") error = True return error def get_uids(users): pw_db = secureio.clpwd.get_user_dict() uids = [] for user in users: try: uids.append(pw_db[user].pw_uid) except KeyError: continue return uids def remove_duplicates(_list): res = [] for i in _list: if i not in res: res.append(i) return res def remount(users): """ Remount list of users. Skeleton should be mounted/unmounted before call of this function Returns True if error has occured :param users: list of usernames :type users: iterable """ error = False if is_running_without_lve(): if delete_namespaces(users): error = True if create_namespaces(users, do_mount_skel=False): error = True return error # Get UIDs of users uids = get_uids(users) uids = remove_duplicates(uids) if destroy_lve(uids): error = True time.sleep(1) if apply_lve(uids): error = True return error # Remount all users. Skeleton should be mounted/unmounted before call of this function # Returns True if error has occured def remount_all(): error = False if is_running_without_lve(): if delete_namespaces(): error = True if create_namespaces(do_mount_skel=False): error = True else: if destroy_and_recreate_all(): error = True return error def files_exist(_list): for _file in _list: if not os.path.exists(SKELETON+_file): return False return True # Returns True if there is any mounted directory in cagefs-skeleton def skeleton_is_mounted(skeleton=None): if skeleton is None: skeleton = SKELETON mounts = open("/proc/mounts", "r") while True: line = mounts.readline() if line == '': break if line.find(skeleton+'/') != -1: mounts.close() return True mounts.close() return False def cagefs_fuse_is_mounted(): if etcfs_is_disabled(): return files_exist(['/var/log/messages']) else: return files_exist(['/etc/passwd', '/var/log/messages']) # Runs "service cagefs-fuse" with specified command ("start", "restart" or "stop") # Returns True if error has occured def cagefs_fuse(command): ATTEMPTS = 3 for _ in range(ATTEMPTS): error = False try: # run the command and suppress it's output p = subprocess.Popen(["/sbin/service", "cagefs-fuse", command],\ stdout=subprocess.PIPE, stderr=subprocess.PIPE) p.communicate() # check return code of the child if p.returncode != 0: error = True except OSError: secureio.print_error('failed to run "service cagefs-fuse '+command+'"') error = True if command == 'start': if cagefs_fuse_is_mounted(): error = False break else: command = 'restart' error = True elif command == 'restart': if cagefs_fuse_is_mounted(): error = False break else: error = True elif command == 'stop': if not cagefs_fuse_is_mounted(): error = False break else: error = True else: break if error: secureio.print_error("executing", '"service cagefs-fuse', command+'"') return error def proxyexecd_is_socket(): return os.path.lexists(os.path.join(PROXYEXEC_SOCKET_DIR, 'socket')) def cagefs_proxyexecd(command): error = False try: # run the command and suppress it's output p = subprocess.Popen(["/sbin/service", "proxyexecd", command],\ stdout=subprocess.PIPE, stderr=subprocess.STDOUT) p.communicate() if p.returncode != 0: error = True if command == 'start' or command == 'restart': p = subprocess.Popen(["/sbin/service", "proxyexecd", "status"],\ stdout=subprocess.PIPE, stderr=subprocess.STDOUT) p.communicate() if p.returncode != 0: error = True except OSError: secureio.print_error('failed to run "service proxyexecd '+command+'"') error = True if error: secureio.print_error("executing", '"service proxyexecd', command+'"') return error def create_mount_points(_list, mode = 0o755): umask_saved = os.umask(0) for path in _list: if os.path.islink(SKELETON+path): try: os.unlink(SKELETON+path) except (IOError, OSError): pass if not os.path.isdir(SKELETON+path): try: mod_makedirs(SKELETON+path, mode) except (IOError, OSError): pass os.umask(umask_saved) def check_mp_file(): if not os.path.isfile(ETC_MPFILE): if not SILENT: secureio.print_error('file', ETC_MPFILE, 'not found\n','Please, run\n',sys.argv[0], '--create-mp') sys.exit(1) mp_file = read_file(ETC_MPFILE) if add_new_line(mp_file): cagefslib.write_file(ETC_MPFILE, mp_file) def remove_service_lockfile(): try: os.remove(SERVICE_CAGEFS_LOCK) except (IOError, OSError): pass def create_service_lockfile(): try: open(SERVICE_CAGEFS_LOCK, 'w').close() except (IOError, OSError): pass # Returns True if error has occured def umount_skeleton(save_mounts = True, all_cagefs_mounts = False, current_namespace_only=False, all_namespaces=False): def unmount(): error = True for _ in range(10): mounts = cagefslib.get_mounted_dirs(all_cagefs_mounts) if not mounts: error = False break umount_list(mounts) umount_list([SKELETON]) return error if not current_namespace_only: remove_service_lockfile() error = unmount() if save_mounts: lvectl_start() # CAG-749: unmount CageFS mounts in all mount namespaces (resolve conflict with systemd) if os.path.isfile('/usr/bin/systemctl'): if current_namespace_only: if error: subprocess.run('/bin/umount -l /usr &>/dev/null', shell=True, executable='/bin/bash') error = unmount() or error elif all_namespaces: destroy_all() time.sleep(1) for pid in Execute('/bin/ps --no-headers -xao pid').split(): if pid: subprocess.run("/usr/bin/nsenter -m -t " + pid + " /bin/bash -c 'if /bin/grep -q cagefs /proc/mounts; then" + " /usr/sbin/cagefsctl --unmount-cur-ns; fi' &>/dev/null", shell=True, executable='/bin/bash') return error def unlock(lockfile, lockname = LOCKNAME): try: fcntl.lockf(lockfile, fcntl.LOCK_UN) except (IOError, OSError): secureio.print_error('failed to unlock', lockname) try: lockfile.close() except (IOError, OSError): pass try: os.unlink(lockname) except (IOError, OSError): pass def Execute(command): proc = subprocess.Popen(command, shell=True, executable='/bin/bash', stdout=subprocess.PIPE, text=True, bufsize=-1) return proc.communicate()[0] def get_parents(process: psutil.Process): """ Helper to get all parents list """ parents = [] process = process.parent() while process is not None: parents.append(process) process = process.parent() return parents def save_processes(file_like_io): """ Saves info about parent processes to lock file """ try: current_process = psutil.Process(os.getpid()) cmd_line = current_process.cmdline() parents = get_parents(current_process) except Exception: parents = [] cmd_line = [] file_like_io.write(f'Command line: {" ".join(cmd_line)}\n') for process in parents: file_like_io.write(f'pid: "{process.pid}", name: "{process.name()}", command line "{process.cmdline()}"\n') file_like_io.flush() def print_lock_data(lockname): """ Prints to stdout info from lockfile """ if os.path.exists(lockname): with open(lockname, 'r') as f: lock_content = f.readlines() secureio.print_error('Currently running cagefsctl process info:\n{}'.format('\n'.join(lock_content))) # Acquire lock def acquire_lock(lockname=LOCKNAME, wait=False, quiet=False): try: if not os.path.exists(lockname): open(lockname, 'w').close() lockfile = open(lockname, 'r+') try: fcntl.lockf(lockfile, fcntl.LOCK_EX | fcntl.LOCK_NB) except OSError: if not wait: raise print('Acquiring lock... Please wait... ') fcntl.lockf(lockfile, fcntl.LOCK_EX) print('Lock acquired') if os.path.exists(DEBUG_CAGEFS_MARKER): lockfile.truncate(0) save_processes(lockfile) return lockfile except (IOError, OSError): if not quiet: if (not wait) and len(Execute('ps aux | grep cagefsctl').split('\n')) > 1: secureio.print_error('cagefsctl is already running. please try again later.') if os.path.exists(DEBUG_CAGEFS_MARKER): secureio.print_error('current cagefsctl process information') save_processes(sys.stdout) print_lock_data(lockname) else: secureio.print_error('failed to acquire lock file', lockname) sys.exit(1) def mount_dir(line, read_only = False, ignore_errors = False): if len(line) > 0 and line[0] == '/': line = line.rstrip() if not os.path.isdir(line): secureio.print_error(ETC_MPFILE, "file contains incorrect path -", line, "is NOT a directory or does NOT exist") if ignore_errors: return sys.exit(1) create_mount_points([line]) ret = subprocess.call([MOUNT, "-n", "-o", "nosuid", "--rbind", line, SKELETON+line]) if read_only: if ret == 0: ret = subprocess.call([MOUNT, "-n", "-o", "remount,ro,nosuid,bind", line, SKELETON+line]) else: if ret == 0: if line == '/dev/shm': # CAG-812: mount /dev/shm with noexec option in CageFS ret = subprocess.call([MOUNT, "-n", "-o", "remount,nosuid,noexec,nodev,bind", line, SKELETON+line]) else: ret = subprocess.call([MOUNT, "-n", "-o", "remount,nosuid,bind", line, SKELETON+line]) if ret != 0: secureio.print_error("failed to mount", line) sys.exit(1) def print_cpanel_home_warning(): secureio.logging('Please ensure that the following option in cPanel/WHM is set to blank value (not default "home"):', SILENT, 1) secureio.logging('WHM -> Server Configuration -> Basic cPanel/WHM Setup -> Basic Config -> Additional home directories', SILENT, 1) secureio.logging('When this option is set to "home", cPanel can create home directories in incorrect places.', SILENT, 1) class MountpointType(Enum): COMMON = '/' PERSONAL = '@' SPLITTED_BY_USERNAME = '%' SPLITTED_BY_UID = '*' READ_ONLY = '!' class MountpointConfig: # mpconfig_cache = { # 'path_to_mp_file1': { # MountpointType.COMMON.name: [ ... list of mount points ... ], # MountpointType.PERSONAL.name: [ ... ], # MountpointType.SPLITTED_BY_USERNAME.name: [ ... ], # ... # } # 'path_to_mp_file2': { # ... # } # } mpconfig_cache: Dict[str, Dict[str, List[str]]] = {} def __init__(self, path: str = ETC_MPFILE, skip_errors: bool = False, skip_cpanel_check: bool = False, ignore_cache: bool = False): self.path = path self.skip_errors = skip_errors self.skip_cpanel_check = skip_cpanel_check self.ignore_cache = ignore_cache self.data = self._load() def _load(self) -> Dict[str, List[str]]: """ Load a list of mount points from the config file. Use cached value if exists unless special option specified. """ if self.ignore_cache or self.path not in self.mpconfig_cache: self.mpconfig_cache[self.path] = self._read_config() return copy.deepcopy(self.mpconfig_cache[self.path]) def _read_config(self) -> Dict[str, List[str]]: """ Read the config file and construct a complete list of mount points. """ mounts = defaultdict(list) try: with open(self.path) as f: for line in f: self._process_mount_line(line, mounts) except OSError: if self.skip_errors: return mounts secureio.print_error('failed to read', self.path) sys.exit(1) if not self.skip_cpanel_check and is_cpanel(): self._process_cpanel_mounts(mounts) self._process_proxyexec_socket_mounts(mounts) return mounts def _process_mount_line(self, line: str, mounts: Dict[str, List[str]]) -> None: """ Process the line specifying a mount point. Determine whether the line belongs to any of predefined types and process it accordingly. """ if line.startswith('#'): # skip comments return line = line.rstrip() # Process each line based on its mount point type for mount_type in MountpointType: if line.startswith(mount_type.value): self._process_line(line, mount_type, mounts) break def _process_line(self, line: str, mount_type: MountpointType, mounts: Dict[str, List[str]]) -> None: # For all mounts other than common cut the first symbol, # for personal mounts also cut the part after the comma start = 0 if mount_type == MountpointType.COMMON else 1 end = line.rfind(',') if mount_type == MountpointType.PERSONAL else -1 path = line[start:end] if end != -1 else line[start:] path = path.rstrip() if self._is_invalid_mount_point(path): if self.skip_errors: return secureio.print_error('Invalid mount point', line, 'in file', self.path) sys.exit(1) # For some reason, we add common mounts with '\n' at the end if mount_type == MountpointType.COMMON: mounts[mount_type.name].append(path + '\n') else: mounts[mount_type.name].append(path) # Read only mounts are also being added to common mounts if mount_type == MountpointType.READ_ONLY: mounts[MountpointType.COMMON.name].append(path + '\n') def _is_invalid_mount_point(self, path: str) -> bool: """ Check if a given path is an invalid mount path. """ return path == '/' \ or not path.startswith('/') \ or '/../' in path \ or path.endswith('/..') def _process_cpanel_mounts(self, mounts: Dict[str, List[str]]) -> None: """ Check invalid paths for cPanel. """ common_mounts = mounts[MountpointType.COMMON.name] for line in common_mounts: line = line.rstrip() if 'home' in line and invalid_homes_exist(): secureio.logging(f'Warning: file {self.path} contains line "{line}"', SILENT, 1) print_cpanel_home_warning() break def _process_proxyexec_socket_mounts(self, mounts: Dict[str, List[str]]) -> None: """ Add correct path to the proxyexec socket file. """ common_mounts = mounts[MountpointType.COMMON.name] proxyexec_socket_dir_old_line = f'{PROXYEXEC_SOCKET_DIR_OLD}\n' proxyexec_socket_dir_line = f'{PROXYEXEC_SOCKET_DIR}\n' if proxyexec_socket_dir_old_line in common_mounts: common_mounts.remove(proxyexec_socket_dir_old_line) if proxyexec_socket_dir_line not in common_mounts: common_mounts.append(proxyexec_socket_dir_line) @property def all_mounts(self) -> Dict[str, List[str]]: return self.data @property def common_mounts(self) -> List[str]: return self.data[MountpointType.COMMON.name] @property def personal_mounts(self): return self.data[MountpointType.PERSONAL.name] @property def splitted_by_username_mounts(self): return self.data[MountpointType.SPLITTED_BY_USERNAME.name] @property def splitted_by_uid_mounts(self): return self.data[MountpointType.SPLITTED_BY_UID.name] @property def read_only_mounts(self): return self.data[MountpointType.READ_ONLY.name] # Save mounts in default VE def lvectl_start(): Execute(LVECTL+' start > /dev/null 2>&1') def mount_should_be_readonly(path, read_only_mounts): """ Return True when path is included in one of the read-only paths :param path: mount path to check :type path: string :param read_only_mounts: list of read-only mounts from cagefs.mp file :type read_only_mounts: list """ if path.startswith('/opt/cpanel/ea-php'): return True path = cagefslib.addslash(path) for mount in read_only_mounts: mount = cagefslib.addslash(mount) if path.startswith(mount): return True return False def unsafe_mounts_exist(): """ Search CageFS for mounts that do not have 'nosuid' option Also search for read-write mounts that should be read-only Return True when found, False otherwise For details see CAG-526, CAG-634 """ no_suid_dirs = cagefslib.get_mounted_dirs(without_nosuid = True) no_suid_dirs = list(filter(lambda x: '/proc/sys/fs/binfmt_misc' not in x, no_suid_dirs)) if no_suid_dirs: return True mp_config = MountpointConfig() rw_mounts = cagefslib.get_mounted_dirs(rw_mounts_only = True) for mount in rw_mounts: path = cagefslib.strip_path(mount) if mount_should_be_readonly(path, mp_config.read_only_mounts): return True return False def remount_unsafe_mounts(read_only_mounts): """ Remount all CageFS "unsafe" mounts, so that they become "safe". Make all mounts "nosuid", and make some mounts "read-only" (when needed) For details see CAG-526, CAG-634 """ def remount_dir(old, new, read_only=False): if read_only: ret = subprocess.call([MOUNT, "-n", "-o", "remount,ro,nosuid,bind", '/usr/share/cagefs/not-existing-directory'+old, new]) else: ret = subprocess.call([MOUNT, "-n", "-o", "remount,nosuid,bind", '/usr/share/cagefs/not-existing-directory'+old, new]) if ret != 0: secureio.print_error("failed to mount", old) remount_dir(SKELETON, SKELETON) wo_nosuid = set(cagefslib.get_mounted_dirs(without_nosuid = True)) for path_new in wo_nosuid: path_old = cagefslib.strip_path(path_new) remount_dir(path_old, path_new, read_only=mount_should_be_readonly(path_old, read_only_mounts)) rw_mounts = set(cagefslib.get_mounted_dirs(rw_mounts_only = True)) - wo_nosuid for path_new in rw_mounts: path_old = cagefslib.strip_path(path_new) if mount_should_be_readonly(path_old, read_only_mounts): remount_dir(path_old, path_new, read_only=True) # Personal (private) mount points for user MOUNT_POINTS = [ '/etc', '/var/log', '/var/run/screen', cagefslib.VAR_RUN_CAGEFS, '/var/spool/cron', '/var/cache/php-eaccelerator', '/var/.cagefs' ] def read_symlink(path): """ Return value of symlink or None when error occurs :param path: path to symlink :type path: string """ try: return os.readlink(path) except OSError: pass return None def mount_file(path, do_mount=False, read_only=False): """ Mount one separate file to CageFS using hardlink & mount :param path: path to file :type path: string :param do_mount: when True mount directory with hardlink to CageFS :type do_mount: bool :param read_only: when True mount read-only, read-write otherwise :type read_only: bool """ if not os.path.isfile(path) and not is_socket_file(path): return path = os.path.realpath(path) skel_path = SKELETON + path filename = os.path.basename(path) dir_path = path + '.cagefs' hardlink_path = os.path.join(dir_path, filename) cagefslib.make_dir(dir_path, 0o755, allow_symlink=False) if not os.path.lexists(hardlink_path) or not os.path.samefile(path, hardlink_path): cagefslib.unlink(hardlink_path) try: os.link(path, hardlink_path) except OSError as e: if not os.path.isfile(hardlink_path) or not os.path.samefile(path, hardlink_path): secureio.logging('Error: failed to create hardlink ' + hardlink_path + ' to ' + path + ' : ' + str(e), SILENT, 1) return cagefslib.make_dir(SKELETON + dir_path, 0o755, allow_symlink=False, update_perm=False) spath = read_symlink(skel_path) if spath != hardlink_path: cagefslib.unlink(skel_path) try: os.symlink(hardlink_path, skel_path) except OSError as e: spath = read_symlink(skel_path) if spath != hardlink_path: secureio.logging('Error: failed to create symlink ' + skel_path + ' to ' + hardlink_path + ' : ' + str(e), SILENT, 1) return if do_mount: mount_dir(dir_path, read_only) def _mount_systemd_journal_socket(do_mount: bool) -> None: """ Mount socket of systemd-journal into CageFS :param do_mount: when True mount directory with hardlink to CageFS """ # Check that /dev/log is really symlink and # real file is socket of systemd-journal if not is_new_syslog_socket_used(): return mount_file(SYSTEMD_JOURNAL_SOCKET, do_mount=do_mount) # Directory `SKELETON/dev dir` may be absent # at the moment of CageFS initialization skeleton_dev_dir = os.path.join( SKELETON, 'dev', ) if not os.path.exists(skeleton_dev_dir): cagefslib.make_dir( path=skeleton_dev_dir, perm=0o755, allow_symlink=False, update_perm=False, ) # Create symlink SKELETON/dev/log -> socket of systemd-journal # like as in real fs create_symlink( SYSTEMD_JOURNAL_SOCKET, os.path.join( skeleton_dev_dir, 'log', ), ) def mount_skeleton(remount_users = False): """ Function remounts skeleton and all users !!WARNING!!: part of this logic is duplicated in jail.c from kmoc-lve project :param remount_users: when True, destroy&create LVE&namespaces for all users :type remount_users: bool """ # Ensure that mp-file exists check_mp_file() Execute('/bin/mount --make-rprivate / >/dev/null 2>&1') # Create mount points in skeleton create_mount_points(MOUNT_POINTS) # --remount_all option is used or cagefs-fuse is not running ? # if remount_users or (not cagefs_fuse_is_mounted()): # Restart cagefs-fuse service # if cagefs_fuse('restart'): # sys.exit(1) if remount_users or (not proxyexecd_is_socket()): # Restart proxyexecd service if cagefs_proxyexecd('restart'): sys.exit(1) # Create mount point for tmp directory create_mount_points(['/tmp']) umount_skeleton(save_mounts = False) ret = subprocess.call([MOUNT, "-n", "-o", "nosuid", "--rbind", SKELETON, SKELETON]) if ret != 0: secureio.print_error("failed to mount", SKELETON) # Read mp-file mp_config = MountpointConfig() read_only_mounts = mp_config.read_only_mounts personal_mounts = mp_config.personal_mounts cagefslib.mounts = mp_config.common_mounts create_mount_points(personal_mounts) umask_saved = os.umask(0) # Mount directories specified in mp-file # we sort mounts because of CAG-709 (cagefs mounts should not break systemd services with directives like ProtectSystem=full) for line in sorted(cagefslib.mounts): line = line.rstrip() if line != '/proc' and line != '/tmp' and not line.startswith('/tmp/'): mount_dir(line, read_only = (line in read_only_mounts), ignore_errors = True) # Mount empty dir over user-defined dirs + default /opt/suphp/sbin see CAG-999 try: emptied_dirs_path = "/etc/cagefs/empty.dirs" for filename in os.listdir(emptied_dirs_path): emptied_config = os.path.join(emptied_dirs_path, filename) if os.path.isfile(emptied_config): with open(emptied_config, "r") as emptied_dirs_file: for emptied_dir in emptied_dirs_file: mount_empty_dir(emptied_dir.rstrip()) except IOError as e: secureio.print_error("Error while reading file.", e) setup_cpanel_multiphp(do_mount=True) # LU-640: mount license file to CageFS (needed by cloudlinux-selector to check license) mount_file(LICENSE_TIMESTAMP_FILE, do_mount=True, read_only=True) _mount_systemd_journal_socket(do_mount=True) # Mount /proc directory last mount_dir('/proc') remount_unsafe_mounts(read_only_mounts) # Ensure that mount points exist after mounting of skeleton create_mount_points(MOUNT_POINTS) create_mount_points(personal_mounts) os.umask(umask_saved) # Save mounts in default VE lvectl_start() if remount_users: # Exclude system users BEFORE remounting skeleton check_exclude() # Remount all users remount_all() remove_unused_mount_points() remove_remount_flag() create_service_lockfile() def verify_paths(paths): for path in paths: path2 = os.path.realpath(path) path2 = path2 + '/' if path2.startswith(SKELETON+'/'): secureio.print_error("path", path, "is incorrect") path2 = cagefslib.stripslash(path2) if path2 != path: secureio.print_error("(it refers to", path2, ")") sys.exit(1) class cagefs_init(object): def __init__(self): self.didfiles = [] self.didsections = [] self.diddevices = [] self.didusers = [] self.didgroups = [] def update_paths(self, config, chroot, paths, try_glob = 0): if paths: verify_paths(paths) self.didfiles = cagefslib.copy_binaries_and_libs(chroot, paths, config['force'], config['verbose'], check_libs=1,\ try_hardlink=config['hardlink'], retain_owner=1, try_glob_matching=try_glob, handledfiles=self.didfiles, update=config['update']) def update_alt_php_libs(self, config, chroot): paths = cagefslib.get_alt_php_libs() self.update_paths(config, chroot, paths) def handle_cfg_section(self,config,chroot,cfg,section): if(chroot[-1] == '/'): chroot = chroot[:-1] # first create the chroot jail itself if it does not yet exist if (not os.path.exists(chroot)): print('Creating jail '+chroot) mod_makedirs(chroot, 0o755) # if the parent is setuid or setgid that is not covered by the umask set above, so we remove that os.chmod(chroot, 0o755) sections = cagefslib.config_get_option_as_list(cfg,section,'includesections') for tmp in sections: sigterm_check() if (tmp not in self.didsections): self.handle_cfg_section(config,chroot,cfg,tmp) self.didsections.append(tmp) #libraries, executables, regularfiles and directories are now all handled as 'paths' paths = cagefslib.config_get_option_as_list(cfg,section,'paths') # CAG-936: remove invalid path /usr/local/awstats/ that is added by /usr/local/directadmin/scripts/awstats_process.sh script if section == 'directadmin': try: paths.remove('/usr/local/awstats/') except ValueError: pass paths = paths + cagefslib.config_get_option_as_list(cfg,section,'libraries') paths = paths + cagefslib.config_get_option_as_list(cfg,section,'executables') paths = paths + cagefslib.config_get_option_as_list(cfg,section,'regularfiles') paths = paths + cagefslib.config_get_option_as_list(cfg,section,'directories') self.update_paths(config, chroot, paths, try_glob = 1) paths_w_owner = cagefslib.config_get_option_as_list(cfg,section,'paths_w_owner') self.update_paths(config, chroot, paths_w_owner, try_glob = 1) emptydirs = cagefslib.config_get_option_as_list(cfg,section,'emptydirs') for edir in emptydirs: cagefslib.create_parent_path(chroot,edir, config['verbose'], copy_permissions=1, allow_suid=0, copy_ownership=1) users = [] groups = [] tmplist = cagefslib.config_get_option_as_list(cfg,section,'users') for tmp in tmplist: if (tmp not in self.didusers): users.append(tmp) tmplist = cagefslib.config_get_option_as_list(cfg,section,'groups') for tmp in tmplist: if (tmp not in self.didusers): groups.append(tmp) cagefslib.init_passwd_and_group(FUSE_DIR, users, groups, config['verbose']) cagefslib.init_safe_users_and_groups(FUSE_DIR, users, groups, config['verbose']) cagefslib.init_passwd_and_group(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', users, groups, config['verbose']) cagefslib.init_shadow(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', users, config['verbose']) self.didusers = self.didusers + users self.didgroups = self.didusers + groups devices = cagefslib.config_get_option_as_list(cfg,section,'devices') for tmp in devices: if (tmp not in self.diddevices): cagefslib.create_parent_path(chroot,os.path.dirname(tmp), config['verbose'], copy_permissions=1, allow_suid=0, copy_ownership=1) cagefslib.copy_device(chroot,tmp,config['verbose']) self.diddevices.append(tmp) def update_etc_paths(self, paths): if paths: verify_paths(paths) cagefslib.copy_to_etc(paths) def update_etc_from_section(self, config, cfg, section): sections = cagefslib.config_get_option_as_list(cfg,section,'includesections') for tmp in sections: if (tmp not in self.didsections): self.update_etc_from_section(config,cfg,tmp) self.didsections.append(tmp) #libraries, executables, regularfiles and directories are now all handled as 'paths' paths = cagefslib.config_get_option_as_list(cfg,section,'paths') paths = paths + cagefslib.config_get_option_as_list(cfg,section,'libraries') paths = paths + cagefslib.config_get_option_as_list(cfg,section,'executables') paths = paths + cagefslib.config_get_option_as_list(cfg,section,'regularfiles') paths = paths + cagefslib.config_get_option_as_list(cfg,section,'directories') self.update_etc_paths(paths) paths_w_owner = cagefslib.config_get_option_as_list(cfg,section,'paths_w_owner') self.update_etc_paths(paths_w_owner) users = [] groups = [] tmplist = cagefslib.config_get_option_as_list(cfg,section,'users') for tmp in tmplist: if (tmp not in self.didusers): users.append(tmp) tmplist = cagefslib.config_get_option_as_list(cfg,section,'groups') for tmp in tmplist: if (tmp not in self.didusers): groups.append(tmp) cagefslib.init_passwd_and_group(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', users, groups, config['verbose']) cagefslib.init_shadow(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', users, config['verbose']) self.didusers = self.didusers + users self.didgroups = self.didusers + groups # Return True if mount points are busy (are used by cagefs-fuse) def mount_points_busy(_list): for path in _list: if os.path.isdir(SKELETON+path) and (not os.path.islink(SKELETON+path)): try: shutil.rmtree(SKELETON+path, False) except (IOError, OSError, shutil.Error): return True # recreate mount point try: umask_saved = os.umask(0) os.mkdir(SKELETON+path) os.umask(umask_saved) except (IOError, OSError): pass return False def check_skeleton_not_busy(): # Check that skeleton is unmounted (mount points are not busy) if mount_points_busy(['/tmp']): secureio.print_error('failed to unmount CageFS - skeleton directory is busy.') sys.exit(1) # Stops cagefs-fuse service, unmounts skeleton and all users def unmount_all(remount_users = False, check_busy = False, all_cagefs_mounts = False): if is_running_without_lve(): delete_namespaces() umount_skeleton(all_cagefs_mounts = all_cagefs_mounts, all_namespaces = True) # cagefs_fuse('stop') # CAG-440 # cagefs_proxyexecd('stop') time.sleep(1) if skeleton_is_mounted(): secureio.print_error('failed to unmount cagefs-skeleton') sys.exit(1) if remount_users: if not is_running_without_lve() and remount_all(): sys.exit(1) remove_unused_mount_points() time.sleep(1) if check_busy: check_skeleton_not_busy() # NOTE: path should include path to cagefs-skeleton def mounts_are_found(path, proc_mounts = None, comparator = cagefslib.mounts_are_found_comparator): cagefslib.mounts = MountpointConfig().common_mounts if cagefslib.mounts_are_found(path, comparator): return True if proc_mounts == None: proc_mounts = cagefslib.get_mounted_dirs() path2 = cagefslib.addslash(os.path.realpath(path)) for mount in proc_mounts: mount = cagefslib.addslash(mount) if comparator(path2, mount): return True return False # NOTE: path should include path to cagefs-skeleton def path_includes_mount_point(path, proc_mounts = None): return mounts_are_found(path, proc_mounts = proc_mounts, comparator = cagefslib.path_includes_mount_point_comparator) # NOTE: path should include path to cagefs-skeleton def path_is_mounted(path, proc_mounts = None): return mounts_are_found(path, proc_mounts = proc_mounts, comparator = cagefslib.path_is_mounted_comparator) def create_remount_flag(): try: open(REMOUNT_FLAG, 'w').close() except IOError as e: secureio.print_error("failed to create", REMOUNT_FLAG, str(e)) def remove_remount_flag(): try: os.unlink(REMOUNT_FLAG) except OSError: pass def home_dirs_search_is_disabled(): return os.path.isfile('/etc/cagefs/disable.home.dirs.search') def create_homeN_dirs_in_skeleton(): """ Create /usr/share/cagefs-skeleton/home* directories and symlinks when needed, so they have the same meaning as in real file system. Make all symlinks relative to /usr/share/cagefs-skeleton. Create need.remount flag when needed. Return True if remount is needed. """ if mount_base_dir_enabled(): return False homes = cagefslib.get_homeN_dirs() if not homes: return False proc_mounts = cagefslib.get_mounted_dirs() always_home_mounting_mode_enabled = home_dirs_search_is_disabled() unmounted = False for home in homes: path = SKELETON + home try: if always_home_mounting_mode_enabled: if os.path.lexists(path): if os.path.islink(path): if home == '/home': if not unmounted: unmount_all(remount_users = True, check_busy = False) unmounted = True os.unlink(path) os.mkdir(path, 0o755) else: skel_link_to = os.readlink(path) if skel_link_to != 'home': if not unmounted: unmount_all(remount_users = True, check_busy = False) unmounted = True os.unlink(path) os.symlink('home', path) elif home == '/home': if not os.path.isdir(path): os.unlink(path) os.mkdir(path, 0o755) elif not mounts_are_found(path, proc_mounts): if not unmounted: unmount_all(remount_users = True, check_busy = False) unmounted = True cagefslib.remove_file_or_dir(path) os.symlink('home', path) else: os.symlink('home', path) else: if os.path.islink(home): link_to = os.path.realpath(home) link_to = link_to[1:] # remove leading slash - make path relative if os.path.lexists(path): if os.path.islink(path): skel_link_to = stripslash(os.readlink(path)) if skel_link_to != link_to: os.unlink(path) os.symlink(link_to, path) elif os.path.isdir(path): if not mounts_are_found(path, proc_mounts): if not unmounted: unmount_all(remount_users = True, check_busy = False) unmounted = True shutil.rmtree(path, True) os.symlink(link_to, path) else: # path is file or socket or device or named pipe os.unlink(path) os.symlink(link_to, path) else: os.symlink(link_to, path) elif os.path.isdir(home): if os.path.lexists(path): if os.path.islink(path): if not unmounted: unmount_all(remount_users = True, check_busy = False) unmounted = True os.unlink(path) os.mkdir(path, 0o755) elif os.path.isdir(path): continue else: # path is file or socket or device or named pipe os.unlink(path) os.mkdir(path, 0o755) else: os.mkdir(path, 0o755) except OSError as err: secureio.print_error('failed to create', path, ':', str(err)) if unmounted: create_remount_flag() return unmounted def update_homeN_symlinks_in_skeleton(): """ Update symlinks /usr/share/cagefs-skeleton/home*, so they point to the same location as in real file system """ if not os.path.isdir(SKELETON) or mount_base_dir_enabled() or home_dirs_search_is_disabled() or not cagefslib.get_homeN_dirs(): return homes = cagefslib.get_homeN_dirs(use_glob=True) for home in homes: path = SKELETON + home try: if os.path.islink(home) and os.path.islink(path): link_to = os.path.realpath(home) link_to = link_to[1:] # remove leading slash - make path relative skel_link_to = stripslash(os.readlink(path)) if skel_link_to != link_to: os.unlink(path) os.symlink(link_to, path) except OSError as err: secureio.print_error('failed to process symlink', path, ':', str(err)) def add_parents(list_of_files, set_of_files): """ Add parent directories to list_of_files if they do not present in set_of_files """ is_dict = True if not isinstance(set_of_files, dict): is_dict = False if not isinstance(set_of_files, set): set_of_files = set(set_of_files) parents = set() for filename in list_of_files: # remove redundant separators in order to prevent infinite loop filename = os.path.normpath(filename) if filename.startswith('/'): parent = os.path.dirname(filename) while parent != '/': if (parent not in set_of_files) and (parent not in parents): parents.add(parent) if is_dict: set_of_files[parent] = 1 else: set_of_files.add(parent) parent = os.path.dirname(parent) list_of_files.extend(list(parents)) # Creates (overwrites) file of white list for cagefs-fuse # (white list of files in system's /etc directory) def save_etc_white_list(): sigterm_check() white_list = {} white_list_copy = list(cagefslib.white_list) for ind in range(len(white_list_copy)): white_list_copy[ind] = white_list_copy[ind].replace('/etc', '', 1) white_list[white_list_copy[ind]] = 1 add_parents(white_list_copy, white_list) white_list_copy.sort() umask_saved = os.umask(0o22) _file = open(FUSE_WHITE_LIST, 'w') for filename in white_list_copy: _file.write('%s\n' % filename) _file.close() os.umask(umask_saved) os.chmod(FUSE_WHITE_LIST, 0o600) list_copy = [] files_list = {} # Function adds parent directories to (global) list_copy and files_list def add_parents_to_lists(): global list_copy, files_list list_copy = list(cagefslib.files_list) files_list = cagefslib.files_list add_parents(list_copy, files_list) # Function add_parents_to_lists() should be called before call of this function def save_list_of_files_in_skeleton(files_list = None): sigterm_check() if files_list is None: files_list = list_copy files_list.sort() umask_saved = os.umask(0o77) try: f = open(FILES_LIST, 'w') for filename in files_list: f.write('%s\n' % filename) f.close() except IOError as e: secureio.print_error('Failed to write ' + FILES_LIST + ' : ' + str(e)) os.umask(umask_saved) try: os.chmod(FILES_LIST, 0o600) except OSError as e: secureio.print_error('Failed to change permissions of ' + FILES_LIST + ' : ' + str(e)) # Load list of files in skeleton def load_list(filename, _list): try: _file = open(filename, 'r') except IOError: return while True: line = _file.readline() if line == '': break if line[0] != '\n': line = line.rstrip() if line != '' and line[0] == '/': _list.append(line) else: secureio.print_error('path', line, 'is relative') _file.close() # Function compares lists of files in skeleton # Returns list of files (or dirs) that present in old list and do not present in new list def compare_lists(old, new, ignore_realpath = False): diff = [] old_len = len(old) item = 0 while item < old_len: line = old[item] if line not in new: if not ignore_realpath: path = os.path.realpath(line) if ignore_realpath or (path not in new): diff.append(line) item += 1 while (item < old_len) and old[item].startswith(line+'/'): diff.append(old[item]) item += 1 continue item += 1 return diff # Function add_parents_to_lists() should be called before call of this function def delete_files_from_skeleton(config): sigterm_check() if config['reinit'] == 1 or config['init'] == 1: return old_list = [] load_list(FILES_LIST, old_list) files_to_delete = compare_lists(old_list, files_list) proc_mounts = cagefslib.get_mounted_dirs() for _file in files_to_delete: sigterm_check() cagefslib.del_libs_from_list(_file) file2 = cagefslib.addslash(_file) path = SKELETON + _file if (not file2.startswith('/dev/')) and (not mounts_are_found(path, proc_mounts)): if config['dont-clean'] == 1: secureio.logging("Skipping "+ path,SILENT,config['verbose']) continue if os.path.isdir(path) and (not os.path.islink(path)): try: shutil.rmtree(path, False) secureio.logging("Removed directory "+ path,SILENT,1) except (OSError, IOError, shutil.Error): secureio.logging('Error while removing directory '+ path,SILENT,1) elif os.path.lexists(path): try: os.unlink(path) secureio.logging('Removed file '+ path,SILENT,1) except (OSError, IOError): secureio.logging('Error while removing file '+ path,SILENT,1) # Function sends SIGUSR1 signal to cagefs-fuse in order to reload cagefs-fuse config files def reload_fuse_conf(): for pid in Execute("/sbin/pidof cagefs-fuse").split(): os.kill(int(pid), signal.SIGUSR1) # Compare dirs # Returns True if dirs are equal, False otherwise def are_dirs_equal(dir1, dir2): if (not os.path.isdir(dir1)) or (not os.path.isdir(dir2)): return False try: # run the "diff" command and suppress it's output p = subprocess.Popen([DIFF, "-r", dir1, dir2],\ stdout=subprocess.PIPE, stderr=subprocess.PIPE) p.communicate() # check return code of the child if p.returncode == 0: # dirs are equal return True except OSError: secureio.logging('failed to run ' + DIFF + " -r " + dir1 + " " + dir2, SILENT, 1) # some differences were found return False def create_symlink_for_selector_config_dir(selector_name): """ Create symlink to NodeJS/Python/etc selector config directory inside template for user etc directory For details see CAG-797, CAG-828 :param selector_name: name of selector: nodejs, python, etc """ # construct symlink path inside etc template (we ommit leading slash) temp_path = os.path.join(cagefslib.ETC_TEMPLATE_NEW_DIR, 'etc/cl.{}'.format(selector_name)) link_to = SELECTOR_CONF_DIR_TEMPLATE.format(selector_name) try: create_symlink(link_to, temp_path) except OSError as e: secureio.logging('Error while creating symlink ' + temp_path + ' to ' + link_to + str(e), SILENT, 1) def compare_etc_templates(force_update_etc = False): # Remove /etc/mail from template of etc directory (/etc/mail is mounted from real system) shutil.rmtree(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc/mail', True) create_symlink_for_selector_config_dir('nodejs') create_symlink_for_selector_config_dir('python') # Remove blacklisted files & dirs from skeleton remove_blacklisted_files() # Old template exists ? if os.path.isfile(cagefslib.ETC_TEMPLATE_DIR+'/etc/passwd'): # Compare old and new templates of etc directory. Increase version if not equal old_etc_version = get_etc_version(cagefslib.ETC_TEMPLATE_DIR+'/etc') # set_etc_version(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', old_etc_version) copy_etc_version(cagefslib.ETC_TEMPLATE_DIR+'/etc', cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc') if (not force_update_etc) and cagefslib.are_dirs_equal(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', cagefslib.ETC_TEMPLATE_DIR+'/etc', shallow = False): # Do not replace etc template (templates are equal) return else: set_etc_version(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', old_etc_version+1) else: # Do not compare old and new templates of etc directory (old template does not exist) # Set version of new template to 1 set_etc_version(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', 1) # Remove old etc template shutil.rmtree(cagefslib.ETC_TEMPLATE_DIR+'/etc', True) # Move new etc template to proper location try: os.rename(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', cagefslib.ETC_TEMPLATE_DIR+'/etc') except (OSError, IOError): secureio.logging("Error moving "+cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc to '+cagefslib.ETC_TEMPLATE_DIR+'/etc', SILENT, 1) sys.exit(1) def remove_nested_skeleton(): sigterm_check() # Hide (override) global SKELETON variable (latter can be changed if /usr/share/cagefs-skeleton is symlink) SKELETON = '/usr/share/cagefs-skeleton' if os.path.lexists(SKELETON+SKELETON): try: if os.path.isdir(SKELETON+SKELETON) and (not os.path.islink(SKELETON+SKELETON)): shutil.rmtree(SKELETON+SKELETON, False) else: os.unlink(SKELETON+SKELETON) except (OSError, IOError, shutil.Error) as e: secureio.logging("Error: failed to remove "+SKELETON+SKELETON+" : "+str(e), SILENT, 1) if os.path.lexists(SKELETON+SKELETON): secureio.logging("Error: "+SKELETON+SKELETON+" exists. Please remove manually.", SILENT, 1) sys.exit(1) bdir = SKELETON+'/var/cagefs' if os.path.lexists(bdir): try: if os.path.isdir(bdir) and (not os.path.islink(bdir)): shutil.rmtree(bdir, False) else: os.unlink(bdir) except (OSError, IOError, shutil.Error) as e: secureio.logging("Error: failed to remove "+bdir+" : "+str(e), SILENT, 1) if os.path.lexists(bdir): secureio.logging("Error: "+bdir+" exists. Please remove manually.", SILENT, 1) def update_etc_only(config, users = None, print_selector_errors = False): remove_log_file() ci = cagefs_init() cfg = read_config() cagefslib.read_native_conf() load_black_list() umask_saved = os.umask(0) # Create empty directory for template of etc directory shutil.rmtree(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', True) if not os.path.isdir(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc'): try: mod_makedirs(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', 0o755) except OSError: secureio.print_error('creating', cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc') sys.exit(1) # Build or update template of etc directory for section in cfg.sections(): ci.update_etc_from_section(config, cfg, section) remove_nested_skeleton() # Update native php files in template of etc directory (if needed) ci.update_etc_paths(list(cagefslib.orig_binaries.values())) # pylint: disable=dict-values-not-iterating if print_selector_errors: for alias, orig_path in cagefslib.orig_binaries.items(): orig_path2 = os.path.realpath(orig_path) if orig_path2.startswith('/etc/'): if (not cagefslib.move_to_alternatives(orig_path2, etc = True)) and cagefslib.is_mandatory(alias): secureio.print_error("CloudLinux Selector setup, path:", orig_path) return True cagefslib.remove_unwanted_users_from_groups() os.umask(umask_saved) create_files_for_symlink_protection() create_dirs_for_symlink_protection() sync_etc_cl_selector_dir() compare_etc_templates(force_update_etc = config['force-update-etc']) if users == None: # Create etc for all users > MIN_UID (ignore users.enabled directory) update_etc(config, all_users = True) else: # update etc directory for specified users update_etc(config, users = users) return False # /etc/cagefs/proxy.commands format: # SENDMAIL=/usr/sbin/sendmail # SENDMAILQ=/var/qmail/bin/sendmail # MAIMANCPAN=/usr/local/cpanel/3rdparty/mailman/mail/mailman # CRONTAB_LIST:proxy.crontab.cagefs=root:/usr/bin/crontab # CRONTAB_SAVE:noproceed=root:/usr/bin/crontab def load_wrappers(update_wrappers = False): wrappers, wrappers_names = build_wrappers_dicts() cagefslib.wrappers.update(wrappers) cagefslib.wrappers_names.update(wrappers_names) if update_wrappers: cagefslib.mounts = MountpointConfig().common_mounts proc_mounts = cagefslib.get_mounted_dirs() # Install wrappers for _file in cagefslib.wrappers: if os.path.isfile(_file): path = SKELETON+_file if not path_is_mounted(path, proc_mounts): if os.path.isfile(path): cagefslib.install_wrapper(_file) elif not os.path.exists(path): cagefslib.create_parent_path(SKELETON, os.path.dirname(_file), copy_permissions=1, copy_ownership=1) cagefslib.install_wrapper(_file) def check_separator(separator, line): if separator not in line: return None, None # main separator not found line_parts = line.split(separator) if len(line_parts) != 2: return None, None # too many main separators in line return line_parts[0], line_parts[1] def validate_with_regex(regex, part, can_be_none): if part is not None: regexp_comp = re.compile(regex) p1 = regexp_comp.match(part) if p1 is None: return False # username is not valid return True if can_be_none: return True return False def check_proxy_line(line): """ Return False if line is corrupted """ if line.startswith('#') or line.isspace(): # skip comments and empty lines return True alias_wrapper, user_command = check_separator('=', line) if alias_wrapper is None or user_command is None: return False if ':' in alias_wrapper: alias, wrapper = check_separator(':', alias_wrapper) else: alias = alias_wrapper wrapper = None if ':' in user_command: user, command = check_separator(':', user_command) else: command = user_command user = None if alias is None or command is None: return False # check that last param is valid asb path if not os.path.isabs(command): return False if command.strip()[-1] == '/': return False # check that string is MAYBE username if not validate_with_regex(r'^[a-z][-a-z0-9]*$', user, can_be_none = True): return False # check alias if not validate_with_regex(r'^[a-zA-Z][-a-zA-Z0-9_]*$', alias, can_be_none = False): return False #check wrapper if not validate_with_regex(r'^[a-zA-Z.][-a-zA-Z0-9_.]*$', wrapper, can_be_none = True): return False return True def build_wrappers_dicts(raise_exception = False): DEFAULT_PROXY_NAME = "cagefs.proxy.program" commands = load_wrappers_commands() # Build hashes wrappers = {} wrappers_names = {} for line in commands: if not check_proxy_line(line): if raise_exception: raise Exception('Warning: Found corrupted line:' + str(line) + '. Skip line.') else: print('Warning: Found corrupted line:' + str(line) + '. Skip line.') continue if line.startswith('#'): continue words = line.strip().split('=', 1) if len(words) == 2: words_left = words[0].strip().split(':', 1) if len(words_left) == 2 and words_left[1] == 'noproceed': continue alias = words_left[0].strip() if len(words_left) == 2: wrapper_name = words_left[1].strip() else: wrapper_name = DEFAULT_PROXY_NAME if words[1].find(':') == -1: command = words[1].strip() else: words_right = words[1].strip().split(':', 1) command = words_right[1].strip() if command not in wrappers: wrappers[command] = alias if command not in wrappers_names: wrappers_names[command] = wrapper_name return wrappers, wrappers_names def load_wrappers_commands(): proxy_commands_dir = os.path.dirname(PROXY_COMMANDS) proxy_commands_name = os.path.basename(PROXY_COMMANDS) # Load user command files first filenames = sorted([ filename for filename in os.listdir(proxy_commands_dir) if filename.endswith(proxy_commands_name) and filename != proxy_commands_name ]) filenames.append(proxy_commands_name) commands = [] for filename in filenames: path = os.path.join(proxy_commands_dir, filename) if os.path.isfile(path): commands.extend(read_file(path)) return commands # ETC_MPFILE should be read to cagefslib.mounts before call of this function def remove_blacklisted_files(): proc_mounts = cagefslib.get_mounted_dirs() for black_list_file in cagefslib.black_list: sigterm_check() # Do not delete jailshell because it should be replaces with symlink to /bin/bash if black_list_file == '/usr/local/cpanel/bin/jailshell': continue is_in_etc = black_list_file.startswith('/etc/') if is_in_etc: path = cagefslib.ETC_TEMPLATE_NEW_DIR + black_list_file else: path = SKELETON + black_list_file if os.path.lexists(path): if not is_in_etc: if path_is_mounted(path, proc_mounts): secureio.logging("Warning: blacklisted path "+black_list_file+" is mounted", SILENT, 1) continue if path_includes_mount_point(path, proc_mounts): secureio.logging("Warning: blacklisted path "+black_list_file+" includes mount point", SILENT, 1) continue try: if os.path.isdir(path) and (not os.path.islink(path)): shutil.rmtree(path, False) else: os.unlink(path) secureio.logging("Removed "+path, SILENT, VERBOSE) except (OSError, IOError, shutil.Error) as e: secureio.logging("Error: failed to remove "+path+" : "+str(e), SILENT, 1) if os.path.lexists(path): secureio.logging("Warning: blacklisted path "+path+" exists. Please remove manually.", SILENT, 1) def load_black_list(remove = False): black_list_dir = os.path.dirname(BLACK_LIST_FILE) black_list_name = os.path.basename(BLACK_LIST_FILE) cagefslib.black_list = [] for filename in os.listdir(black_list_dir): path = os.path.join(black_list_dir, filename) if filename.endswith(black_list_name) and os.path.isfile(path): # Read content of black list file, remove path to skeleton and trailing newlines black_list = read_file(path) for line in black_list: line = cagefslib.strip_path(line.rstrip()) if line.startswith('/'): if line =="/" or line.find("/../") != -1 or line.endswith("/.."): secureio.print_error("Invalid path", line, "in file", path) continue if not line.startswith('/etc/'): line = os.path.realpath(line) if line not in cagefslib.black_list: cagefslib.black_list.append(line) cagefslib.mounts = MountpointConfig().common_mounts if remove: remove_blacklisted_files() def replace_jailshell(): bin_list = ['/usr/local/cpanel/bin/jailshell', '/usr/local/psa/bin/chrootsh'] dest = '/bin/bash' for bin_name in bin_list: parent_dir = SKELETON + os.path.dirname(bin_name) link_name = SKELETON + bin_name if os.path.isdir(parent_dir) and (not os.path.islink(link_name)): if os.path.lexists(link_name): cagefslib.remove_file_or_dir(link_name, check_mounts = True) try: os.symlink(dest, link_name) except (OSError, IOError): secureio.print_error('creating symlink', link_name, 'to', dest) sys.exit(1) def copy_options(src, section, dst, new_section): for option in src.options(section): dst.set(new_section, option, src.get(section, option)) def copy_section(src, dst, section): if src.has_section(section): if (not dst.has_section(section)) and (section.lower() != 'default'): dst.add_section(section) copy_options(src, section, dst, section) else: error = True for num in range(100): new_section = section + str(num) if not dst.has_section(new_section): error = False break if error: new_section = section + id_generator() dst.add_section(new_section) copy_options(src, section, dst, new_section) def read_config(fail_if_sections_are_duplicated = False): cfg = configparser.RawConfigParser(strict=False) # section -> filepath sections = {} # Read base config files for _file in os.listdir(CONFIG_DIR): if _file.endswith('.cfg'): path = CONFIG_DIR+_file tmp = configparser.RawConfigParser(strict=False) tmp.read(path) if fail_if_sections_are_duplicated: for section in tmp.sections(): if section in sections: secureio.logging("Error: duplicated section ["+section+"] in files "+sections[section]+" and "+path, SILENT, 1) sys.exit(1) else: sections[section] = path for section in tmp.sections(): copy_section(tmp, cfg, section) # Read config files for packages that are installed by user if os.path.isdir(WORK_CONFIG_DIR): for _file in os.listdir(WORK_CONFIG_DIR): if _file.endswith('.work'): path = os.path.join(WORK_CONFIG_DIR, _file) tmp = configparser.RawConfigParser(strict=False) tmp.read(path) if fail_if_sections_are_duplicated: for section in tmp.sections(): if section in sections: secureio.logging("Error: duplicated section ["+section+"] in files "+sections[section]+" and "+path, SILENT, 1) sys.exit(1) else: sections[section] = path for section in tmp.sections(): copy_section(tmp, cfg, section) return cfg def create_symlinks_in_skeleton(): """ Create symlinks in CageFS skeleton """ symlinks = { SKELETON+'/var/tmp' : '../tmp', SKELETON+'/var/run' : '../run' } cagefslib.write_symlinks(symlinks) def update_cagefs(config): if (config['update'] == 1) and (config['force-update'] == 0): if not cagefslib.update_of_cagefs_skeleton_is_needed(): if not SILENT: print("cagefs-skeleton has been updated recently, if you want to force the update, please run:") print('"cagefsctl --force-update"') return update_rpm_packages() add_default_rpm_packages_to_cagefs() remove_log_file() ci = cagefs_init() # create cagefs.mp if config['init'] == 1: create_mp(False, exit_on_error=True) cagefslib.mounts = MountpointConfig().common_mounts cfg = read_config() cagefslib.read_native_conf() create_files_for_symlink_protection() create_dirs_for_symlink_protection() if config['reinit'] == 0 and config['init'] == 0: # Load list of libraries in skeleton cagefslib.load_libs(LIBS_LIST) # Debugging, checks... if cagefslib.debug_option and cagefslib.libs_list and os.path.isfile(LIBDIR+'/libs.dat'): print('Loading libs.dat', end=' ', flush=True) rtf = open(LIBDIR+'/libs.dat', 'r') td = eval(rtf.read()) rtf.close() print("Done") if td: print("Pickle len", len(cagefslib.libs_list)) print("Eval len", len(td)) print('Comparing', end=' ', flush=True) try: for key in td: for item in td[key]: if item not in cagefslib.libs_list[key]: secureio.print_error('line', cagefslib.lineno(), ' : NOT EQUAL', key) break except KeyError as e: secureio.print_error('line', cagefslib.lineno(), " : Key Error", e) try: for key in cagefslib.libs_list: for item in cagefslib.libs_list[key]: if item not in td[key]: secureio.print_error('line', cagefslib.lineno(), ' : NOT EQUAL', key) break except KeyError as e: secureio.print_error('line', cagefslib.lineno(), " : Key Error", e) print("Done") load_wrappers(True) load_black_list() umask_saved = os.umask(0) # Create empty directory for template of etc directory sigterm_check() shutil.rmtree(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', True) if not os.path.isdir(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc'): try: mod_makedirs(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', 0o755) except OSError: secureio.print_error('creating', cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc') sys.exit(1) # Build or update skeleton for section in cfg.sections(): sigterm_check() ci.handle_cfg_section(config, SKELETON, cfg, section) remove_nested_skeleton() # Update native php files in cagefs-skeleton and in template of etc directory ci.update_paths(config, SKELETON, list(cagefslib.orig_binaries.values())) # pylint: disable=dict-values-not-iterating ci.update_alt_php_libs(config, SKELETON) cagefslib.remove_unwanted_users_from_groups() # Create mount points in skeleton create_mount_points(MOUNT_POINTS) os.umask(umask_saved) # CAG-993: create /root directory inside CageFS, because # home directory for root user is needed when running cagefsctl --enter cagefslib.make_dir(SKELETON+'/root', 0o550, allow_symlink=False, update_perm=True) cagefslib.set_owner(SKELETON+'/root', 0, 0) cagefslib.save_etc_safe_list(['/passwd', '/group']) # Add parent directories to lists add_parents_to_lists() # Load old list of files, compare to current list, # delete files from skeleton, delete appropriate libs from cagefslib.libs_list delete_files_from_skeleton(config) replace_jailshell() create_symlinks_in_skeleton() cagefs_da_lib.create_symlink_to_php_ini_for_DA(SKELETON) # Save new list of files save_list_of_files_in_skeleton() # Save list of libraries in skeleton cagefslib.save_libs(LIBS_LIST) if cagefslib.debug_option: tf2 = open(LIBDIR+'/libs.txt', 'w') for key in sorted(cagefslib.libs_list): tf2.write("%s : %s\n" % (key, " ".join(cagefslib.libs_list[key]))) tf2.close() print('Saving libs.dat', end=' ', flush=True) tf = open(LIBDIR+'/libs.dat', 'w') tf.write(repr(cagefslib.libs_list)) tf.close() print("Done") # Create (overwrite) white list for cagefs-fuse save_etc_white_list() sync_etc_cl_selector_dir() compare_etc_templates() if etcfs_is_disabled(): # CageFS is enabled ? if (not save_dir_exists()): update_etc(config) else: # Update etc for all users > MIN_UID update_etc(config, all_users = True) elif config['update'] == 1: reload_fuse_conf() if (config['reinit'] == 1) and ('cagefs_was_enabled' in config): enable_cagefs() create_empty_dir() # CAG-706: setup emulation for /var/run/utmp cagefslib.create_utmp_in_skeleton() setup_cpanel_multiphp(do_mount=False) # LU-640: update license file inside CageFS (needed by cloudlinux-selector to check license) mount_file(LICENSE_TIMESTAMP_FILE, do_mount=False) cagefslib.add_syslog_socket() # CAG-1062: [CL8.2] Mount socket of systemd-journal into CageFS _mount_systemd_journal_socket(do_mount=False) unmounted = create_homeN_dirs_in_skeleton() update_homeN_symlinks_in_skeleton() from cagefsreconfigure import add_mounts_for_passenger add_mounts_for_passenger() # CageFS is enabled and init (or reinit) command is running ? if (config['reinit'] == 1 or config['init'] == 1) and (not save_dir_exists()): # Remount skeleton and all users mount_skeleton(True) unmounted = False if os.path.isfile('/usr/bin/systemctl'): # oshyshatskyi: we don't really care when systemd is going to start the service # moreover, original task CAG-650 fixed the bug with non-started cagefs service # after REBOOT in a different way: by adding systemctl enable cagefs.service in spec. # this call to start does not really do anything for cagefs because (re)init process # does all the needed actions to make cagefs work without the service subprocess.Popen(['/usr/bin/systemctl', 'start', 'cagefs'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL) if unmounted: mount_skeleton(True) # Update statuses of users (needed for PHP Selector) if cagefs_is_enabled(): secureio.logging("Updating statuses of users ...", SILENT, 1) update_users_status() cagefslib.save_last_update_time() do_not_ask_option = False def confirm(message): print(message, end=' ', flush=True) if do_not_ask_option: print('yes') return while True: line = sys.stdin.readline() if line == "yes\n": break elif line == "no\n": print("Aborting") sys.exit(1) print("Please, reply with yes or no") def unmount_skeleton_in_all_namespaces(): print("Unmounting skeleton ", end=' ', flush=True) if umount_skeleton(all_cagefs_mounts=True, all_namespaces=True): secureio.print_error('unmounting skeleton') sys.exit(1) time.sleep(1) print("[DONE]") def remove_all(): confirm("WARNING: If you continue, CageFS will be disabled, and all " "related files and directories will be removed. Do you want to continue (yes/no)? ") print("Disabling CageFS ", end=' ', flush=True) shutil.rmtree(INIPREFIX+'users.disabled', True) shutil.rmtree(INIPREFIX+'users.enabled', True) update_users_status(disable_all = True) print("[DONE]") if is_running_without_lve(): print("Unmounting users ", end=' ', flush=True) if delete_namespaces(): secureio.print_error('unmounting users') sys.exit(1) time.sleep(1) print("[DONE]") unmount_skeleton_in_all_namespaces() else: unmount_skeleton_in_all_namespaces() # CAG-440 - do not stop proxyexecd service because it is needed for cagefs_enter # print "Stopping proxyexecd service ", # cagefs_proxyexecd('stop') # time.sleep(1) # print "[DONE]" print("Unmounting users ", end=' ', flush=True) if remount_all(): secureio.print_error('unmounting users') sys.exit(1) time.sleep(1) print("[DONE]") check_skeleton_not_busy() if skeleton_is_mounted(): secureio.print_error('failed to unmount cagefs-skeleton') sys.exit(1) if repair_homes.invalid_homes_exist(): print('Users with invalid pathes to home directories exist! DO NOT REMOVE /var/cagefs !') else: print("Removing "+BASEDIR, end=' ', flush=True) shutil.rmtree(BASEDIR, True) print(" [DONE]") print("Removing "+SKELETON, end=' ', flush=True) shutil.rmtree(SKELETON, True) print(" [DONE]") old_skel = SKELETON + '.old' if os.path.isdir(old_skel) and not skeleton_is_mounted(old_skel): print("Removing "+old_skel, end=' ', flush=True) shutil.rmtree(old_skel, True) print(" [DONE]") def usage(): print('') print('Use following syntax to manage CageFS:') print(sys.argv[0]+" [OPTIONS]") print('Options:') print(" -i | --init : initialize CageFS (create CageFS if it does not exist)") print(" -r | --reinit : reinitialize CageFS (make backup and recreate CageFS)") print(" -u | --update : update files in CageFS (add new and modified files to CageFS,") print(" remove unneeded files)") print(" -f | --force : recreate CageFS (do not make backup, overwrite existing files)") print(' -d | --dont-clean : do not delete any files from skeleton (use with --update option)') print(" -k | --hardlink : use hardlinks if possible") print(' --create-mp : Recreates /etc/cagefs/cagefs.mp file with default set of mount points.') print(' WARNING: Any previous changes made to file by admin or by any software will be lost') print(' --mount-skel : mount CageFS skeleton directory') print(" --unmount-skel : unmount CageFS skeleton directory") print(' --remove-all : disable CageFS, remove templates and /var/cagefs directory') print(' --sanity-check : perform basic self-diagnistics of common cagefs-related issues(mostly useful for support)') print(' --addrpm : add rpm-packages into CageFS (run "cagefsctl --update" in order to apply changes)') print(' : only package name should be specified (without package version and release)') print(' : example: cagefsctl --addrpm ImageMagick') print(' --delrpm : remove rpm-packages from CageFS (run "cagefsctl --update" in order to apply changes)') print(' --list-rpm : list rpm-packages that are installed in CageFS') print(" -e | --enter : enter into user's CageFS as root") print(' --update-list : update specified files only (paths are read from stdin)') print(' --update-etc : update etc directory of all or specified users') print(' --set-update-period : set min period of update of CageFS in days (default = 1 day)') print(' --force-update : force update of CageFS (ignore period of update)') print(' --force-update-etc : force update of /etc directories for users in CageFS') print(' --reconfigure-cagefs : configure CageFS integration with other software (control panels,') print(' database servers, etc)') print('') print('Use following syntax to manage users:') print(sys.argv[0]+' [OPTIONS] username [more usernames]') print('Options:') print(' -m | --remount : remount specified user(s)') print(' -M | --remount-all : remount CageFS skeleton directory and all users') print(' (use this each time you have changed cagefs.mp file)') print(' -w | --unmount : unmount specified user(s)') print(' | --unmount-dir : unmount specified dir in all mount namespaces') print(' -W | --unmount-all : unmount CageFS skeleton directory and all users') print(' -l | --list : list users that entered in CageFS') print(' --list-logged-in : list users that entered in CageFS via SSH') print(' --enable : enable CageFS for the user') print(' --disable : disable CageFS for the user') print(' --enable-all : enable all users, except specified in', disabled_dir) print(' --disable-all : disable all users, except specified in', enabled_dir) print(' --display-user-mode : display current mode ("Enable All" or "Disable All")') print(' --toggle-mode : toggle mode saving current lists of users') print(' (lists of enabled and disabled users remain unchanged)') print(' --list-enabled : list enabled users') print(' --list-disabled : list disabled users') print(' --user-status : print status of specified user (enabled or disabled)') print(" --getprefix : display prefix for user") print('') print('PHP Selector related options:') print(' --setup-cl-selector : setup PHP Selector or register new alt-php versions') print(' --remove-cl-selector : unregister alt-php versions, switch users to default php version when needed') print(' --rebuild-alt-php-ini : rebuild alt_php.ini file for specified users (or all users if none specified)') print(' --validate-alt-php-ini : same as --rebuild-alt-php-ini but also validates alt_php.ini options ') print(' --cl-selector-reset-versions: reset php version for specifed users to default (or all users if none specified)') print(' --cl-selector-reset-modules : reset php modules (extensions) for specific users to defaults (or all users if none specified)') print(' --create-virt-mp : create virtual mount points for the user') print(' --create-virt-mp-all : create virtual mount points for all users') print(' --remount-virtmp : create virtual mount points and remount user') print(' --apply-global-php-ini : use with 0, 1 or 2 arguments from the list: error_log, date.timezone') print(' without arguments applies all global php options including two above') print('') print('Common options:') print(' --enable-cagefs : enable CageFS') print(' --disable-cagefs : disable CageFS') print(' --cagefs-status : print CageFS status (enabled or disabled)') print(' --check-cagefs-initialized : properly checks whether CageFS is initialized and print result') print(' --set-min-uid : Set min UID') print(' --get-min-uid : Display current MIN_UID setting') print(' --print-suids : Print list of SUID and SGID programs in skeleton') print(' --do-not-ask : assume "yes" in all queries (should be the first option in command)') print(' --clean-var-cagefs : clean /var/cagefs directory (remove data of non-existent users)') print(' --set-tmpwatch : set tmpwatch command and parameters (save to '+cagefslib.CAGEFS_INI+' file)') print(' --tmpwatch : execute tmpwatch (remove outdated files in tmp directories in CageFS for all users)') print(" --toggle-plugin : disable/enable CageFS plugin") if is_running_without_lve(): print(" --create-namespace USER : create namespace for the USER (only for containers)") print(" --create-namespaces : create namespaces for all users (only for containers)") print(" --delete-namespace USER : delete namespace or the USER (only for containers)") print(" --delete-namespaces : delete namespaces for all users (only for containers)") print(' -v | --verbose : verbose output') print(' --wait-lock : wait for end of execution of other cagefsctl processes (when needed) before execution of the command') print(' -h | --help : this message') print('') def check_skeleton(): if not check_cagefs_skeleton(): secureio.print_error('directory', SKELETON, 'does NOT exist or is empty.') secureio.print_error('Use "'+sys.argv[0]+' --init" to create CageFS') sys.exit(1) os.chmod(SKELETON, 0o755) def remove_parent_dirs(paths): result = [] for path in paths: spath = cagefslib.addslash(path) parent = False for path2 in paths: if path2.startswith(spath): parent = True break if not parent: result.append(path) return result def addrpm(pkg_name, silent = False): WORK_DIR = WORK_CONFIG_DIR from simple_rpm import get_package_files package_files = get_package_files(pkg_name) if package_files is None: if not silent: print("Package %s not installed" % pkg_name) return if not os.path.lexists(WORK_DIR): try: mod_makedirs(WORK_DIR, 0o700) except (OSError, IOError): secureio.print_error("failed to create", WORK_DIR) sys.exit(1) elif not os.path.isdir(WORK_DIR): secureio.print_error("path", WORK_DIR, "should be directory") sys.exit(1) umask_saved = os.umask(0o77) WORK_FILE = os.path.join(WORK_DIR, pkg_name+".work") aFile = open ( WORK_FILE, 'w' ) aFile.write("["+pkg_name+"]\n") aFile.write("paths=") i = 0 package_files = remove_duplicates(package_files) package_files = remove_parent_dirs(package_files) for b in package_files: if not b.startswith("/usr/share/man/") and not b.startswith("/usr/share/locale/") and \ not b.startswith("/usr/share/doc/") and not b.startswith("/usr/share/info/") and \ not b.startswith("/usr/lib/.build-id/") and not b.startswith("/usr/share/licenses/"): if (i != 0): aFile.write(", "+b) else: aFile.write(b) i = i + 1 aFile.write("\n") aFile.close() os.umask(umask_saved) def delrpm(pkg_name, silent = False): WORK_DIR = WORK_CONFIG_DIR WORK_FILE = os.path.join(WORK_DIR, pkg_name+".work") if not os.path.lexists(WORK_FILE): if not silent: print("Rpm %s is not installed in CageFS" % pkg_name) elif os.path.islink(WORK_FILE) or (not os.path.isfile(WORK_FILE)): secureio.print_error(WORK_FILE, "should be regular file") else: try: os.remove(WORK_FILE) except (OSError, IOError): secureio.print_error("failed to remove", WORK_FILE) # if this is standard packet, save it if pkg_name in STD_PACKAGES: try: # Add package to file '/usr/share/cagefs/exclude.packages' # read old list packagesToExclude = [] if os.path.exists ( STD_PACKAGES_FILE ): packagesToExclude = read_file ( STD_PACKAGES_FILE ) # Append to exclude list, if it is not there if (pkg_name+'\n') not in packagesToExclude: packagesToExclude.append ( pkg_name+'\n' ) cagefslib.write_file ( STD_PACKAGES_FILE, packagesToExclude, False ) except (OSError, IOError): secureio.print_error("failed to write package list") def list_rpm(silent = False): WORK_DIR = WORK_CONFIG_DIR rpms = [] if os.path.isdir(WORK_DIR): for work in os.listdir(WORK_DIR): file_name = work[:work.rfind(".")] rpms.append(file_name) rpms.sort() if not silent: for package in rpms: print(package) return rpms def add_rpm_packages_to_cagefs(args, overwrite = False): for rpm in args: sigterm_check() if overwrite or (not os.path.isfile(os.path.join(WORK_CONFIG_DIR, rpm + '.work'))): addrpm(rpm, silent = True) def remove_rpm_packages_from_cagefs(args): for rpm in args: delrpm(rpm, silent = True) def add_default_rpm_packages_to_cagefs(): # standard packages - STD_PACKAGES # packages to exclude (file '/usr/share/cagefs/exclude.packages') packagesToExclude = [] if os.path.exists ( STD_PACKAGES_FILE ): packagesToExclude = read_file ( STD_PACKAGES_FILE ) packagesToAdd = [] for packName in STD_PACKAGES: if (packName+'\n') not in packagesToExclude: packagesToAdd.append ( packName ) # Add to cageFs add_rpm_packages_to_cagefs ( packagesToAdd ) def update_rpm_packages(): sigterm_check() rpms = list_rpm(silent = True) add_rpm_packages_to_cagefs(rpms, overwrite = True) # Write MIN_UID to file def set_min_uid(value): global MIN_UID buf_val = int(value) if buf_val < 100: secureio.print_error("MIN UID should be >= 100") sys.exit(1) MIN_UID = buf_val try: binfile = open(MIN_UID_FILENAME, 'wb') data = struct.pack('i', MIN_UID) binfile.write(data) binfile.close() except: secureio.print_error("writting MIN UID to file", MIN_UID_FILENAME) sys.exit(1) # Read MIN_UID from file def get_min_uid(): global MIN_UID try: uid = read_min_uid() except ValueError as e: secureio.print_error(str(e), MIN_UID_FILENAME) sys.exit(1) if uid is not None: MIN_UID = uid def read_min_uid(): """ Gets minuid from file and returns unpacked value if no errors happened otherwise None """ if not os.path.isfile(MIN_UID_FILENAME): return None try: binfile = open(MIN_UID_FILENAME, 'rb') intsize = struct.calcsize('i') data = binfile.read(intsize) binfile.close() except: raise ValueError('failed to read MIN UID from file') if len(data) != intsize: raise ValueError('reading MIN UID from file') num = struct.unpack('i', data) if len(num) > 0 and num[0] >= 100: return num[0] return None # toggle mode saving current lists of users # (lists of enabled and disabled users remain unchanged) def toggle_mode(): check_save_dir() mode = get_user_mode() check_mode_error(mode) if mode == 'Enable All': # Get list of enabled users enabled_users = get_list_of_users(True) # Set mode "Disable All" set_user_mode(False) for user in enabled_users: # Enable user toggle_user(user, True) elif mode == 'Disable All': # Get list of disabled users disabled_users = filter_users(get_list_of_users(False)) # Set mode "Enable All" set_user_mode(True) for user in disabled_users: # Disable user toggle_user(user, False) def addgrouptojail(bdir, group_id, user, config): bdir = cagefslib.stripslash(bdir) try: gr = grp.getgrgid(group_id) except: secureio.logging('Warning: getgrgid() failed for group id '+str(group_id)+" skipping...", SILENT, 1) return 0 if (not cagefslib.test_group_exist(gr.gr_name, bdir+'/etc/group')): _file = bdir+'/etc/group' secureio.logging('adding group '+gr[0]+' to '+_file, SILENT, config['verbose']) try: tmp = gr[0]+':x:'+str(gr[2])+':' if (type(user)==str and len(user)>0): tmp = tmp + user tmp = tmp + '\n' fd = open(_file, 'a') fd.write(tmp) fd.close() except IOError: secureio.logging('ERROR: failed to write group '+gr[0]+' to '+_file, SILENT, 1) return 0 return 1 def addusertogroupinjail(bdir, group_id, user, config): ret = addgrouptojail(bdir, group_id, user, config) if (ret == 0): return 0 try: _file = bdir+'/etc/group' fd = open(_file, 'r+') line = fd.readline() while (len(line)>0): splitted = line.split(':') if (len(splitted)==4 and int(splitted[2]) == group_id): users = splitted[3][:-1].split(',') if (user in users): fd.close() return 1 else : secureio.logging('Adding user '+user+' to group '+splitted[0], SILENT, config['verbose']) pos = fd.tell() buf = fd.read() fd.seek(pos-len(line)) if (len(users)==1 and users[0] == ''): users = [user] else: users.append(user) tmp = splitted[0]+':x:'+splitted[2]+':' tmp2 = ','.join(users) tmp += tmp2+'\n'+buf fd.write(tmp) fd.close() return 1 line = fd.readline() except IOError: secureio.logging('ERROR: failed to add user '+user+' to group '+str(group_id)+' in '+_file, SILENT, 1) return 0 return 0 #Create .htaccess def create_htaccess(path): file_name = path + "/.htaccess" try: _file = open(file_name, 'w') _file.write('#CageFS autogenerated file\n') _file.write('deny from all\n') _file.close() os.chmod(file_name, 0o644) except (IOError, OSError): pass def remove_htaccess(path): file_name = path + "/.htaccess" try: os.remove(file_name) except (IOError, OSError): pass domlogs_found = None def copyetc(user, config, ignore_errors = False, recreate = False, passwd_only = False, custom_etc_files = None): global domlogs_found, SPECIAL_PATHS pw_line = secureio.clpwd.get_pw_by_name(user) prefix = get_user_prefix(user) etcskel = cagefslib.ETC_TEMPLATE_DIR + '/etc' bdir = BASEDIR + '/' + prefix + '/' + user + '/' etcuser = bdir + 'etc' if passwd_only: for cf in ['/passwd', '/shadow']: if cagefslib.copy_file(etcskel+cf, etcuser+cf, create_parent_dir = False) == 1: secureio.logging("Error copying "+etcskel+cf+' to '+etcuser+cf, SILENT, 1) if not ignore_errors: sys.exit(2) else: if custom_etc_files == None: custom_etc_files = cagefslib.get_additional_etc_files_for_user(user, etcuser) if (config['init'] == 1) or (config['reinit'] == 1) or recreate: try: shutil.rmtree(etcuser, True) if cagefslib.copytree(etcskel, etcuser, True, skip_dst_files = custom_etc_files) == 1: raise Exception('copytree() failed') if create_etc_alternatives(users = [user]): raise Exception('Failed to setup cl-selector for user '+user) except Exception as e: secureio.logging("Error while copying "+etcskel+' to '+etcuser+': '+str(e), SILENT, 1) if not ignore_errors: sys.exit(2) else: if (cagefslib.copytree(etcskel, etcuser, True, update = True, skip_dst_files = custom_etc_files) == 1) or \ create_etc_alternatives(users = [user]): secureio.logging("Error copying "+etcskel+' to '+etcuser, SILENT, 1) if not ignore_errors: sys.exit(2) if domlogs_found is None: domlogs_found = os.path.isdir('/usr/local/apache/domlogs') and os.path.isdir('/etc/apache2/logs/domlogs') SPECIAL_PATHS.append('/apache2/') if domlogs_found: try: logs_dir_path = etcuser + '/apache2/logs' domlogs_path = etcuser + '/apache2/logs/domlogs' if not os.path.lexists(domlogs_path): if not os.path.lexists(logs_dir_path): mod_makedirs(logs_dir_path, 0o755) relative_symlink('/usr/local/apache/domlogs', domlogs_path) except OSError as e: secureio.logging("Error while creating "+domlogs_path+' : '+str(e), SILENT, 1) if not ignore_errors: sys.exit(2) # get all entries for users with uid pw_db = secureio.clpwd.get_pw_by_uid(pw_line.pw_uid) # get /etc/group database groups = grp.getgrall() # Process all users with uid for pw in pw_db: # add user to passwd file try: fd = open(etcuser + '/passwd', 'a') fd.write(pw.pw_name+':x:'+str(pw[2])+':'+str(pw[3])+':'+pw[4]+':'+pw[5]+':'+pw[6]+'\n') fd.close() except: secureio.logging('Error while adding user '+pw.pw_name+' to passwd file', SILENT, 1) if not ignore_errors: sys.exit(2) # lookup the primary group and make sure it also exists in the jail if not addgrouptojail(bdir, pw[3], None, config): if not ignore_errors: sys.exit(2) # look up all other groups for gr in groups: if (pw.pw_name in gr.gr_mem): ret = addusertogroupinjail(bdir, gr.gr_gid, pw.pw_name, config) if not ret: if not ignore_errors: sys.exit(2) cagefslib.add_user_to_shadow(etcuser, pw.pw_name, config['verbose']) def remove_file_or_directory(path): try: sbuf = os.lstat(path) except (OSError, IOError): return if stat.S_ISDIR(sbuf.st_mode): try: shutil.rmtree(path, False) secureio.logging("Removed directory "+ path,SILENT,1) except (OSError, IOError, shutil.Error): secureio.logging('Error while removing directory '+ path,SILENT,1) else: try: os.unlink(path) secureio.logging('Removed file '+ path,SILENT,1) except (OSError, IOError): secureio.logging('Error while removing file '+ path,SILENT,1) # etc_user_version -> files_to_delete files_to_delete_cache = {} SPECIAL_PATHS = ['/mail/', '/'+cagefslib.CL_ALT_NAME+'/', '/'+cagefslib.CL_PHP_DIR_NAME+'/', '/'+cagefslib.CL_ALT_NAME+'/'] def check_special_paths(path): for spec_path in SPECIAL_PATHS: if path.startswith(spec_path): return False return True def clean_etc(user, userdir, etc_skel, config, etc_user_version, custom_etc_files = None): global files_to_delete_cache if cagefslib.custom_etc_present() or (etc_user_version not in files_to_delete_cache): # Get list of files in etc in userdir etc_user = {} cagefslib.add_tree_to_list(userdir+'/etc', etc_user, cut_path = userdir+'/etc') etc_user_list = list(etc_user) etc_user_list.sort() files_to_delete = compare_lists(etc_user_list, etc_skel) files_to_delete_cache[etc_user_version] = files_to_delete else: files_to_delete = files_to_delete_cache[etc_user_version] if custom_etc_files is None: custom_etc_files = cagefslib.get_additional_etc_files_for_user(user, userdir+'/etc') for _file in files_to_delete: file2 = cagefslib.addslash(_file) path = userdir + '/etc' + _file if path not in custom_etc_files and check_special_paths(file2): if config['dont-clean'] == 1: secureio.logging("Skipping "+ path, SILENT, config['verbose']) continue remove_file_or_directory(path) def get_all_users_from_passwd(): # get all users from /etc/passwd return list(secureio.clpwd.get_user_dict()) def update_etc(config, users=None, ignore_errors=True, all_users=False): secureio.logging("Updating users ..." , SILENT, 1) users = get_cagefs_users(users, all_users) # Get list of users whose passwd entry has been changed modified_users = get_modified_users() # Get version of etc skeleton etc_skel_version = get_etc_version(cagefslib.ETC_TEMPLATE_DIR+'/etc') cagefslib.make_dir(BASEDIR, 0o751, allow_symlink = True) if is_running_without_lve(): cagefslib.make_dir(BASEDIR_UID, 0o751, allow_symlink=True) etc_skel = None for user in users: prefix = get_user_prefix(user) prefixdir = BASEDIR + '/' + prefix dirname = '/' + prefix + '/' + user userdir = BASEDIR + dirname user_etc_path = userdir + '/etc' if cagefslib.make_dir(prefixdir, 0o751): # Error continue if cagefslib.make_dir(userdir, 0o751): # Error continue custom_etc_files = cagefslib.get_additional_etc_files_for_user(user, user_etc_path) custom_etc_files2 = cagefslib.cut_path(custom_etc_files, user_etc_path) # Get version of etc of user etc_user_version = get_etc_version(user_etc_path) if config['force-update-etc'] or (etc_skel_version > etc_user_version): secureio.logging("Updating user " + user + " ...", SILENT, 1) if (not os.path.isfile(userdir+'/etc/passwd')) or (config['init'] == 1) or (config['reinit'] == 1): copyetc(user, config, ignore_errors, custom_etc_files = custom_etc_files) else: copyetc(user, config, ignore_errors, custom_etc_files = custom_etc_files) if etc_skel == None: # Get list of files in etc template etc_skel = {} cagefslib.add_tree_to_list(cagefslib.ETC_TEMPLATE_DIR+'/etc', etc_skel, cut_path = cagefslib.ETC_TEMPLATE_DIR+'/etc') clean_etc(user, userdir, etc_skel, config, etc_user_version, custom_etc_files = custom_etc_files) else: message_printed = False if user in modified_users: secureio.logging("Updating user " + user + " ...", SILENT, 1) message_printed = True copyetc(user, config, ignore_errors, passwd_only = True) custom_files_to_delete = cagefslib.get_custom_etc_files_to_delete(user, custom_etc_files2) if custom_files_to_delete: if not message_printed: secureio.logging("Updating user " + user + " ...", SILENT, 1) if etc_skel == None: # Get list of files in etc template etc_skel = {} cagefslib.add_tree_to_list(cagefslib.ETC_TEMPLATE_DIR+'/etc', etc_skel, cut_path = cagefslib.ETC_TEMPLATE_DIR+'/etc') copyetc_needed = False for path in custom_files_to_delete: if path not in etc_skel: fullpath = user_etc_path + path if config['dont-clean'] == 1: secureio.logging("Skipping "+ fullpath, SILENT, config['verbose']) continue remove_file_or_directory(fullpath) else: copyetc_needed = True if copyetc_needed: copyetc(user, config, ignore_errors, custom_etc_files = custom_etc_files) cagefslib.update_custom_etc_files_for_user(user, user_etc_path) cagefslib.save_custom_etc_log(user, custom_etc_files2) cagefslib.create_utmp_for_user(user, exit_on_error=False) def enable_cagefs_for_users_with_duplicate_uids(enabled_users = None): if enabled_users is None: enabled_users = filter_users(get_list_of_users(True)) for user in enabled_users: toggle_user(user, True) def update_users_status(disable_all=False, users=None, status=None, fix_owner=False, old_enabled_users=None): if disable_all: users = get_all_users_from_passwd() cagefslib.update_status(users, False) elif users is not None and status is not None: if status: users = filter_users(users) cagefslib.update_status(users, status) else: enabled_users = filter_users(get_list_of_users(True)) cagefslib.update_status(enabled_users, True, fix_owner=fix_owner) if fix_owner: # process users with duplicate uids also enable_cagefs_for_users_with_duplicate_uids(enabled_users) disabled_users = get_list_of_users(False) cagefslib.update_status(disabled_users, False, fix_owner=fix_owner) # for reinit case we do not want to reload php process # because reinit clear skeleton and we do not need this extra action if old_enabled_users is not None: # it is enough to know only list of enabled users before status change reload_php_for_users_with_changed_status(old_enabled_users) def reload_php_for_users_with_changed_status(old_enabled_users): """ Filter users and reload php process only if status was REALLY changed :param old_enabled_users: enabled users before any status change :return: """ new_enabled_users = get_enabled_users() # old enabled users: u1, u2 # we want to enable: u2, u3 # php process will be reloaded only for u1, u3 # the same with disable status users_to_kill_process = list(set(old_enabled_users).symmetric_difference(new_enabled_users)) reload_php_for_users(users_to_kill_process) def get_cagefs_users(users = None, all_users = False, raise_exception = False): if users is None: if all_users: users = get_all_users_from_passwd() else: # Get list of enabled users users = get_list_of_users(True, raise_exception) users = filter_users(users) return users def create_utmp_for_all_users(): """ Create user's personal /home/user/.cagefs/var/run/cagefs/utmp file for all users For details see CAG-706 """ for user in get_cagefs_users(all_users=True): cagefslib.create_utmp_for_user(user, exit_on_error=False) def reload_php_for_users(users = None, all_users = False): users = get_cagefs_users(users, all_users) for user in users: reload_processes('php', user) EA4_PHP_CONF = '/etc/cpanel/ea4/php.conf' def create_files_for_symlink_protection(): """ Configure symlink protection for symlinks created for integration with cPanel MultiPHP Return True if error has occured """ if is_running_without_lve(): return False if not is_ea4_enabled(): return False try: linksafe_gid = grp.getgrnam('linksafe').gr_gid except KeyError: # linksafe group not found - symlink protection is not configured properly return False conf = read_cpanel_ea4_php_conf() if not conf: return False error = False umask_old = os.umask(0o22) for alias in conf: if alias.startswith('ea-php'): for path in SYMLINKS.values(): try: file_path = path[1] % alias f = open(file_path, 'w') f.write('CageFS integration for cPanel MultiPHP\n') f.close() os.chown(file_path, 0, linksafe_gid) except OSError as e: secureio.print_error('failed to create file', file_path, ':', str(e)) error = True os.umask(umask_old) return error def php_version_is_removed(php_vers): if php_vers == 'native': return False return php_vers not in cagefslib.get_alt_versions() def php_version_is_disabled(php_vers, cl_alt_def_php_state): return (cl_alt_def_php_state != None) and (php_vers in cl_alt_def_php_state) and (not cl_alt_def_php_state[php_vers]) # Returns True if error has occurred def create_etc_alternatives(users=None, all_users=False, repair_symlinks=False, reset_modules_to_default=False, rebuild_alt_php_ini=False): users = get_cagefs_users(users, all_users) error = False umask_saved = os.umask(0) for user in users: pw = secureio.clpwd.get_pw_by_name(user) prefix = get_user_prefix(user) dirname = '/'+prefix+'/'+user userdir = BASEDIR + dirname # userdir = /var/cagefs/<prefix>/<user> if os.path.isdir(userdir): LINK_DIR = cagefslib.ETC_CL_ALT_PATH link_dir = userdir + LINK_DIR # cannot drop permissions because root only can write to /var/cagefs/prefix/user/etc if cagefslib.make_dir(link_dir, 0o755): continue cagefslib.set_owner(link_dir, pw.pw_uid, pw.pw_gid) # cannot drop permissions because root only can write to /var/cagefs/prefix/user/etc user_php_dir = userdir + cagefslib.ETC_CL_PHP_PATH # user_php_dir = /var/cagefs/prefix/user/etc/cl.php.d if cagefslib.make_dir(user_php_dir, 0o755): continue cagefslib.set_owner(user_php_dir, pw.pw_uid, pw.pw_gid) cl_alt_def_vers, cl_alt_def_modules, cl_alt_def_php_state, cl_alt_def_other = cagefslib.read_cl_alt_defaults() # @UnusedVariable def_vers, php_modules, php_state_ignored, other_ignored = cagefslib.read_cl_alt_backup_as_user(pw.pw_dir, pw.pw_uid, pw.pw_gid) # @UnusedVariable def_vers_old = def_vers # Backup is absent or backup exists and php version (from backup) is disabled or removed? if (def_vers == None) or ((def_vers != None) and (php_version_is_removed(def_vers) or php_version_is_disabled(def_vers, cl_alt_def_php_state))): # global defaults are absent or global defaults exist and default php version is disabled or removed? if (cl_alt_def_vers == None) or ((cl_alt_def_vers != None) and (php_version_is_removed(cl_alt_def_vers) or php_version_is_disabled(cl_alt_def_vers, cl_alt_def_php_state))): def_vers = 'native' else: def_vers = cl_alt_def_vers changed = False for alias in cagefslib.orig_binaries: # orig_path = cagefslib.orig_binaries[alias] filename = alias if def_vers == 'native': LINK_TO = cagefslib.get_usr_selector_path(alias) else: alt_path = cagefslib.get_alt_conf(def_vers, alias) if alt_path != None: LINK_TO = alt_path else: LINK_TO = cagefslib.get_usr_selector_path(alias) LINK_NAME = cagefslib.ETC_CL_ALT_PATH+'/'+filename link_name = userdir + LINK_NAME if not os.path.islink(link_name): cagefslib.remove_file_or_dir(link_name) try: os.symlink(LINK_TO, link_name) if alias == 'php.ini': changed = True except OSError as e: msg = f'Error: failed to create symlink {link_name} : {str(e).replace("Errno", "Err code")}' logger.error(msg, exc_info=e) secureio.logging(msg, SILENT, 1) error = True elif repair_symlinks: try: link_to = os.readlink(link_name) except OSError as e: msg = f'Error: failed to read symlink {link_name} : {str(e).replace("Errno", "Err code")}' logger.error(msg, exc_info=e) secureio.logging(msg, SILENT, 1) error = True continue if link_to.startswith('/opt/alt/php'): link_to_dirname, link_to_filename = os.path.split(link_to) _dirname, _filename = os.path.split(LINK_TO) repaired = None if link_to_dirname == _dirname and link_to_filename != _filename: repaired = LINK_TO if repaired != None: try: os.unlink(link_name) os.symlink(repaired, link_name) except (OSError, IOError) as e: msg = f'Error: failed to create symlink {link_name} : {str(e).replace("Errno", "Err code")}' logger.error(msg, exc_info=e) secureio.logging(msg, SILENT, 1) error = True cagefslib.select_default_php_modules(user_php_dir, pw.pw_dir, pw.pw_uid, pw.pw_gid, def_vers, cl_alt_def_modules, php_modules, changed, def_vers_old, reset_modules_to_default, rebuild_alt_php_ini, user) if cagefs_da_lib.create_php_ini_for_DA(userdir, user, def_vers, pw.pw_uid, pw.pw_gid): error = True # Switch symlink so it will point to directory with modules for alt-php # For details see CAG-447 if configure_alt_php(pw, def_vers, force=changed): error = True os.umask(umask_saved) return error def reset_modules_to_default(users = None): if not users: users = None create_etc_alternatives(users = users, all_users = True, reset_modules_to_default = True) reload_php_for_users(users = users) def rebuild_alt_php_ini(users = None): if not users: users = None create_etc_alternatives(users = users, all_users = True, rebuild_alt_php_ini = True) reload_php_for_users(users = users) def check_php_ini_options(users=None): if not users: users = None cagefslib.validate_alt_php_ini = True create_etc_alternatives(users=users, all_users=True, rebuild_alt_php_ini=True) reload_php_for_users(users=users) def add_new_line(lines): # file is not empty and last line in the file does not end with new line symbol ? if lines and lines[0] != '' and lines[-1][-1] != '\n': lines[-1] += '\n' return True return False def write_cagefs_mp(new_lines): # write cagefs.mp file if needed if new_lines: lines = read_file(ETC_MPFILE) add_new_line(lines) lines.extend(new_lines) cagefslib.write_file(ETC_MPFILE, lines) os.chmod(ETC_MPFILE, 0o600) create_remount_flag() def add_mounts_for_ea_php_sessions(): """ Add mount points like "@/var/cpanel/php/sessions/ea-php56,700" to /etc/cagefs/cagefs.mp file """ if os.path.isdir('/var/cpanel/php/sessions') and os.path.isfile(ETC_MPFILE): mp_config = MountpointConfig( skip_errors=True, skip_cpanel_check=True, ignore_cache=True, ) personal_mounts = mp_config.personal_mounts new_lines = [] # get dirnames of all alt-php dirs as list php_dirs = glob.glob('/var/cpanel/php/sessions/ea*') for php_dir in php_dirs: if php_dir not in personal_mounts and os.path.isdir(php_dir): mount_str = '@%s,700\n' % php_dir new_lines.append(mount_str) write_cagefs_mp(new_lines) def add_mounts_for_php_selector(): """ Add mount points for php selector and alt-php to /etc/cagefs/cagefs.mp file """ mp_config = MountpointConfig( skip_errors=True, skip_cpanel_check=True, ignore_cache = True, ) cagefslib.mounts = mp_config.common_mounts personal_mounts = mp_config.personal_mounts new_lines = [] # Add '/opt/alt' to cagefs.mp if needed if ('/opt/alt\n' not in cagefslib.mounts) and ('/opt\n' not in cagefslib.mounts): cagefslib.mounts.append('/opt/alt\n') new_lines.append('/opt/alt\n') # get dirnames of all alt-php dirs as list alt_php_dirs = cagefslib.get_alt_dirs() for php_dir in alt_php_dirs: # something like /opt/alt/php55/link mount_path = '/opt/alt/%s/link' % php_dir mount_str = '@/opt/alt/%s/link,700\n' % php_dir if mount_path not in personal_mounts: new_lines.append(mount_str) mount_path = '/opt/alt/%s/var/lib/php/session' % php_dir mount_str = '@/opt/alt/%s/var/lib/php/session,700\n' % php_dir if mount_path not in personal_mounts: new_lines.append(mount_str) # add php-newrelic logs path, CAG-1118 mount_path = '/var/log/alt-%s-newrelic' % php_dir mount_str = '@/var/log/alt-%s-newrelic,700\n' % php_dir if mount_path not in personal_mounts: new_lines.append(mount_str) write_cagefs_mp(new_lines) def remove_mounts_for_php_selector(): """ Remove mount points for uninstalled alt-php versions from /etc/cagefs/cagefs.mp file """ php_alt_dirs = cagefslib.get_alt_dirs() needed_mounts = set(['/opt/alt/%s/link' % php_dir for php_dir in php_alt_dirs]) needed_mounts.update(['/opt/alt/%s/var/lib/php/session' % php_dir for php_dir in php_alt_dirs]) needed_mounts.update(['/var/log/alt-%s-newrelic' % php_dir for php_dir in php_alt_dirs]) lines = read_file(ETC_MPFILE) new_lines = [] pattern = re.compile(r'@(/opt/alt/php\d\d/link|/opt/alt/php\d\d/var/lib/php/session|/var/log/alt-php\d\d-newrelic),') changed = False for line in lines: m = pattern.match(line) if m: if m.group(1) in needed_mounts: new_lines.append(line) else: changed = True else: new_lines.append(line) if changed: cagefslib.write_file(ETC_MPFILE, new_lines) os.chmod(ETC_MPFILE, 0o600) create_remount_flag() def add_mount_for_php_apm(): """ Adds mount point for default location of PHP APM DB :return: """ if not os.path.isfile(ETC_MPFILE): # do nothing, if cagefs.mp is absent return path_to_add = '/var/php/apm/db' personal_mounts = [] mp_config = MountpointConfig( skip_errors=True, skip_cpanel_check=True, ) mounts = mp_config.common_mounts personal_mounts = mp_config.personal_mounts if path_to_add not in personal_mounts and path_to_add not in mounts: # Path not found, add it lines = read_file(ETC_MPFILE) add_new_line(lines) lines.append('@%s,777\n' % path_to_add) cagefslib.write_file(ETC_MPFILE, lines) os.chmod(ETC_MPFILE, 0o600) create_remount_flag() def create_dirs_for_symlink_protection(): """ Cretate /etc/cl.php.d/alt-phpNN directories for all alt-php versions (in real filesystem) with group owner 'linksafe' for details see CAG-532, CAG-454 Return True if error has occured """ if is_running_without_lve(): return False try: linksafe_gid = grp.getgrnam('linksafe').gr_gid except KeyError: # linksafe group not found - symlink protection is not configured properly return False alt_php_dirs = cagefslib.get_alt_dirs() error = False for php_dir in alt_php_dirs: etc_php_dir = '/etc/cl.php.d/alt-%s' % php_dir alt_php_ini = '/etc/cl.php.d/alt-%s/alt_php.ini' % php_dir try: if not os.path.exists(etc_php_dir): mod_makedirs(etc_php_dir, 0o755) os.chown(etc_php_dir, 0, linksafe_gid) open(alt_php_ini, 'w').close() os.chown(alt_php_ini, 0, linksafe_gid) except OSError as e: secureio.print_error('failed to configure linksafe', ':', str(e)) error = True return error def sync_etc_cl_selector_dir(): # create copy of /etc/cl.selector as /etc/cl.selector.conf.d in etc template cl_selector_target = os.path.join(cagefslib.ETC_TEMPLATE_NEW_DIR, 'etc/cl.selector.conf.d') cagefslib.copytree('/etc/cl.selector', cl_selector_target) def clean_dir_recursive(cagefs_dir, orig_dir): """ Delete from cagefs_dir files/dirs that do not exist in orig_dir :param cagefs_dir: path to dir in CageFS (dir to delete files from) :type cagefs_dir: string :param orig_dir: path to original dir :type orig_dir: string """ for file_name in os.listdir(cagefs_dir): if file_name.endswith('.cagefs'): continue orig_path = os.path.join(orig_dir, file_name) cagefs_path = os.path.join(cagefs_dir, file_name) if not os.path.lexists(orig_path): remove_file_or_directory(cagefs_path) elif os.path.isdir(cagefs_path) and not os.path.islink(cagefs_path): # cagefs_path is a directory ? clean_dir_recursive(cagefs_path, orig_path) def setup_cpanel_multiphp(do_mount=False, config=None): """ Setup CageFS for integration with cPanel MultiPHP For details please see CAG-445 :param do_mount: when True, do mounting; when False, do copying files to CageFS :type do_mount: bool """ if not is_ea4_enabled(): # EasyApache4 is not setup return SYMLINK_NAMES = {'php-cgi':'/etc/cl.selector/ea-php', 'php':'/etc/cl.selector/ea-php-cli', 'php.ini':'/etc/cl.selector/ea-php.ini', 'lsphp':'/etc/cl.selector/ea-lsphp'} CAGEFS_PHP_BASEDIR = '/usr/share/cagefs/.cpanel.multiphp' DIRS_TO_MOUNT = ('/opt/cpanel/%s/root/usr/bin', '/opt/cpanel/%s/root/etc') conf = read_cpanel_ea4_php_conf() if not conf: return try: # get default system php version selected via MultiPHP Manager in cPanel WHM default_php = conf['default'] except KeyError: return for alias in conf: if alias.startswith('ea-php'): for path in DIRS_TO_MOUNT: optdir = path % alias cagefs_optdir = '%s%s' % (SKELETON, optdir) cagefs_dir = '%s%s' % (CAGEFS_PHP_BASEDIR, optdir) if os.path.isdir(optdir): if not os.path.isdir(cagefs_dir): create_remount_flag() if cagefslib.make_dir(cagefs_dir, 0o755): # failed to create directory. so, we remove need.remount flag because we should not do remount in such case remove_remount_flag() sys.exit(1) if do_mount: ret = subprocess.call([MOUNT, "-n", "-o", "nosuid", "--rbind", cagefs_dir, cagefs_optdir]) if ret == 0: ret = subprocess.call([MOUNT, "-n", "-o", "remount,ro,nosuid,bind", cagefs_dir, cagefs_optdir]) if ret != 0: secureio.print_error("failed to mount", cagefs_dir) sys.exit(1) else: # copy php files to cagefs for file_name in os.listdir(optdir): if file_name.endswith('.cagefs'): continue orig_path = os.path.join(optdir, file_name) dest_file = os.path.join(cagefs_dir, file_name) if os.path.isdir(orig_path): if cagefslib.copytree(orig_path, dest_file, update=True): secureio.logging('Error copying '+orig_path+' to '+dest_file, SILENT, 1) sys.exit(1) continue if alias == default_php and file_name in SYMLINK_NAMES: if switch_symlink(SYMLINK_NAMES[file_name], dest_file, silent=SILENT): cagefslib.kill_php(file_name) if switch_symlink(SYMLINK_NAMES[file_name], dest_file, silent=SILENT): sys.exit(1) dest_file += '.cagefs' if cagefslib.copy_file(orig_path, dest_file, create_parent_dir=False, update=True): # Error has occured - kill php processes and try copy again cagefslib.kill_php(file_name) if cagefslib.copy_file(orig_path, dest_file, create_parent_dir=False): secureio.logging('Error copying '+orig_path+' to '+dest_file, SILENT, 1) sys.exit(1) if dest_file.endswith('.cagefs') and (linksafe_gid := get_linksafe_gid()) is not None: os.chown(dest_file, -1, linksafe_gid) if not do_mount: # delete old (unneeded) files clean_dir_recursive(CAGEFS_PHP_BASEDIR+'/opt/cpanel', '/opt/cpanel') # Compare php.conf files and update php.conf file in CageFS when needed if (config is not None) and (not do_mount): php_conf = cagefslib.ETC_TEMPLATE_DIR + EA4_PHP_CONF if not os.path.isfile(php_conf) or cagefslib.is_update_needed(EA4_PHP_CONF, php_conf, use_cache=False): update_etc_only(config) def setup_cl_alt(config, options=None): check_skeleton() error = create_files_for_symlink_protection() error = create_dirs_for_symlink_protection() or error cagefs_da_lib.create_symlink_to_php_ini_for_DA(SKELETON) cagefs_da_lib.configure_selector_for_directadmin() # copy lsphp binary if needed if not os.path.isfile('/usr/local/bin/lsphp'): if os.path.isfile('/usr/local/lsws/fcgi-bin/lsphp5'): lsphp_path = os.path.realpath('/usr/local/lsws/fcgi-bin/lsphp5') cagefslib.copy_file(lsphp_path, '/usr/local/bin/lsphp') # alt-php versions are installed ? alt = cagefslib.get_alt_versions() if alt: from cagefsreconfigure import litespeed_configure_selector, replace_alt_settings litespeed_configure_selector() replace_alt_settings(options) # Create /opt/alt directory if needed if not os.path.lexists('/opt/alt'): try: mod_makedirs('/opt/alt', 0o755) create_remount_flag() except (OSError, IOError): secureio.print_error('failed to create directory /opt/alt') sys.exit(1) add_mounts_for_php_selector() add_mounts_for_ea_php_sessions() # Read paths to php binaries from config file cagefslib.read_native_conf() # Create directories of CL Selector in etc and convert modules from symlinks to single ini file (if needed) create_etc_alternatives(all_users = True, repair_symlinks = True) # Update native php files in etc directory if cagefslib.is_etc_in_native_conf(): error = update_etc_only(config, print_selector_errors = True) or error # Update native php files in cagefs-skeleton for alias, orig_path in cagefslib.orig_binaries.items(): orig_path2 = os.path.realpath(orig_path) if (not orig_path.startswith('/etc/') and os.path.islink(orig_path) and not cagefslib.path_is_mounted(orig_path) and cagefslib.copy_file(orig_path, SKELETON + orig_path) == 1): error = True if is_ea4_enabled() and alias in ('php', 'php-cli', 'lsphp', 'php.ini'): # Update php file inside cagefs without replacing it with symlink if (not orig_path2.startswith('/etc/') and os.path.exists(orig_path2) and not cagefslib.path_is_mounted(orig_path2) and cagefslib.copy_file(orig_path2, SKELETON + orig_path2) == 1): error = True # Create stubs for php files to make selectorctl, cl-selector work as before if cagefslib.create_php_stub(alias): error = True continue if not orig_path2.startswith('/etc/'): if cagefslib.path_is_mounted(orig_path2): secureio.print_error(orig_path2, 'is mounted to CageFS. CloudLinux Selector will not be available.') error = True elif not cagefslib.move_to_alternatives(orig_path2, etc = False) and cagefslib.is_mandatory(alias): secureio.print_error("CloudLinux Selector setup error for path:", orig_path) error = True # alt-php versions are installed ? if alt and (not error): import cagefs_ispmanager_lib cagefs_ispmanager_lib.configure_selector_for_ispmanager() setup_cpanel_multiphp(do_mount=False, config=config) if not config['skip-php-reload']: reload_php_for_users() if not error: print("CloudLinux Selector setup: successful") # Checks symlinks in /etc/cl.selector for users and replaces broken symlinks (i.e. symlinks # to non-existent alternatives) with symlinks to native (original) binaries. # when force == True, replaces ALL symlinks with symlinks to native (original) binaries # (i.e. reset "alternatives" settings for users to "native") # Returns True if error has occured def remove_etc_alternatives(users = None, all_users = False, force = False): users = get_cagefs_users(users, all_users) cl_alt_def_vers, cl_alt_def_modules, cl_alt_def_php_state, cl_alt_def_other = cagefslib.read_cl_alt_defaults() alt_versions = cagefslib.get_alt_versions() alt_paths = get_alt_paths(cl_alt_def_php_state) if cl_alt_def_vers == None: # global defaults are not set (link files to native binary) dest_vers = 'native' elif cl_alt_def_vers == 'native': # global default version is native (link files to native binary) dest_vers = 'native' elif (cl_alt_def_vers not in alt_versions) or ((cl_alt_def_vers in cl_alt_def_php_state) and (not cl_alt_def_php_state[cl_alt_def_vers])): # default version has been removed or disabled # link files to native binary # set global default version to native dest_vers = 'native' # write global defaults (owner - root) cagefslib.write_cl_alt_to_backup(None, dest_vers, cl_alt_def_modules, 0, 0, cl_alt_def_php_state, cl_alt_def_other) else: # link files to global default binary (global defaults are set already) dest_vers = cl_alt_def_vers native_php_is_disabled = False if (cl_alt_def_php_state != None) and ('native' in cl_alt_def_php_state) and (not cl_alt_def_php_state['native']): native_php_is_disabled = True error = False for user in users: pw = secureio.clpwd.get_pw_by_name(user) prefix = get_user_prefix(user) dirname = '/'+prefix+'/'+user userdir = BASEDIR + dirname LINK_DIR = cagefslib.ETC_CL_ALT_PATH link_dir = userdir + LINK_DIR if os.path.isdir(link_dir): changed = False # for filename in os.listdir(link_dir): for filename in cagefslib.get_alt_aliases(dest_vers): native_path = cagefslib.get_usr_selector_path(filename) if dest_vers == 'native': dest_path = native_path else: dest_path = cagefslib.get_alt_conf(dest_vers, filename) if dest_path == None: continue link_name = link_dir+'/'+filename if not os.path.islink(link_name): if filename == 'php.ini': changed = True cagefslib.remove_file_or_dir(link_name) try: os.symlink(dest_path, link_name) except (OSError, IOError): secureio.logging('Error while creating symlink ' + link_name, SILENT, 1) error = True else: try: link_to = os.readlink(link_name) except (OSError, IOError): secureio.logging('Error: failed to read symlink ' + link_name, SILENT, 1) error = True continue # Switch symlink if native php is disabled if (link_to == native_path) and (dest_vers != 'native') and (native_php_is_disabled or force): if filename == 'php.ini': changed = True if switch_symlink(dest_path, link_name, silent=SILENT): error = True # Path to alternative (link_to) must exist in real system. So path to skeleton is not added to os.path.lexists(link_to) # Skeleton may be unmounted, so path SKELETON+link_to may not exist # Switch symlink if alt-php version has been removed (or disabled) elif (link_to not in (native_path, dest_path)) and ( force or (link_to not in alt_paths) or (not os.path.lexists(link_to)) ): if filename == 'php.ini': changed = True if switch_symlink(dest_path, link_name, silent=SILENT): error = True # Switch symlink so it will point to directory with modules for alt-php # For details see CAG-447 if configure_alt_php(pw, dest_vers, force=changed): error = True if changed: reload_processes('php', user) # read user's backup def_vers, php_modules, php_state_ignored, other_ignored = cagefslib.read_cl_alt_backup_as_user(pw.pw_dir, pw.pw_uid, pw.pw_gid) # @UnusedVariable if (def_vers == None or def_vers != dest_vers) and changed: # Save backup cagefslib.write_cl_alt_to_backup(pw.pw_dir, dest_vers, php_modules, pw.pw_uid, pw.pw_gid) return error def kernel_is_supported(): if is_running_without_lve(): return True try: f = open('/proc/lve/list', 'r') line = f.readline() f.close() return bool(line) except IOError: return False def check_kernel(): if not kernel_is_supported(): remove_log_file() secureio.logging('Error: current running kernel is NOT supported') sys.exit(1) config_copy = {} def do_profiling(): update_cagefs(config_copy) def run_toggle_plugin_utility(): if not os.path.isfile(PLUGIN_STATE): return False try: ret = subprocess.call([PLUGIN_STATE, "--toggle-plugin"]) if ret != 0: secureio.logging('Error: '+PLUGIN_STATE, SILENT, 1) return True except OSError: secureio.logging('Error: failed to run '+PLUGIN_STATE, SILENT, 1) return True return False def toggle_plugin(): run_toggle_plugin_utility() def print_user_status(user): if cagefs_is_enabled(): if user in get_list_of_users(True): print('Enabled') sys.exit(0) print('Disabled') sys.exit(1) def print_cagefs_status(): if cagefs_is_enabled(): print('Enabled') sys.exit(0) print('Disabled') sys.exit(1) def read_paths_from_file(filename): """ Read paths separated by commas from a file """ cfg = configparser.ConfigParser(interpolation=None, strict=False) try: cfg.read(filename) except configparser.Error: return [] return cagefslib.config_get_option_as_list(cfg, filename, 'paths') def write_paths_to_file(filename, paths, comment = None): """ Write paths from list to a file """ paths = remove_duplicates(paths) paths = remove_parent_dirs(paths) umask_saved = os.umask(0o77) try: f = open(filename, 'w') f.write("[%s]\n" % filename) if comment: f.write("comment=%s\n" % comment) f.write("paths=") for index in range(len(paths)): if index > 0: f.write(", " + paths[index]) else: f.write(paths[index]) f.write("\n") f.close() except IOError as e: secureio.print_error('Failed to write ' + filename + ' : ' + str(e)) os.umask(umask_saved) def update_list(config): """ Read paths from stdin and updates appropriate files in cagefs-skeleton """ check_skeleton() if not os.path.isdir(cagefslib.ETC_TEMPLATE_DIR+'/etc'): secureio.print_error('skeleton of etc directory is not found') sys.exit(1) cagefslib.mounts = MountpointConfig().common_mounts files = sys.stdin.readlines() etc_found = False for filename in files: if filename.startswith('/etc/'): etc_found = True break if etc_found: # Create empty directory for template of etc directory shutil.rmtree(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', True) if not os.path.isdir(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc'): try: mod_makedirs(cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', 0o755) except OSError: secureio.print_error('creating', cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc') sys.exit(1) if cagefslib.copytree(cagefslib.ETC_TEMPLATE_DIR+'/etc', cagefslib.ETC_TEMPLATE_NEW_DIR+'/etc', True) == 1: secureio.print_error('Error while creating skeleton of etc directory') sys.exit(1) etc_modified = False copied_files = [] for filename in files: filename = filename.strip() if filename.startswith('/') and (os.path.isfile(filename) or os.path.islink(filename)): if filename.startswith('/etc/'): if not cagefslib.copy_file(filename, cagefslib.ETC_TEMPLATE_NEW_DIR+filename, create_parent_dir = True): print(filename) etc_modified = True copied_files.append(filename) elif (not cagefslib.path_is_mounted(filename)): if not cagefslib.copy_file(filename, SKELETON+filename, create_parent_dir = True): print(filename) copied_files.append(filename) if etc_modified: compare_etc_templates() # CageFS is enabled ? if (not save_dir_exists()) and etcfs_is_disabled(): update_etc(config) # Create cfg-file that contains copied paths (needed for cagefsctl --update to update those paths) UPDATE_LIST_CFG_FILE = CONFIG_DIR + 'cagefsctl-update-list.cfg' cfg = read_paths_from_file(UPDATE_LIST_CFG_FILE) cfg.extend(copied_files) write_paths_to_file(UPDATE_LIST_CFG_FILE, cfg, 'Files added by "cagefsctl --update-list" command') # Update list of files stored in cagefs-skeleton in order to handle deletion of files in cagefs-skeleton properly list_of_files = [] load_list(FILES_LIST, list_of_files) add_parents(copied_files, copied_files) list_of_files.extend(copied_files) save_list_of_files_in_skeleton(list_of_files) def demote(uid, gid): def func(): os.setgid(gid) os.setuid(uid) return func def tmpwatch(): pw = secureio.clpwd.get_user_dict() tmpwatch_command = cagefslib.get_tmpwatch_params() tmpwatch_dirs = set(cagefslib.get_tmpwatch_dirs()) tmpwatch_dirs.add('/var/cache/php-eaccelerator') if os.path.isdir('/var/lib/php/session'): tmpwatch_dirs.add('/var/lib/php/session') if not is_cpanel() and not is_plesk(): # clean php sessions for all alt-php versions (for CP other than cPanel: # on cPanel sessions are cleaned by /usr/share/cagefs/clean_user_php_sessions script # on Plesk sessions are cleaned by /usr/share/cagefs/clean_user_alt_php_sessions_plesk script # executed via /etc/cron.d/cpanel_php_sessions_cron) alt_php_dirs = cagefslib.get_alt_dirs() for php_dir in alt_php_dirs: tmpwatch_dirs.add('/opt/alt/%s/var/lib/php/session' % php_dir) dev_null = open('/dev/null', 'w') for user in pw: cmd = tmpwatch_command.split() line = pw[user] tmp_path = os.path.join(line.pw_dir, '.cagefs/tmp') # we assume that tmp directory always exists when cagefs is enabled # whenever there aren't tmp dir, we assume that cagefs isn't enabled if os.path.isdir(tmp_path): cmd.append(tmp_path) for dir_name in tmpwatch_dirs: dir_path = os.path.join(line.pw_dir, '.cagefs') + dir_name if os.path.isdir(dir_path): cmd.append(dir_path) subprocess.call(cmd, stderr=dev_null, stdout=dev_null, preexec_fn=demote(line.pw_uid, line.pw_gid), cwd=line.pw_dir) def invalid_homes_exist(): # get all users from /etc/passwd pw = secureio.get_pwd_dict() for user in pw: if pw[user].pw_dir.find('cagefs-skeleton') != -1: return True return False def get_users_from_args(args): users = [] for username in args: if user_exists(username): users.append(username) else: secureio.print_error('user', username, 'does not exist') sys.exit(1) return users def is_user_enabled(username): """ Check that cagefs enabled for user """ try: uid = secureio.clpwd.get_uid(username) except ClPwd.NoSuchUserException: return False user_prefix = get_user_prefix(username) get_min_uid() if uid >= MIN_UID: if os.path.exists(enabled_dir) and os.path.exists(enabled_dir + '/' + str(user_prefix) + '/' + username): return True if os.path.exists(disabled_dir) and not os.path.exists(disabled_dir + '/' + str(user_prefix) + '/' + username): return True return False def cpetc_for_user(username, config): user_etc_path = BASEDIR + '/' + get_user_prefix(username) + '/' + username + '/etc' custom_etc_files = cagefslib.get_additional_etc_files_for_user(username, user_etc_path) custom_etc_files2 = cagefslib.cut_path(custom_etc_files, user_etc_path) copyetc(username, config, ignore_errors = False, recreate = False, custom_etc_files = custom_etc_files) cagefslib.update_custom_etc_files_for_user(username, user_etc_path) cagefslib.save_custom_etc_log(username, custom_etc_files2) # Add the spamassassin directories to CageFs in cPanel def add_spamassassin_dirs_cpanel(): # Add spamassassin directories to cagefs.mp file try: # exit, if cagefs.mp absent if not os.path.isfile(ETC_MPFILE): return # 1. Read cagefs.mp file f = open(ETC_MPFILE, 'r') cagefs_mp_lines = f.readlines() f.close() cagefs_mp_lines = [l.strip() for l in cagefs_mp_lines] # 2. Modify cagefs.mp contents for line in SPAMASSASSIN_DIRS_FOR_CAGEFS: line_to_check_and_write = '!'+line if line_to_check_and_write not in cagefs_mp_lines and os.path.isdir(line): cagefs_mp_lines.append(line_to_check_and_write) cagefs_mp_lines = [l+'\n' for l in cagefs_mp_lines] # 3. Write cagefs.mp back f = open(ETC_MPFILE, 'w') f.writelines(cagefs_mp_lines) f.close() except OSError as e: print('Error:', str(e), file=sys.stderr) def unmount_dir_in_lve(path: str, lve_list: List[int]) -> None: """ Unmount path in all LVE namespaces. Enter to LVE and unmount directory without destroying LVE. :param: path `str` path for unmount :param: lve_list `list` list of id's for existing LVEs :return: None """ for lve_id in lve_list: p = subprocess.Popen([LVE_UMOUNT, str(lve_id), path], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) _, strerr = p.communicate() if VERBOSE: if p.returncode: # error occured secureio.print_error('LVE', lve_id, strerr.strip()) else: print('Unmount for LVE', lve_id, 'succeeded') def unmount_dir_for_all_processes(path: str) -> None: """ Unmount directory in all mount namespaces of all processes running in a system :param path: absolute path to directory to unmount """ # get list of PIDs of all running processes ps_cmd = ['/bin/ps', '--no-headers', '-xao', 'pid'] p = subprocess.run(ps_cmd, capture_output=True, text=True) if p.returncode: # error occured secureio.print_error('failed to execute:', *ps_cmd, 'return code:', p.returncode, 'stderr:', str(p.stderr).strip()) sys.exit(1) pids = p.stdout.split() for pid in pids: if pid: # enter mount namespace and unmount directory ignoring errors p = subprocess.run(['/usr/bin/nsenter', '-m', '-t', pid, UMOUNT, '-l', path], capture_output=True, text=True) if VERBOSE: if p.returncode: # error occured secureio.print_error('PID', pid, 'stderr:', str(p.stderr).strip()) else: print('Unmount for PID', pid, 'succeeded') def unmount_dir(dir_list: List[str]) -> None: """ Unmount directories from list in all mount namespaces :param dir_list: list of paths to directories for unmounting """ # check if all directories are unmounted in real FS mounted_dirs = cagefslib.get_mounted_dirs(all_mounts=True) found = False for directory in dir_list: if os.path.realpath(directory) in mounted_dirs: found = True secureio.print_error('directory', directory, 'is mounted. ', 'Please unmount the directory before running this command.') if found: sys.exit(1) # update namespace template using "lvectl start", so subsequent enters # to CageFS/namespace will be with updated set of mounts lvectl_start() for directory in dir_list: # unmount directory in all existing LVE/namespaces unmount_dir_in_lve(directory, get_lve_list()) unmount_dir_for_all_processes(directory) def unmount_user(user_name): """ Unmount CageFS for user. Return True if error has occured :param user_name: name of user :type user_name: str """ try: pw = secureio.clpwd.get_pw_by_name(user_name) except ClPwd.NoSuchUserException: secureio.print_error("User", user_name, "does not exists") return True if is_running_without_lve(): return cagefs_without_lve_lib._delete_namespace_user(user_name) # acqire lock prefix = get_user_prefix(user_name) lock_path = os.path.join(BASEDIR, prefix, user_name+'.lock') dir_path = os.path.dirname(lock_path) cagefslib.make_dir(dir_path, 0o751) _ = acquire_lock(lock_path, wait=True, quiet=True) # destroy and recreate LVE in order to remove effect of chroot/pivot_root syscalls # (otherwise we will be not able to unmount all CageFS mounts) if remount([user_name]): secureio.print_error("Failed to destroy/apply LVE for user", user_name) return True # enter LVE/namespace and unmount all CageFS mounts # use '-mek' options for lve_suwrapper in order to disable PMEM, NPROC, EP limits # and also prevent killing cagefsctl process inside LVE by lvectl destroy cmd = ["/bin/lve_suwrapper", '-mek', str(pw.pw_uid), "/usr/sbin/cagefsctl", "--unmount-cur-ns"] try: subprocess.call(cmd, shell=False) except OSError: secureio.print_error(*cmd) return True return False def check_cagefs_skeleton(): """ Checks that cagefs skeleton exists and is not empty """ return os.path.isdir(os.path.join(SKELETON, 'bin')) def print_cagefs_skeleton_status(): if not check_cagefs_skeleton(): print(SKELETON_NOT_INITIALIZED) sys.exit(1) print(SKELETON_INITIALIZED) def main(): import syslog syslog.openlog(native_str('cagefsctl')) try: main_func() except SystemExit as e: sys.exit(int(str(e))) except Exception as e: cagefslib.print_exception(level = syslog.LOG_ERR, includetraceback = True) sys.exit(1) def init_min_uid(): # Read MIN_UID from file get_min_uid() # Copy MIN_UID to securelve module secureio.MIN_UID = MIN_UID # create ClPwd instance secureio.clpwd = ClPwd(min_uid = MIN_UID) init_min_uid() def _get_username_list_from_args(args: List[str]) -> List[str]: """ Retrives users list from cmd line :param args: args list :return: users list """ users_list = [] for username in args: try: pw_db = secureio.clpwd.get_pw_by_uid(int(username)) for pw in pw_db: users_list.append(pw.pw_name) except (ValueError, ClPwd.NoSuchUserException): if user_exists(username): users_list.append(username) else: secureio.print_error('user or UID', username, 'does not exist') sys.exit(1) return users_list def exit_if_lve_supported(): """ Print error and exit when LVE is supported """ if not is_running_without_lve(): print("ERROR: This command is workable only in environments without LVE support") sys.exit(1) def exit_if_lve_not_supported(): """ Print error and exit when LVE is not supported """ if is_running_without_lve(): print("ERROR: This command is not supported in environments without LVE support") sys.exit(1) def create_namespaces(users : Optional[List[str]] = None, do_mount_skel : bool = False) -> bool: exit_if_lve_supported() check_skeleton() remove_nested_skeleton() if do_mount_skel and not skeleton_is_mounted(): mount_skeleton() if users is None: users = get_enabled_users() errors_number = cagefs_without_lve_lib.create_namespace_user_list(users) # return error only if all users fail # ignore errors when only couple users fail (CLOS-2985) return bool(errors_number) and errors_number == len(users) def delete_namespaces(users : Optional[List[str]] = None) -> bool: exit_if_lve_supported() if users is None: users = get_cagefs_users(all_users=True) return cagefs_without_lve_lib.delete_namespace_user_list(users) def clean_without_lve_environment() -> int: exit_if_lve_supported() rc = 0 rc += delete_namespaces() cagefs_without_lve_lib.restore_httpd_php_fpm_services() cagefs_universal_hook_lib.remove_without_lve_universal_hooks() return rc def main_func(): global SKELETON, do_not_ask_option, config_copy, VERBOSE try: options_list = ["update", "dont-clean", "reinit", "help", 'version', "verbose", "force", 'hardlink', 'init', 'remove-all', 'set-tmpwatch=', 'tmpwatch', 'list', 'help', 'unmount', 'unmount-dir', 'unmount-all', 'unmount-really-all', 'enable', 'disable', 'enable-all', 'disable-all', 'display-user-mode', 'list-enabled', 'wait-lock', 'list-disabled', 'create-mp', 'check-mp', 'mount-skel', 'unmount-skel', 'remount-all', 'remount', 'addrpm','delrpm', 'enter=', 'enable-cagefs', 'disable-cagefs','do-not-ask', 'debug', 'profiling', 'migrate-prefixes', 'getprefix=', 'list-rpm', 'apply-global-php-ini', 'set-min-uid=', 'get-min-uid', 'toggle-mode', 'silent', 'cpetc', 'update-etc', 'force-update-etc', 'check-kernel-version', 'update-wrappers', 'remove-blacklisted', 'detect-postgres', "toggle-plugin", 'print-suids', 'hook-install', 'hook-remove', 'reconfigure-cagefs', 'configure-litespeed', 'clean-var-cagefs', 'user-status=', 'cagefs-status', 'update-list', 'rebuild-alt-php-ini', 'validate-alt-php-ini', 'setup-cl-selector', 'skip-php-reload', 'check-for-unsafe-mounts', 'remove-cl-selector', 'cl-selector-reset-versions', 'setup-cl-alt', 'remove-cl-alt', 'cl-selector-reset-modules', 'update-users-status', "update-users-status-fix-owner", 'set-default-user-status', 'remove-unused-mount-points', 'create-homeN-dirs-in-skeleton', 'unmount-cur-ns', 'configure-openlitespeed', 'enable-cagefs-without-etc-update', 'without-lock', 'set-update-period=', 'force-update', 'add-default-rpm-packages', 'create-virt-mp', 'create-virt-mp-all', 'remount-virtmp', 'list-logged-in', 'clean-config-dirs', 'create-dirs-for-symlink-protection', 'sanity-check', 'check-cagefs-initialized' ] if is_running_without_lve(): options_list.extend(['create-namespace', 'create-namespaces', 'delete-namespace', 'delete-namespaces', 'clean-without-lve-environment']) opts, args = getopt.getopt(sys.argv[1:], "ihvVfurdkwW?lmMe:", options_list) except getopt.GetoptError as e: usage() print("\nError:", str(e)) sys.exit(1) import cagefsreconfigure import virtmp_mount # Logging all args from clcommon import ClAuditLog log = ClAuditLog ( INFO_LOG_FILE ) log.info_log_write () config = {} config['verbose'] = 0 for o, a in opts: if o in ("-h", "-?", "--help"): usage() sys.exit(0) elif o in ("-V", "--version"): print(cagefs_version) sys.exit(0) elif o in ("--check-kernel-version",): check_kernel() sys.exit(0) elif o in ("-v", "--verbose"): config['verbose'] = 1 VERBOSE = 1 cagefslib.VERBOSE_FLAG = 1 elif o in ("--silent",): global SILENT SILENT=1 secureio.SILENT_FLAG = 1 if (os.geteuid()!=0): secureio.print_error('root privileges required. Abort.') sys.exit(5) # remove possible symlinks from path to cagefs-skeleton try: SKELETON = os.path.realpath(SKELETON) cagefslib.SKELETON = SKELETON except: sys.stderr.write('Error while determining real path to skeleton directory '+SKELETON+'\n') sys.exit(1) config['dry-run'] = 0 config['interactive'] = 0 config['unmount'] = 0 config['enable'] = 0 config['disable'] = 0 config['force'] = 0 config['update'] = 0 config['reinit'] = 0 config['dont-clean'] = 0 config['hardlink'] = 0 config['init'] = 0 config['remount'] = 0 config['profiling'] = 0 config['force-update'] = 0 config['force-update-etc'] = False config['skip-php-reload'] = False manage_user_flag = False build_jail_flag = False lock_is_required = True wait_lock = True for o, a in opts: if o in ('--getprefix',): print(get_user_prefix(a)) sys.exit(0) elif o in ("-d", "--dont-clean"): config['dont-clean'] = 1 build_jail_flag = True elif o in ("--cpetc",): if (len(args) == 0): secureio.print_error('no username or UID specified') sys.exit(2) for username in args: try: pw_db = secureio.clpwd.get_pw_by_uid(int(username)) for pw in pw_db: cpetc_for_user(pw.pw_name, config) cagefslib.create_utmp_for_user(pw.pw_name) except (ValueError, ClPwd.NoSuchUserException): if user_exists(username): cpetc_for_user(username, config) cagefslib.create_utmp_for_user(username) else: secureio.print_error('user or UID', username, 'does not exist') sys.exit(1) sys.exit(0) elif o in ('--clean-config-dirs',): clean_config_dirs() sys.exit(0) elif o in ('--skip-php-reload',): config['skip-php-reload'] = True elif o in ('--create-dirs-for-symlink-protection',): create_dirs_for_symlink_protection() create_files_for_symlink_protection() sys.exit(0) elif o in ('--sanity-check',): import sanity_check sanity_check.check() sys.exit(0) elif o in ('--hook-install',): cagefshooks.HooksInstall() sys.exit(0) elif o in ('--hook-remove',): cagefshooks.HooksRemove() sys.exit(0) elif o in ('--reconfigure-cagefs',): cagefsreconfigure.reconfigure_cagefs() sys.exit(0) elif o in ('--configure-litespeed',): cagefsreconfigure.litespeed_configure() sys.exit(0) elif o in ('--configure-openlitespeed',): cagefsreconfigure.configure_open_litespeed() sys.exit(0) elif o in ('--list-enabled', '--list-disabled'): check_exclude() list_users(o == '--list-enabled') sys.exit(0) elif o in ("--cl-selector-reset-modules",): users = get_users_from_args(args) reset_modules_to_default(users) sys.exit(0) elif o in ("--rebuild-alt-php-ini",): # Retrive user names from args users = get_users_from_args(args) rebuild_alt_php_ini(users) sys.exit(0) elif o in ("--validate-alt-php-ini",): # Retrive user names from args users = get_users_from_args(args) check_php_ini_options(users) sys.exit(0) elif o in ("--cl-selector-reset-versions", "--remove-cl-selector", "--remove-cl-alt"): users = get_users_from_args(args) if users: remove_etc_alternatives(users = users, force = (o == "--cl-selector-reset-versions")) else: remove_etc_alternatives(all_users = True, force = (o == "--cl-selector-reset-versions")) remove_mounts_for_php_selector() sys.exit(0) elif o in ('-W', '--unmount-all', '--unmount-really-all'): # Unmounting for CageFS 2.0 is needed ? if (not os.path.exists('/etc/cagefs/etc.safe')) and (not os.path.exists(PROXY_COMMANDS)): repair_homes.umount_all() else: # Do unmounting for CageFS 3.0 unmount_all(remount_users = True, check_busy = False, all_cagefs_mounts = (o == '--unmount-really-all')) sys.exit(0) elif o in ("--unmount-dir",): exit_if_lve_not_supported() # check directory in args list existing if (len(args) == 0): secureio.print_error('no directory to unmount specified') sys.exit(2) unmount_dir(args) sys.exit(0) elif o in ("--migrate-prefixes",): migrate_to_new_prefixes() sys.exit(0) elif o in ('--set-min-uid',): set_min_uid(a) check_exclude() sys.exit(0) elif o in ('--get-min-uid',): print(MIN_UID) sys.exit(0) elif o in ('--set-update-period',): cagefslib.set_update_period(a) sys.exit(0) elif o in ('--force-update',): config['force-update'] = 1 config['update'] = 1 build_jail_flag = True check_skeleton() elif o in ('--clean-var-cagefs',): clean_var_cagefs() sys.exit(0) elif o in ('--update-wrappers',): load_wrappers(update_wrappers = True) sys.exit(0) elif o in ('--remove-blacklisted',): load_black_list(remove = True) sys.exit(0) elif o in ('--detect-postgres',): cagefslib.detect_postgres() sys.exit(0) elif o in ('--print-suids',): cagefslib.mounts = MountpointConfig().common_mounts # Print list of SUID and SGID programs in skeleton cagefslib.print_suids(SKELETON) sys.exit(0) elif o in ('--toggle-plugin',): toggle_plugin() sys.exit(0) elif o in ('--display-user-mode',): print_user_mode() sys.exit(0) elif o in ('--user-status',): check_exclude() print_user_status(a) sys.exit(0) elif o in ('--cagefs-status',): print_cagefs_status() sys.exit(0) elif o in ('--check-cagefs-initialized',): print_cagefs_skeleton_status() sys.exit(0) elif o in ("--disable-cagefs",): old_enabled_users = get_enabled_users() disable_cagefs() update_users_status(disable_all=True, old_enabled_users=old_enabled_users) remount_all() sys.exit(0) elif o in ("--update-users-status", "--update-users-status-fix-owner"): if o == "--update-users-status-fix-owner": get_mounted_users(fix_permissions=True) if cagefs_is_enabled(): old_enabled_users = get_enabled_users() check_exclude() update_users_status(fix_owner=(o == "--update-users-status-fix-owner"), old_enabled_users=old_enabled_users) sys.exit(0) elif o in ("--set-default-user-status",): # For use in postwwwacct hook only if args: mode = get_user_mode() if mode == 'Enable All': old_enabled_users = get_enabled_users() for username in args: toggle_user(username, True) update_users_status(users=args, status=True, old_enabled_users=old_enabled_users) elif mode == 'Disable All': old_enabled_users = get_enabled_users() for username in args: toggle_user(username, False) update_users_status(users=args, status=False, old_enabled_users=old_enabled_users) sys.exit(0) elif o in ("--remove-unused-mount-points",): remove_unused_mount_points() sys.exit(0) elif o in ('--create-homeN-dirs-in-skeleton',): check_skeleton() cagefslib.mounts = MountpointConfig().common_mounts create_homeN_dirs_in_skeleton() update_homeN_symlinks_in_skeleton() sys.exit(0) elif o in ("--mount-skel", "--unmount-skel"): lock_is_required = False elif ( o in ("-w", "--unmount", "-m", "--remount", "--enable", "--disable") and not is_running_without_lve() ): lock_is_required = False elif o in ("--without-lock",): lock_is_required = False elif o in ('--enable-cagefs-without-etc-update',): old_enabled_users = get_enabled_users() enable_cagefs() enable_cagefs_for_users_with_duplicate_uids() update_users_status(old_enabled_users=old_enabled_users) check_skeleton() remove_nested_skeleton() # Remount skeleton and all users mount_skeleton(True) sys.exit(0) elif o in ("--add-default-rpm-packages",): add_default_rpm_packages_to_cagefs() sys.exit(0) elif o in ('--tmpwatch',): tmpwatch() sys.exit(0) elif o in ('--set-tmpwatch',): cagefslib.set_tmpwatch_params(a) sys.exit(0) elif o in ('--create-virt-mp',): if len(sys.argv) != 3: secureio.print_error("No username provided") sys.exit(1) virtmp_mount.create_virtmp(sys.argv[2]) sys.exit(0) elif o in ('--create-virt-mp-all',): virtmp_mount.create_virtmp() sys.exit(0) elif o in ('--remount-virtmp',): if len(args) != 1: secureio.print_error("No username provided") sys.exit(1) virtmp_mount.create_virtmp(args[0]) # --remount USER check_save_dir() check_skeleton() remove_nested_skeleton() config['remount'] = 1 manage_user_flag = True elif o in ('--enter','-e'): check_save_dir() check_skeleton() check_kernel() remove_nested_skeleton() print('You are entering to CageFS for user', a, 'as superuser (root).') print('NOTE: You can use "su -s /bin/bash - {}" instead to enter to CageFS as user {}'.format(a, a)) try: subprocess.call(["/sbin/cagefs_enter_user", "--root", a, "/bin/bash"], shell=False) except Exception: secureio.print_error('executing /sbin/cagefs_enter_user') sys.exit(1) sys.exit(0) elif o in ("--check-mp",): check_mp_file() sys.exit(0) elif o in ("--check-for-unsafe-mounts",): if unsafe_mounts_exist(): create_remount_flag() sys.exit(0) elif o in ('-l', '--list'): exit_if_lve_not_supported() users = get_mounted_users() print_users(users, 1, 'CageFS currently mounted for users:') sys.exit(0) elif o in ('--list-logged-in',): exit_if_lve_not_supported() users = get_logged_in_users() print_users(users, 1, 'Users currently logged in CageFS via ssh:') sys.exit(0) elif o in ('--unmount-cur-ns',): # CAG-749: unmount CageFS mounts in all mount namespaces (resolve conflict with systemd) umount_skeleton(save_mounts = False, all_cagefs_mounts = True, current_namespace_only=True) sys.exit(0) check_kernel() if lock_is_required: # Acquire lock lockfile = acquire_lock(wait=wait_lock) # @UnusedVariable ex_list = get_exclude_user_list() check_exclude(ex_list) if SKELETON.find('home') != -1: if os.path.isdir('/var/cpanel'): if invalid_homes_exist(): print_cpanel_home_warning() for o, a in opts: if o in ('--mount-skel',): check_skeleton() remove_nested_skeleton() mount_skeleton() sys.exit(0) elif o in ('--unmount-skel',): check_skeleton() umount_skeleton() # cagefs_fuse('stop') sys.exit(0) elif o in ("--debug",): cagefslib.debug_option = True elif o in ("--profiling",): config['profiling'] = 1 build_jail_flag = True elif o in ("--do-not-ask",): do_not_ask_option = True elif o in ("-w", "--unmount"): check_save_dir() config['unmount'] = 1 manage_user_flag = True elif o in ("--enable-cagefs",): old_enabled_users = get_enabled_users() enable_cagefs() enable_cagefs_for_users_with_duplicate_uids() update_users_status(old_enabled_users=old_enabled_users) check_skeleton() remove_nested_skeleton() if etcfs_is_disabled(): update_etc(config) # Remount skeleton and all users mount_skeleton(True) sys.exit(0) elif o in ('--enable',): check_save_dir() check_skeleton() remove_nested_skeleton() config['enable'] = 1 manage_user_flag = True elif o in ('--disable',): check_save_dir() config['disable'] = 1 manage_user_flag = True elif o in ('--remove-all',): remove_all() sys.exit(0) elif o in ('-M', '--remount-all'): check_save_dir() check_skeleton() remove_nested_skeleton() # Remount skeleton and all users mount_skeleton(True) sys.exit(0) elif o in ("-m", "--remount"): check_save_dir() check_skeleton() remove_nested_skeleton() config['remount'] = 1 manage_user_flag = True elif o in ('--enable-all',): old_enabled_users = get_enabled_users() check_skeleton() remove_nested_skeleton() set_user_mode(True) update_users_status(old_enabled_users=old_enabled_users) if etcfs_is_disabled(): update_etc(config) # Remount skeleton and all users mount_skeleton(True) sys.exit(0) elif o in ('--disable-all',): old_enabled_users = get_enabled_users() set_user_mode(False) update_users_status(old_enabled_users=old_enabled_users) remount_all() sys.exit(0) elif o in ('--toggle-mode',): toggle_mode() sys.exit(0) elif o in ("-f", "--force"): config['force'] = 1 build_jail_flag = True check_skeleton() elif o in ("-u", "--update"): config['update'] = 1 build_jail_flag = True check_skeleton() elif o in ("-i", "--init"): if os.path.isdir(SKELETON+'/bin'): secureio.logging('Error : directory %s already exists.\nUse "%s --reinit" if you want to reinitialize CageFS' % (SKELETON, sys.argv[0])) sys.exit(1) config['init'] = 1 build_jail_flag = True user_mode = get_user_mode() # mode is not set yet and CageFS is enabled ? if (user_mode == 'Error' or user_mode == 'Not Initialized') and (not save_dir_exists()): # set mode to "Disable All" set_user_mode(False) elif o in ("-r", "--reinit"): config['reinit'] = 1 build_jail_flag = True check_skeleton() elif o in ("-k", "--hardlink"): config['hardlink'] = 1 build_jail_flag = True elif o in ("--create-mp",): create_mp(True) sys.exit(0) elif o in ("--addrpm",): for rpm in args: addrpm(rpm) sys.exit(0) elif o in ("--delrpm",): for rpm in args: delrpm(rpm) sys.exit(0) elif o in ("--list-rpm",): list_rpm() sys.exit(0) elif o in ('--update-list',): update_list(config) sys.exit(0) elif o in ("--update-etc", '--force-update-etc'): config['force-update-etc'] = (o == '--force-update-etc') users = get_users_from_args(args) if users: update_etc_only(config, users = users) else: update_etc_only(config) sys.exit(0) elif o in ("--setup-cl-selector", "--setup-cl-alt"): setup_cl_alt(config) sys.exit(0) elif o in ("--apply-global-php-ini",): # alt-php versions are installed ? if cagefslib.get_alt_versions(): cagefsreconfigure.replace_alt_settings(options=args) sys.exit(0) elif o in ('--create-namespace',): # for USER username_list = _get_username_list_from_args(args) sys.exit(int(create_namespaces(username_list, do_mount_skel=True))) elif o in ('--create-namespaces',): # For all users sys.exit(int(create_namespaces(do_mount_skel=True))) elif o in ('--delete-namespace',): username_list = _get_username_list_from_args(args) sys.exit(int(delete_namespaces(username_list))) elif o in ('--delete-namespaces',): sys.exit(int(delete_namespaces())) elif o in ('--clean-without-lve-environment',): sys.exit(clean_without_lve_environment()) if manage_user_flag and build_jail_flag: usage() print("\nError: incompatible options specified") sys.exit(1) if manage_user_flag: if (len(args)==0): print() print('aborted, no username specified') sys.exit(2) if config['unmount'] != 1 and not is_running_without_lve(): # CAG-770: unshare mount namespace, so mounting/unmounting cagefs-skeleton will not affect all system unshare.unshare(unshare.CLONE_NEWNS) try: if (config['unmount'] == 1): error = False for username in args: error = unmount_user(username) or error sys.exit(int(error)) elif (config['disable'] == 1): old_enabled_users = get_enabled_users() usernames = [] for username in args: if user_exists(username): toggle_user(username, False) usernames.append(username) else: secureio.print_error('user '+username+' does not exist') update_users_status(users=usernames, status=False, old_enabled_users=old_enabled_users) if is_running_without_lve(): delete_namespaces(usernames) else: remount(usernames) elif (config['remount'] == 1) or (config['enable'] == 1): old_enabled_users = get_enabled_users() mount_skeleton() usernames = [] for username in args: if user_exists(username): if username not in ex_list: if (config['enable'] == 1): toggle_user(username, True) if etcfs_is_disabled(): update_etc(config, [username], False) usernames.append(username) elif config['enable'] == 1: secureio.print_error('user '+username+' is excluded') else: secureio.print_error('user '+username+' does not exist') if config['enable'] == 1: update_users_status(users=usernames, status=True, old_enabled_users=old_enabled_users) remount(usernames) else: secureio.print_error('No options specified. Nothing to do...') sys.exit(2) except KeyboardInterrupt: print() print('aborted.. ') sys.exit(1) elif build_jail_flag: jail = SKELETON if (config['dont-clean'] and (config['init'] or config['reinit'])): secureio.print_error('cannot specify --dont-clean with --init or --reinit options') sys.exit(1) count_of_modes = config['init'] + config['reinit'] + config['update'] + config['force'] if config['profiling'] == 0: if count_of_modes != 1: secureio.print_error('you should specify one of the --init, --reinit, --update or --force options\n') sys.exit(1) if config['reinit'] == 1: users = get_mounted_users() users_count = len(users) if users_count != 0: print('WARNING: ', users_count, 'CageFS currently mounted.') print('If you proceed, CageFS will be temporarily disabled and unmounted.') confirm("Do you want to continue (yes/no)? ") if cagefs_is_enabled(): config['cagefs_was_enabled'] = 1 disable_cagefs() # update ISP manager user wrappers update_users_status(disable_all=True) unmount_all(remount_users=True, check_busy=False) try: jbuf = os.lstat(jail+'.old') if stat.S_ISDIR(jbuf[stat.ST_MODE]): shutil.rmtree(jail+'.old') else: os.unlink(jail+'.old') except (OSError, IOError, shutil.Error): pass try: os.rename(jail, jail+'.old') except (OSError, IOError): pass try: mod_makedirs(jail, 0o755) except (OSError, IOError): pass if config['profiling'] == 1: config_copy = config # one of the init, reinit, update or force options is specified ? if count_of_modes == 1: import profile profile.run('cagefsctl.do_profiling()', LIBDIR+'/profiling.log') import pstats p = pstats.Stats(LIBDIR+'/profiling.log') print() print('--------------------------------------') print('Cumulative time:') p.sort_stats('cumulative').print_stats(20) print() print('--------------------------------------') print('Total time:') p.sort_stats('time').print_stats(20) else: update_cagefs(config) if config['init'] or config['reinit']: config['init'] = config['reinit'] = 0 setup_cl_alt(config) else: usage() sys.exit(1) if __name__ == "__main__": main()