From b8c079c770d3eaa4de49e997d42e1501c28a153b Mon Sep 17 00:00:00 2001 From: progier389 Date: Mon, 8 Jul 2024 11:19:09 +0200 Subject: [PATCH] Issue 6155 - ldap-agent fails to start because of permission error (#6179) Issue: dirsrv-snmp service fails to starts when SELinux is enforced because of AVC preventing to open some files One workaround is to use the dac_override capability but it is a bad practice. Fix: Setting proper permissions: Running ldap-agent with uid=root and gid=dirsrv to be able to access both snmp and dirsrv resources. Setting read permission on the group for the dse.ldif file Setting r/w permissions on the group for the snmp semaphore and mmap file For that one special care is needed because ns-slapd umask overrides the file creation permission as is better to avoid changing the umask (changing umask within the code is not thread safe, and the current 0022 umask value is correct for most of the files) so the safest way is to chmod the snmp file if the needed permission are not set. Issue: #6155 Reviewed by: @droideck , @vashirov (Thanks ! ) (cherry picked from commit eb7e57d77b557b63c65fdf38f9069893b021f049) --- .github/scripts/generate_matrix.py | 4 +- dirsrvtests/tests/suites/snmp/snmp.py | 214 ++++++++++++++++++++++++++ ldap/servers/slapd/agtmmap.c | 72 ++++++++- ldap/servers/slapd/agtmmap.h | 13 ++ ldap/servers/slapd/dse.c | 6 +- ldap/servers/slapd/slap.h | 6 + ldap/servers/slapd/snmp_collator.c | 4 +- src/lib389/lib389/instance/setup.py | 5 + wrappers/systemd-snmp.service.in | 1 + 9 files changed, 313 insertions(+), 12 deletions(-) create mode 100644 dirsrvtests/tests/suites/snmp/snmp.py diff --git a/.github/scripts/generate_matrix.py b/.github/scripts/generate_matrix.py index 584374597..8d67a1dc7 100644 --- a/.github/scripts/generate_matrix.py +++ b/.github/scripts/generate_matrix.py @@ -21,8 +21,8 @@ else: # Use tests from the source suites = next(os.walk('dirsrvtests/tests/suites/'))[1] - # Filter out snmp as it is an empty directory: - suites.remove('snmp') + # Filter out webui because of broken tests + suites.remove('webui') # Run each replication test module separately to speed things up suites.remove('replication') diff --git a/dirsrvtests/tests/suites/snmp/snmp.py b/dirsrvtests/tests/suites/snmp/snmp.py new file mode 100644 index 000000000..0952deb40 --- /dev/null +++ b/dirsrvtests/tests/suites/snmp/snmp.py @@ -0,0 +1,214 @@ +# --- BEGIN COPYRIGHT BLOCK --- +# Copyright (C) 2024 Red Hat, Inc. +# All rights reserved. +# +# License: GPL (version 3 or any later version). +# See LICENSE for details. +# --- END COPYRIGHT BLOCK --- +# +import os +import pytest +import logging +import subprocess +import ldap +from datetime import datetime +from shutil import copyfile +from lib389.topologies import topology_m2 as topo_m2 +from lib389.utils import selinux_present + + +DEBUGGING = os.getenv("DEBUGGING", default=False) +if DEBUGGING: + logging.getLogger(__name__).setLevel(logging.DEBUG) +else: + logging.getLogger(__name__).setLevel(logging.INFO) +log = logging.getLogger(__name__) + + +SNMP_USER = 'user_name' +SNMP_PASSWORD = 'authentication_password' +SNMP_PRIVATE = 'private_password' + +# LDAP OID in MIB +LDAP_OID = '.1.3.6.1.4.1.2312.6.1.1' +LDAPCONNECTIONS_OID = f'{LDAP_OID}.21' + + +def run_cmd(cmd, check_returncode=True): + """Run a command""" + + log.info(f'Run: {cmd}') + result = subprocess.run(cmd, capture_output=True, universal_newlines=True) + log.info(f'STDOUT of {cmd} is:\n{result.stdout}') + log.info(f'STDERR of {cmd} is:\n{result.stderr}') + if check_returncode: + result.check_returncode() + return result + + +def add_lines(lines, filename): + """Add lines that are not already present at the end of a file""" + + log.info(f'add_lines({lines}, {filename})') + try: + with open(filename, 'r') as fd: + for line in fd: + try: + lines.remove(line.strip()) + except ValueError: + pass + except FileNotFoundError: + pass + if lines: + with open(filename, 'a') as fd: + for line in lines: + fd.write(f'{line}\n') + + +def remove_lines(lines, filename): + """Remove lines in a file""" + + log.info(f'remove_lines({lines}, {filename})') + file_lines = [] + with open(filename, 'r') as fd: + for line in fd: + if not line.strip() in lines: + file_lines.append(line) + with open(filename, 'w') as fd: + for line in file_lines: + fd.write(line) + + +@pytest.fixture(scope="module") +def setup_snmp(topo_m2, request): + """Install snmp and configure it + + Returns the time just before dirsrv-snmp get restarted + """ + + inst1 = topo_m2.ms["supplier1"] + inst2 = topo_m2.ms["supplier2"] + + # Check for the test prerequisites + if os.getuid() != 0: + pytest.skip('This test should be run by root superuser') + return None + if not inst1.with_systemd_running(): + pytest.skip('This test requires systemd') + return None + required_packages = { + '389-ds-base-snmp': os.path.join(inst1.get_sbin_dir(), 'ldap-agent'), + 'net-snmp': '/etc/snmp/snmpd.conf', } + skip_msg = "" + for package,file in required_packages.items(): + if not os.path.exists(file): + skip_msg += f"Package {package} is not installed ({file} is missing).\n" + if skip_msg != "": + pytest.skip(f'This test requires the following package(s): {skip_msg}') + return None + + # Install snmp + # run_cmd(['/usr/bin/dnf', 'install', '-y', 'net-snmp', 'net-snmp-utils', '389-ds-base-snmp']) + + # Prepare the lines to add/remove in files: + # master agentx + # snmp user (user_name - authentication_password - private_password) + # ldap_agent ds instances + # + # Adding rwuser and createUser lines is the same as running: + # net-snmp-create-v3-user -A authentication_password -a SHA -X private_password -x AES user_name + # but has the advantage of removing the user at cleanup phase + # + agent_cfg = '/etc/dirsrv/config/ldap-agent.conf' + lines_dict = { '/etc/snmp/snmpd.conf' : ['master agentx', f'rwuser {SNMP_USER}'], + '/var/lib/net-snmp/snmpd.conf' : [ + f'createUser {SNMP_USER} SHA "{SNMP_PASSWORD}" AES "{SNMP_PRIVATE}"',], + agent_cfg : [] } + for inst in topo_m2: + lines_dict[agent_cfg].append(f'server slapd-{inst.serverid}') + + # Prepare the cleanup + def fin(): + run_cmd(['systemctl', 'stop', 'dirsrv-snmp']) + if not DEBUGGING: + run_cmd(['systemctl', 'stop', 'snmpd']) + try: + os.remove('/usr/share/snmp/mibs/redhat-directory.mib') + except FileNotFoundError: + pass + for filename,lines in lines_dict.items(): + remove_lines(lines, filename) + run_cmd(['systemctl', 'start', 'snmpd']) + + request.addfinalizer(fin) + + # Copy RHDS MIB in default MIB search path (Ugly because I have not found how to change the search path) + copyfile('/usr/share/dirsrv/mibs/redhat-directory.mib', '/usr/share/snmp/mibs/redhat-directory.mib') + + run_cmd(['systemctl', 'stop', 'snmpd']) + for filename,lines in lines_dict.items(): + add_lines(lines, filename) + + run_cmd(['systemctl', 'start', 'snmpd']) + + curtime = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + + run_cmd(['systemctl', 'start', 'dirsrv-snmp']) + return curtime + + +@pytest.mark.skipif(not os.path.exists('/usr/bin/snmpwalk'), reason="net-snmp-utils package is not installed") +def test_snmpwalk(topo_m2, setup_snmp): + """snmp smoke tests. + + :id: e5d29998-1c21-11ef-a654-482ae39447e5 + :setup: Two suppliers replication setup, snmp + :steps: + 1. use snmpwalk to display LDAP statistics + 2. use snmpwalk to get the number of open connections + :expectedresults: + 1. Success and no messages in stderr + 2. The number of open connections should be positive + """ + + inst1 = topo_m2.ms["supplier1"] + inst2 = topo_m2.ms["supplier2"] + + + cmd = [ '/usr/bin/snmpwalk', '-v3', '-u', SNMP_USER, '-l', 'AuthPriv', + '-m', '+RHDS-MIB', '-A', SNMP_PASSWORD, '-a', 'SHA', + '-X', SNMP_PRIVATE, '-x', 'AES', 'localhost', + LDAP_OID ] + result = run_cmd(cmd) + assert not result.stderr + + cmd = [ '/usr/bin/snmpwalk', '-v3', '-u', SNMP_USER, '-l', 'AuthPriv', + '-m', '+RHDS-MIB', '-A', SNMP_PASSWORD, '-a', 'SHA', + '-X', SNMP_PRIVATE, '-x', 'AES', 'localhost', + f'{LDAPCONNECTIONS_OID}.{inst1.port}', '-Ov' ] + result = run_cmd(cmd) + nbconns = int(result.stdout.split()[1]) + log.info(f'There are {nbconns} open connections on {inst1.serverid}') + assert nbconns > 0 + + +@pytest.mark.skipif(not selinux_present(), reason="SELinux is not enabled") +def test_snmp_avc(topo_m2, setup_snmp): + """snmp smoke tests. + + :id: fb79728e-1d0d-11ef-9213-482ae39447e5 + :setup: Two suppliers replication setup, snmp + :steps: + 1. Get the system journal about ldap-agent + :expectedresults: + 1. No AVC should be present + """ + result = run_cmd(['journalctl', '-S', setup_snmp, '-g', 'ldap-agent']) + assert not 'AVC' in result.stdout + + +if __name__ == '__main__': + # Run isolated + # -s for DEBUG mode + CURRENT_FILE = os.path.realpath(__file__) + pytest.main("-s %s" % CURRENT_FILE) diff --git a/ldap/servers/slapd/agtmmap.c b/ldap/servers/slapd/agtmmap.c index bc5fe1ee1..4dc67dcfb 100644 --- a/ldap/servers/slapd/agtmmap.c +++ b/ldap/servers/slapd/agtmmap.c @@ -34,6 +34,70 @@ agt_mmap_context_t mmap_tbl[2] = {{AGT_MAP_UNINIT, -1, (caddr_t)-1}, {AGT_MAP_UNINIT, -1, (caddr_t)-1}}; +#define CHECK_MAP_FAILURE(addr) ((addr)==NULL || (addr) == (caddr_t) -1) + + +/**************************************************************************** + * + * agt_set_fmode () - try to increase file mode if some flags are missing. + * + * + * Inputs: + * fd -> The file descriptor. + * + * mode -> the wanted mode + * + * Outputs: None + * Return Values: None + * + ****************************************************************************/ +static void +agt_set_fmode(int fd, mode_t mode) +{ + /* ns-slapd umask is 0022 which is usually fine. + * but ldap-agen needs S_IWGRP permission on snmp semaphore and mmap file + * ( when SELinux is enforced process with uid=0 does not bypass the file permission + * (unless the unfamous dac_override capability is set) + * Changing umask could lead to race conditions so it is better to check the + * file permission and change them if needed and if the process own the file. + */ + struct stat fileinfo = {0}; + if (fstat(fd, &fileinfo) == 0 && fileinfo.st_uid == getuid() && + (fileinfo.st_mode & mode) != mode) { + (void) fchmod(fd, fileinfo.st_mode | mode); + } +} + +/**************************************************************************** + * + * agt_sem_open () - Like sem_open but ignores umask + * + * + * Inputs: see sem_open man page. + * Outputs: see sem_open man page. + * Return Values: see sem_open man page. + * + ****************************************************************************/ +sem_t * +agt_sem_open(const char *name, int oflag, mode_t mode, unsigned int value) +{ + sem_t *sem = sem_open(name, oflag, mode, value); + char *semname = NULL; + + if (sem != NULL) { + if (asprintf(&semname, "/dev/shm/sem.%s", name+1) > 0) { + int fd = open(semname, O_RDONLY); + if (fd >= 0) { + agt_set_fmode(fd, mode); + (void) close(fd); + } + free(semname); + semname = NULL; + } + } + return sem; +} + /**************************************************************************** * * agt_mopen_stats () - open and Memory Map the stats file. agt_mclose_stats() @@ -52,7 +116,6 @@ agt_mmap_context_t mmap_tbl[2] = {{AGT_MAP_UNINIT, -1, (caddr_t)-1}, * as defined in , otherwise. * ****************************************************************************/ - int agt_mopen_stats(char *statsfile, int mode, int *hdl) { @@ -64,6 +127,7 @@ agt_mopen_stats(char *statsfile, int mode, int *hdl) int err; size_t sz; struct stat fileinfo; + mode_t rw_mode = S_IWUSR | S_IRUSR | S_IRGRP | S_IWGRP | S_IROTH; switch (mode) { case O_RDONLY: @@ -128,10 +192,7 @@ agt_mopen_stats(char *statsfile, int mode, int *hdl) break; case O_RDWR: - fd = open(path, - O_RDWR | O_CREAT, - S_IWUSR | S_IRUSR | S_IRGRP | S_IROTH); - + fd = open(path, O_RDWR | O_CREAT, rw_mode); if (fd < 0) { err = errno; #if (0) @@ -140,6 +201,7 @@ agt_mopen_stats(char *statsfile, int mode, int *hdl) rc = err; goto bail; } + agt_set_fmode(fd, rw_mode); if (fstat(fd, &fileinfo) != 0) { close(fd); diff --git a/ldap/servers/slapd/agtmmap.h b/ldap/servers/slapd/agtmmap.h index fb27ab2c4..99a8584a3 100644 --- a/ldap/servers/slapd/agtmmap.h +++ b/ldap/servers/slapd/agtmmap.h @@ -28,6 +28,7 @@ #include #include #include +#include #include #include "nspr.h" @@ -188,6 +189,18 @@ int agt_mclose_stats(int hdl); int agt_mread_stats(int hdl, struct hdr_stats_t *, struct ops_stats_t *, struct entries_stats_t *); +/**************************************************************************** + * + * agt_sem_open () - Like sem_open but ignores umask + * + * + * Inputs: see sem_open man page. + * Outputs: see sem_open man page. + * Return Values: see sem_open man page. + * + ****************************************************************************/ +sem_t *agt_sem_open(const char *name, int oflag, mode_t mode, unsigned int value); + #ifdef __cplusplus } #endif diff --git a/ldap/servers/slapd/dse.c b/ldap/servers/slapd/dse.c index b04fafde6..f1e48c6b1 100644 --- a/ldap/servers/slapd/dse.c +++ b/ldap/servers/slapd/dse.c @@ -683,7 +683,7 @@ dse_read_one_file(struct dse *pdse, const char *filename, Slapi_PBlock *pb, int "The configuration file %s could not be accessed, error %d\n", filename, rc); rc = 0; /* Fail */ - } else if ((prfd = PR_Open(filename, PR_RDONLY, SLAPD_DEFAULT_FILE_MODE)) == NULL) { + } else if ((prfd = PR_Open(filename, PR_RDONLY, SLAPD_DEFAULT_DSE_FILE_MODE)) == NULL) { slapi_log_err(SLAPI_LOG_ERR, "dse_read_one_file", "The configuration file %s could not be read. " SLAPI_COMPONENT_NAME_NSPR " %d (%s)\n", filename, @@ -871,7 +871,7 @@ dse_rw_permission_to_one_file(const char *name, int loglevel) PRFileDesc *prfd; prfd = PR_Open(name, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, - SLAPD_DEFAULT_FILE_MODE); + SLAPD_DEFAULT_DSE_FILE_MODE); if (NULL == prfd) { prerr = PR_GetError(); accesstype = "create"; @@ -970,7 +970,7 @@ dse_write_file_nolock(struct dse *pdse) fpw.fpw_prfd = NULL; if (NULL != pdse->dse_filename) { - if ((fpw.fpw_prfd = PR_Open(pdse->dse_tmpfile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, SLAPD_DEFAULT_FILE_MODE)) == NULL) { + if ((fpw.fpw_prfd = PR_Open(pdse->dse_tmpfile, PR_RDWR | PR_CREATE_FILE | PR_TRUNCATE, SLAPD_DEFAULT_DSE_FILE_MODE)) == NULL) { rc = PR_GetOSError(); slapi_log_err(SLAPI_LOG_ERR, "dse_write_file_nolock", "Cannot open " "temporary DSE file \"%s\" for update: OS error %d (%s)\n", diff --git a/ldap/servers/slapd/slap.h b/ldap/servers/slapd/slap.h index 469874fd1..927576b70 100644 --- a/ldap/servers/slapd/slap.h +++ b/ldap/servers/slapd/slap.h @@ -238,6 +238,12 @@ typedef void (*VFPV)(); /* takes undefined arguments */ */ #define SLAPD_DEFAULT_FILE_MODE S_IRUSR | S_IWUSR +/* ldap_agent run as uid=root gid=dirsrv and requires S_IRGRP | S_IWGRP + * on semaphore and mmap file if SELinux is enforced. + */ +#define SLAPD_DEFAULT_SNMP_FILE_MODE S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP +/* ldap_agent run as uid=root gid=dirsrv and requires S_IRGRP on dse.ldif if SELinux is enforced. */ +#define SLAPD_DEFAULT_DSE_FILE_MODE S_IRUSR | S_IWUSR | S_IRGRP #define SLAPD_DEFAULT_DIR_MODE S_IRWXU #define SLAPD_DEFAULT_IDLE_TIMEOUT 3600 /* seconds - 0 == never */ #define SLAPD_DEFAULT_IDLE_TIMEOUT_STR "3600" diff --git a/ldap/servers/slapd/snmp_collator.c b/ldap/servers/slapd/snmp_collator.c index c998d4262..bd7020585 100644 --- a/ldap/servers/slapd/snmp_collator.c +++ b/ldap/servers/slapd/snmp_collator.c @@ -474,7 +474,7 @@ static void snmp_collator_create_semaphore(void) { /* First just try to create the semaphore. This should usually just work. */ - if ((stats_sem = sem_open(stats_sem_name, O_CREAT | O_EXCL, SLAPD_DEFAULT_FILE_MODE, 1)) == SEM_FAILED) { + if ((stats_sem = agt_sem_open(stats_sem_name, O_CREAT | O_EXCL, SLAPD_DEFAULT_SNMP_FILE_MODE, 1)) == SEM_FAILED) { if (errno == EEXIST) { /* It appears that we didn't exit cleanly last time and left the semaphore * around. Recreate it since we don't know what state it is in. */ @@ -486,7 +486,7 @@ snmp_collator_create_semaphore(void) exit(1); } - if ((stats_sem = sem_open(stats_sem_name, O_CREAT | O_EXCL, SLAPD_DEFAULT_FILE_MODE, 1)) == SEM_FAILED) { + if ((stats_sem = agt_sem_open(stats_sem_name, O_CREAT | O_EXCL, SLAPD_DEFAULT_SNMP_FILE_MODE, 1)) == SEM_FAILED) { /* No dice */ slapi_log_err(SLAPI_LOG_EMERG, "snmp_collator_create_semaphore", "Failed to create semaphore for stats file (/dev/shm/sem.%s). Error %d (%s).\n", diff --git a/src/lib389/lib389/instance/setup.py b/src/lib389/lib389/instance/setup.py index 036664447..fca03383e 100644 --- a/src/lib389/lib389/instance/setup.py +++ b/src/lib389/lib389/instance/setup.py @@ -10,6 +10,7 @@ import os import sys import shutil +import stat import pwd import grp import re @@ -773,6 +774,10 @@ class SetupDs(object): ldapi_autobind="on", ) file_dse.write(dse_fmt) + # Set minimum permission required by snmp ldap-agent + status = os.fstat(file_dse.fileno()) + os.fchmod(file_dse.fileno(), status.st_mode | stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP) + os.chown(os.path.join(slapd['config_dir'], 'dse.ldif'), slapd['user_uid'], slapd['group_gid']) self.log.info("Create file system structures ...") # Create all the needed paths diff --git a/wrappers/systemd-snmp.service.in b/wrappers/systemd-snmp.service.in index f18766cb4..d344367a0 100644 --- a/wrappers/systemd-snmp.service.in +++ b/wrappers/systemd-snmp.service.in @@ -9,6 +9,7 @@ After=network.target [Service] Type=forking +Group=@defaultgroup@ PIDFile=/run/dirsrv/ldap-agent.pid ExecStart=@sbindir@/ldap-agent @configdir@/ldap-agent.conf -- 2.49.0