File: //usr/lib/systemd/system/imunify360.service
[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 <auditlog>.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 <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
# CSF installs its post-hook script as /usr/local/csf/bin/csfpost.sh (preferred
# over /etc/csf/csfpost.sh when present); the ExportWBList plugin rewrites it
# on activation. ProtectSystem=true makes /usr read-only, so without this
# carve-out the rewrite fails with EROFS.
ReadWritePaths=-/usr/local/csf
# 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