[Unit] Description=Imunify360 resident Before=cagefs.service After=network.target iptables.service firewalld.service systemd-modules-load.service Wants=ossec-hids.service imunify360-php-daemon.service imunify-realtime-av.service imunify-notifier.socket # Service will NOT start if this file exists ConditionPathExists=!/var/lib/rpm-state/imunify360-transaction-in-progress [Service] CPUAccounting=true MemoryAccounting=true BlockIOAccounting=true Slice=Imunify-agent.slice Environment=PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION=upb Environment=LANG=en_US.UTF-8 Environment=LC_ALL=en_US.UTF-8 Environment=PYTHONNOUSERSITE=1 Environment=IMUNIFY360_LOGGING_PREFIX=r. Environment=SQLITE_TMPDIR=/var/imunify360/tmp Environment=FGW_FS_BASE_DIR=/var/imunify360/gw.dir Environment=FGW_FS_MAX_CONCURRENT_QUEUES=100 # NATS embedded server for cross-component messaging (DEF-39879) Environment=I360_NATS_ENABLED=true Environment=I360_NATS_STORE_DIR=/var/imunify360/nats Environment=I360_NATS_PORT=44222 Environment=I360_NATS_TOKEN_PATH=/var/run/imunify360/nats.token # Manage /var/run/imunify360/ manually instead of using systemd's RuntimeDirectory=. # RuntimeDirectoryPreserve=yes was added in systemd v235 and is silently ignored on # systemd v219 (CloudLinux 7); without preserve, RuntimeDirectory= would delete the # directory on service stop, wiping wafd_imunify_daemon's libiplists-daemon.sock and # breaking webshield until wafd is manually restarted (DEF-41462). Type=notify ExecStartPre=/bin/mkdir -p /var/run/imunify360 ExecStartPre=/bin/chmod 0755 /var/run/imunify360 ExecStartPre=/usr/share/imunify360/scripts/set-service-resources.sh imunify360.service 50 50 ExecStart=/usr/bin/imunify-service ExecStartPost=/bin/bash -c "echo $MAINPID > /var/run/imunify360.pid" ExecStartPost=/bin/systemctl restart imunify360-resource-unlock@imunify360.timer PIDFile=/var/run/imunify360.pid #TODO: must be not less than defence360agent/cli/server.py:stop(seconds=8) TimeoutStartSec=900 TimeoutStopSec=60 Restart=on-failure RestartSec=5 StartLimitInterval=600s StartLimitBurst=5 # Don't send SIGTERM on service stop to remaining processes in cgroup # (SIGKILL on timeout is still sent) KillMode=mixed NoNewPrivileges=true CapabilityBoundingSet=CAP_BPF CAP_CHOWN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_KILL CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_PERFMON CAP_SETGID CAP_SETUID CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SYS_RESOURCE # AmbientCapabilities= covers caps subprocesses need as effective. # NoNewPrivileges=true disables the kernel's "raise effective from # permitted on UID-0 exec" path, so without Ambient children run with # effective=empty and fail with EPERM. Why each non-obvious cap is here: # - CAP_NET_RAW: iptables-1.8.5's xt_set extension opens # AF_INET SOCK_RAW IPPROTO_RAW (libxt_set.c:get_version) to probe # ipset; EPERM here is reported as "Can't open socket to ipset". # - CAP_BPF / CAP_PERFMON / CAP_SYS_ADMIN: the agent's Go firewall # stack creates a BPF map (nats_port) via bpf(BPF_MAP_CREATE); # fails with "operation not permitted" without these. # - CAP_SYS_RESOURCE: cagefsctl opens /proc/lve/list to probe the # CloudLinux LVE kernel module before any other operation; the # LVE handler gates that open on CAP_SYS_RESOURCE, and EPERM # there is reported as "Error: current running kernel is NOT # supported" (misleading — the kernel IS supported, the caller # just lacks the cap). Verified by strace + bisect on CL9 + CSF. # - The rest are baseline daemon caps: CHOWN/FOWNER/SETUID/SETGID for # managing per-user file ownership during malware fixes; # DAC_OVERRIDE / DAC_READ_SEARCH for traversing system dirs; # SYS_PTRACE for the agent's own diagnostic helpers. AmbientCapabilities=CAP_BPF CAP_CHOWN CAP_DAC_OVERRIDE CAP_DAC_READ_SEARCH CAP_FOWNER CAP_KILL CAP_NET_ADMIN CAP_NET_BIND_SERVICE CAP_NET_RAW CAP_PERFMON CAP_SETGID CAP_SETUID CAP_SYS_ADMIN CAP_SYS_PTRACE CAP_SYS_RESOURCE # ProtectSystem=true keeps /usr, /boot, /efi read-only. We previously # tried =full (which also locks /etc) but every concrete failure in # this MR's review came from panel-provided tools that the agent # invokes as subprocesses (whmapi, custombuild, server_pref, # rebuildhttpdconf, ie_config, cagefsctl, …) needing to write # somewhere under /etc/. The allowlist for those grew to ~20 entries # and was still racy on fresh installs (companion packages create # their /etc/ dirs after the unit starts; `-` prefix entries are # evaluated at start time and silently skipped). =true gets us out # of the wack-a-mole and still delivers the MR's stated security # goal — preventing privilege escalation — via NoNewPrivileges=true # and CapabilityBoundingSet=. The /etc/* ReadWritePaths= entries # below are now redundant under =true; kept for documentation and # resilience if =true is ever reverted to =full. ProtectSystem=true ReadWritePaths=/etc/sysconfig/imunify360 ReadWritePaths=/etc/imunify360 ReadWritePaths=/etc/imunify-agent-proxy ReadWritePaths=/etc/cron.d # Optional ('-' prefix): wafd/webshield ship in companion packages and # may not be present at service start (e.g. fresh install); panel # integration dirs only exist on specific distros. ReadWritePaths=-/etc/imunify360-wafd ReadWritePaths=-/etc/imunify360-webshield ReadWritePaths=-/etc/csf ReadWritePaths=-/etc/httpd/conf.d ReadWritePaths=-/etc/httpd/conf/plesk.conf.d ReadWritePaths=-/etc/httpd/conf/extra ReadWritePaths=-/etc/apache2/conf.d ReadWritePaths=-/etc/apache2/conf-enabled ReadWritePaths=-/etc/apache2/plesk.conf.d ReadWritePaths=-/etc/modsecurity.d ReadWritePaths=-/etc/yum.repos.d ReadWritePaths=-/etc/apt/sources.list.d # Additional dirs the agent rewrites at runtime, surfaced by build 523: # - /usr/share/i360-php-opts: proactive-defense PHP-immunity DB # (im360/subsys/proactive.py + i360-storage-replacehdb-v2 helper). # - /etc/httpd/conf/modsecurity.d: Plesk modsec rules (RBL whitelist, # malware-list .tmp tempfiles); sibling of plesk.conf.d, not a child. # - /usr/local/directadmin: DA modsec includes and per-user domain # files rewritten on each sync. ReadWritePaths=-/usr/share/i360-php-opts ReadWritePaths=-/etc/httpd/conf/modsecurity.d ReadWritePaths=-/usr/local/directadmin # Additional cPanel + CageFS dirs surfaced by build 535: # - /usr/local/cpanel: where the cPanel hook installer drops # ImunifyHook.pm. # - /etc/cagefs, /var/cagefs, /usr/share/cagefs, /usr/share/cagefs-skeleton: # rewritten by `cagefsctl --force-update-etc` which the agent's cagefs plugin # spawns; without these the subprocess hangs and the agent's # asyncio cancel surfaces as cascading test setup failures. # /usr/share/cagefs-skeleton is a SEPARATE top-level dir (not under # /usr/share/cagefs); cagefsctl's check_skeleton() does os.chmod() on it, # which raises EROFS under ProtectSystem=. On a CageFS host the resulting # cagefsctl failure makes migration 129_fixed_cagefs_unmount fall back to a # synchronous `systemctl restart cagefs`, which deadlocks against # Before=cagefs.service and hangs agent startup (DEF-47738). ReadWritePaths=-/usr/local/cpanel ReadWritePaths=-/etc/cagefs ReadWritePaths=-/var/cagefs ReadWritePaths=-/usr/share/cagefs ReadWritePaths=-/usr/share/cagefs-skeleton # Plesk plugin scripts dir (build 550): the agent installs/updates # /usr/local/psa/admin/plib/modules/imunify360/scripts/ on hooks. ReadWritePaths=-/usr/local/psa/admin/plib/modules/imunify360 # Plesk runtime state — notification log written by send-notifications.php # (/usr/local/psa/var/modules/imunify360/imunify360-local.log) and the # plesk-sendmail spool/tempfile dir. ProtectSystem=true bind-mounts /usr # read-only and CAP_DAC_OVERRIDE cannot bypass a mount-layer RO, so the # Plesk notification hook fails with EACCES without this entry. ReadWritePaths=-/usr/local/psa/var # Webuzo keeps Apache (and the modsec audit log) under /usr/local/apps; # the resident-agent's modsec sensor tails that log and persists its read # position to a .filetail.state file beside it. ProtectSystem=true # bind-mounts /usr read-only, so without this the state write fails with EROFS. ReadWritePaths=-/usr/local/apps # LiteSpeed keeps its config tree under /usr/local/lsws, including the # per-domain .d/modsec.conf files rewritten by the agent's # integration.sh rewrite-domain-configs. ProtectSystem=true makes /usr # read-only, so without this carve-out the per-domain modsec rewrite fails # with EROFS. ReadWritePaths=-/usr/local/lsws # PrivateTmp= deliberately not set. Tried =yes in v9.x and reverted: # the agent and several subsystems share /tmp with co-resident # processes — pytest fixtures touch /tmp/sample_enabled to enable # the Sample backup backend (rpm-tests/utils/backups.py), and the # realtime inotify malware scanner watches user-controlled paths # including /tmp on production hosts (PHP session/upload files). # A private /tmp namespace silently hides both. The security goal # of preventing /tmp data leaks is mostly carried by ProtectSystem= # and NoNewPrivileges= already. [Install] WantedBy=multi-user.target