Files
Cactidocumentation/Hardening-Cacti-Installation.md
Thomas Vincent 57b8088e13 docs: comprehensive validation audit, Spine poller overhaul, and technical refactor (#229)
* docs: comprehensive audit, Spine poller overhaul, and technical refactor

- Overhaul Spine documentation (Spine.md) with complete end-to-end guide: architecture, prerequisites for modern distros (EL8/9 CRB, Ubuntu 22/24, Debian 11/12, FreeBSD), SUID root permissions, secure spine.conf setup (0640), Web UI configuration (Paths, Poller type, Data Collectors tuning), MySQL max_connections sizing, and troubleshooting.
- Fix broken links and anchors (Hardening, SNMP data queries, Hook API references).
- Fix broken image reference in Sites.md (add-site.JPG -> add-site.png).
- Fix syntax errors in documentation PHP code blocks, including invalid parenthesization, missing SQL execute quotes, and malformed comments in FAQ.
- Fix DevOps installation commands: use config.php.dist, correct systemctl service names for MySQL/MariaDB/PHP-FPM, remove nonexistent php-json, add missing composer install, and correct Debian/Ubuntu systemd environment path.
- Correct grammatical errors, repeated words, and typos across all markdown documents.
- Remove obsolete unreferenced duplicate installation guides.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* docs: correct reviewed examples and guidance

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* docs: correct guidance that disagrees with the Cacti source

Verified each claim against include/global_arrays.php, lib/auth.php,
lib/functions.php, host_templates.php and the Spine config parser. The
notable ones: api_plugin_safe_exec() does not exist on either branch,
Spine reads DB_Pass rather than DB_Password, and the CDEF special data
source table was missing five entries and renamed four others.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* docs: wrap Spine prose and fence the connection sizing formula

The formula's bare asterisks rendered as emphasis, dropping the multiplication signs.

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

* chore: refresh copyright year in scripts and issue templates

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-09-08 09:41:21 -04:00

10 KiB

Hardening a Cacti Installation

This page covers OS-level hardening controls for a production Cacti host. The goal is defense in depth: even if a vulnerability in Cacti's PHP code is exploited, the attacker's reach should be limited.

Why These Controls Matter

A recurring class of vulnerability in Cacti involves writing an arbitrary file into the resource/ or scripts/ tree and then reaching that file over the web. The package-import advisory (GHSA-7cmj-g5qc-pj88, CVE-2024-25641) is a concrete example: a crafted package archive could write resource/test.php and execute it as a web request. The controls below close or limit that path at the OS and web-server layers, independent of the PHP fix.


1. File Permissions

Cacti writes to three paths at runtime. Everything else should be owned by root (or a non-web user) and not writable by the web process.

Path Purpose Recommended mode
rra/ RRDtool data files www-data:www-data 750
log/ Cacti log files www-data:www-data 750
cache/ Boost / poller cache www-data:www-data 750
resource/ XML templates root:root 755 (read-only for web)
scripts/ Data-source scripts root:root 755 (read-only for web)
lib/, include/ PHP libraries root:root 755

Apply once after installation:

# Writable paths (web user only)
chown -R www-data:www-data /var/www/html/cacti/rra \
                           /var/www/html/cacti/log \
                           /var/www/html/cacti/cache
chmod -R 750              /var/www/html/cacti/rra \
                           /var/www/html/cacti/log \
                           /var/www/html/cacti/cache

# Everything else read-only for the web user
chown -R root:root         /var/www/html/cacti
chmod -R 755               /var/www/html/cacti
# Re-apply writable exceptions after the recursive chmod
chown -R www-data:www-data /var/www/html/cacti/rra \
                           /var/www/html/cacti/log \
                           /var/www/html/cacti/cache

# Protect the database credentials in config.php.  The recursive 755 above
# leaves it world-readable; restrict it to root and the web user.
chown root:www-data /var/www/html/cacti/include/config.php
chmod 640           /var/www/html/cacti/include/config.php

Note: The scripts/ and resource/ paths may be written during template import or package import by a privileged CLI process running as root, not by the web process. If your workflow requires the web process to write there, use a strict code-review policy for any import and consider the compat profile instead.


2. Web Server — Block Direct Access to Scripts and Resources

PHP files inside scripts/ and resource/ must not be reachable via HTTP. These directories hold data-source scripts and XML templates; a web request to a .php file written there is the direct exploitation path.

Apache

<DirectoryMatch> is only valid in server config or <VirtualHost> context, not .htaccess. Add this inside your Cacti <VirtualHost>:

# Deny direct HTTP access to the script and resource trees.
# A PHP file written here via import cannot be reached over the web.
<DirectoryMatch "^/var/www/html/cacti/(scripts|resource)/">
    <FilesMatch "\.php$">
        Require all denied
    </FilesMatch>
</DirectoryMatch>

If you only have access to .htaccess (e.g. shared hosting), use a FilesMatch block inside each directory instead:

# scripts/.htaccess and resource/.htaccess
<FilesMatch "\.php$">
    Require all denied
</FilesMatch>

Or, more broadly, deny all non-XML/non-script files from direct access:

<Directory "/var/www/html/cacti/scripts">
    Require all denied
</Directory>

<Directory "/var/www/html/cacti/resource">
    <FilesMatch "\.php$">
        Require all denied
    </FilesMatch>
</Directory>

Nginx

Add inside your server {} block:

# Block PHP execution in scripts/ and resource/
location ~ ^/cacti/(scripts|resource)/.*\.php$ {
    return 403;
}

3. PHP-FPM Pool Hardening

PHP itself warns that php-fpm pools are not a true security boundary, but they provide useful knobs. Create a dedicated pool for Cacti (/etc/php/8.x/fpm/pool.d/cacti.conf):

[cacti]
user  = www-data
group = www-data

listen = /run/php/php-cacti.sock
listen.owner = www-data
listen.group = www-data

; Drop all inherited environment variables — pass only what Cacti needs.
clear_env = yes

; Only execute .php files — blocks .phtml, .php3, .phar, etc.
security.limit_extensions = .php

; Disable dangerous functions at the pool level (not overridable by ini_set).
; Cacti's web process shells out to RRDtool, fping and helper binaries via
; exec, shell_exec, system, popen and proc_open (see lib/rrd.php, lib/ping.php,
; lib/spikekill.php), so disabling those breaks graph rendering and diagnostics.
; Only passthru and pcntl_exec are unused by the web tier and safe to disable.
; For tighter control, constrain the process with open_basedir and a hardened
; PATH rather than removing the shell-exec family Cacti depends on.
php_admin_value[disable_functions] = passthru,pcntl_exec

; Restrict file operations to the Cacti tree.
; NOTE: open_basedir disables the PHP realpath cache.  Measure the
; performance impact before enabling in production.  It is an extra
; safety net, not a full security boundary (PHP documentation).
; php_admin_value[open_basedir] = /var/www/html/cacti:/tmp

; Suppress PHP version disclosure.
php_admin_flag[expose_php] = Off

Reload php-fpm after changes:

systemctl reload php8.x-fpm   # Debian/Ubuntu
systemctl reload php-fpm       # RHEL/Rocky/Alma

4. SELinux (RHEL / AlmaLinux / Rocky Linux)

SELinux ships enforcing on RHEL-family systems. These commands apply the correct file contexts so Apache can serve Cacti while limiting what the web process can write.

File Contexts

CACTI=/var/www/html/cacti

# Read-only web content (Apache can read, not write)
semanage fcontext -a -t httpd_sys_content_t      "${CACTI}(/.*)?"
semanage fcontext -a -t httpd_sys_content_t      "${CACTI}/scripts(/.*)?"
semanage fcontext -a -t httpd_sys_content_t      "${CACTI}/resource(/.*)?"

# Read-write content for runtime paths only
semanage fcontext -a -t httpd_sys_rw_content_t   "${CACTI}/rra(/.*)?"
semanage fcontext -a -t httpd_sys_rw_content_t   "${CACTI}/log(/.*)?"
semanage fcontext -a -t httpd_sys_rw_content_t   "${CACTI}/cache(/.*)?"

# Apply the contexts
restorecon -Rv "${CACTI}"

Database Network Access

If your database server is reached over TCP (not a local Unix socket), enable the SELinux boolean that permits it:

setsebool -P httpd_can_network_connect_db on

Poller Systemd Drop-in

The Cacti spine poller runs as a separate daemon and may need to connect outward for SNMP. Do not force it into the httpd_t domain: that confines spine to Apache's policy rather than freeing it, and outbound access in httpd_t is still gated by booleans. If SNMP polling is blocked by SELinux, enable the relevant boolean instead and leave the daemon in its own domain:

setsebool -P httpd_can_network_connect on

Run systemctl daemon-reload && systemctl restart cactid after changes.

Verify: After applying contexts, confirm Apache can still serve pages and the poller can poll. Use audit2why on any AVC denials in /var/log/audit/audit.log.


5. AppArmor (Debian / Ubuntu)

Ubuntu Server ships AppArmor enforcing by default. Create a profile for the Cacti php-fpm pool at /etc/apparmor.d/cacti-fpm:

#include <tunables/global>

profile cacti-fpm /usr/sbin/php-fpm* {
    #include <abstractions/base>
    #include <abstractions/php>
    #include <abstractions/mysql>

    # Cacti tree — read-only by default
    /var/www/html/cacti/           r,
    /var/www/html/cacti/**         r,

    # Allow execution of Cacti PHP files
    /var/www/html/cacti/*.php      r,
    /var/www/html/cacti/**/*.php   r,

    # Runtime write paths
    /var/www/html/cacti/rra/**     rw,
    /var/www/html/cacti/log/**     rw,
    /var/www/html/cacti/cache/**   rw,

    # Helper binaries Cacti shells out to.  AppArmor is default-deny once
    # file rules are present, so these need explicit execute (ix) or graphs
    # and polling break under enforce mode.
    /usr/bin/rrdtool               ix,
    /usr/bin/fping                 ix,
    /usr/bin/ping                  ix,
    /usr/bin/snmpget               ix,
    /usr/bin/snmpbulkwalk          ix,
    /usr/bin/php                   ix,

    # Temp files
    /tmp/**                        rw,
    /var/tmp/**                    rw,

    # Deny write to scripts/ and resource/
    deny /var/www/html/cacti/scripts/**  w,
    deny /var/www/html/cacti/resource/** w,
}

Load and enforce the profile:

apparmor_parser -r /etc/apparmor.d/cacti-fpm
aa-enforce /etc/apparmor.d/cacti-fpm

Attach the Profile to the php-fpm Service

Add a drop-in at /etc/systemd/system/php8.x-fpm.service.d/apparmor.conf:

[Service]
AppArmorProfile=cacti-fpm

Reload and restart:

systemctl daemon-reload
systemctl restart php8.x-fpm

Use complain mode (aa-complain) first to test without blocking, then switch to enforce once logs show no legitimate denials:

# Test mode
aa-complain /etc/apparmor.d/cacti-fpm

# Enforce when ready
aa-enforce  /etc/apparmor.d/cacti-fpm

6. Verification Checklist

After applying all controls, confirm:

  • curl -s http://localhost/cacti/scripts/ss_webseer.php returns 403
  • curl -s http://localhost/cacti/resource/ returns 403 for any .php inside
  • The Cacti web UI loads and graphs render (rra/ is writable)
  • The poller runs without errors (tail -f /var/www/html/cacti/log/cacti.log)
  • On RHEL: no AVC denials in ausearch -m avc -ts recent
  • On Debian/Ubuntu: no AppArmor denials in journalctl -k | grep apparmor

See Also


Copyright (c) 2004-2026 The Cacti Group