|
| 1 | +# Hardening a Cacti Installation |
| 2 | + |
| 3 | +This page covers OS-level hardening controls for a production Cacti host. |
| 4 | +The goal is defense in depth: even if a vulnerability in Cacti's PHP code |
| 5 | +is exploited, the attacker's reach should be limited. |
| 6 | + |
| 7 | +## Why These Controls Matter |
| 8 | + |
| 9 | +A recurring class of vulnerability in Cacti involves writing an arbitrary |
| 10 | +file into the `resource/` or `scripts/` tree and then reaching that file |
| 11 | +over the web. The package-import advisory (GHSA-jj7m-3x5c-9xvr) is a |
| 12 | +concrete example: a crafted package archive could write `resource/test.php` |
| 13 | +and execute it as a web request. The controls below close or limit that |
| 14 | +path at the OS and web-server layers, independent of the PHP fix. |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## 1. File Permissions |
| 19 | + |
| 20 | +Cacti writes to three paths at runtime. Everything else should be owned |
| 21 | +by root (or a non-web user) and not writable by the web process. |
| 22 | + |
| 23 | +| Path | Purpose | Recommended mode | |
| 24 | +|------|---------|-----------------| |
| 25 | +| `rra/` | RRDtool data files | `www-data:www-data 750` | |
| 26 | +| `log/` | Cacti log files | `www-data:www-data 750` | |
| 27 | +| `cache/` | Boost / poller cache | `www-data:www-data 750` | |
| 28 | +| `resource/` | XML templates | `root:root 755` (read-only for web) | |
| 29 | +| `scripts/` | Data-source scripts | `root:root 755` (read-only for web) | |
| 30 | +| `lib/`, `include/` | PHP libraries | `root:root 755` | |
| 31 | + |
| 32 | +Apply once after installation: |
| 33 | + |
| 34 | +```bash |
| 35 | +# Writable paths (web user only) |
| 36 | +chown -R www-data:www-data /var/www/html/cacti/rra \ |
| 37 | + /var/www/html/cacti/log \ |
| 38 | + /var/www/html/cacti/cache |
| 39 | +chmod -R 750 /var/www/html/cacti/rra \ |
| 40 | + /var/www/html/cacti/log \ |
| 41 | + /var/www/html/cacti/cache |
| 42 | + |
| 43 | +# Everything else read-only for the web user |
| 44 | +chown -R root:root /var/www/html/cacti |
| 45 | +chmod -R 755 /var/www/html/cacti |
| 46 | +# Re-apply writable exceptions after the recursive chmod |
| 47 | +chown -R www-data:www-data /var/www/html/cacti/rra \ |
| 48 | + /var/www/html/cacti/log \ |
| 49 | + /var/www/html/cacti/cache |
| 50 | +``` |
| 51 | + |
| 52 | +> **Note:** The `scripts/` and `resource/` paths may be written during |
| 53 | +> template import or package import by a privileged CLI process running |
| 54 | +> as root, not by the web process. If your workflow requires the web |
| 55 | +> process to write there, use a strict code-review policy for any import |
| 56 | +> and consider the compat profile instead. |
| 57 | +
|
| 58 | +--- |
| 59 | + |
| 60 | +## 2. Web Server — Block Direct Access to Scripts and Resources |
| 61 | + |
| 62 | +PHP files inside `scripts/` and `resource/` must not be reachable via HTTP. |
| 63 | +These directories hold data-source scripts and XML templates; a web request |
| 64 | +to a `.php` file written there is the direct exploitation path. |
| 65 | + |
| 66 | +### Apache |
| 67 | + |
| 68 | +Add inside your Cacti `<VirtualHost>` or `.htaccess`: |
| 69 | + |
| 70 | +```apache |
| 71 | +# Deny direct HTTP access to the script and resource trees. |
| 72 | +# A PHP file written here via import cannot be reached over the web. |
| 73 | +<DirectoryMatch "^/var/www/html/cacti/(scripts|resource)/"> |
| 74 | + <FilesMatch "\.php$"> |
| 75 | + Require all denied |
| 76 | + </FilesMatch> |
| 77 | +</DirectoryMatch> |
| 78 | +``` |
| 79 | + |
| 80 | +Or, more broadly, deny all non-XML/non-script files from direct access: |
| 81 | + |
| 82 | +```apache |
| 83 | +<Directory "/var/www/html/cacti/scripts"> |
| 84 | + Require all denied |
| 85 | +</Directory> |
| 86 | +
|
| 87 | +<Directory "/var/www/html/cacti/resource"> |
| 88 | + <FilesMatch "\.php$"> |
| 89 | + Require all denied |
| 90 | + </FilesMatch> |
| 91 | +</Directory> |
| 92 | +``` |
| 93 | + |
| 94 | +### Nginx |
| 95 | + |
| 96 | +Add inside your `server {}` block: |
| 97 | + |
| 98 | +```nginx |
| 99 | +# Block PHP execution in scripts/ and resource/ |
| 100 | +location ~ ^/cacti/(scripts|resource)/.*\.php$ { |
| 101 | + return 403; |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +--- |
| 106 | + |
| 107 | +## 3. PHP-FPM Pool Hardening |
| 108 | + |
| 109 | +PHP itself warns that php-fpm pools are not a true security boundary, but |
| 110 | +they provide useful knobs. Create a dedicated pool for Cacti |
| 111 | +(`/etc/php/8.x/fpm/pool.d/cacti.conf`): |
| 112 | + |
| 113 | +```ini |
| 114 | +[cacti] |
| 115 | +user = www-data |
| 116 | +group = www-data |
| 117 | + |
| 118 | +listen = /run/php/php-cacti.sock |
| 119 | +listen.owner = www-data |
| 120 | +listen.group = www-data |
| 121 | + |
| 122 | +; Drop all inherited environment variables — pass only what Cacti needs. |
| 123 | +clear_env = yes |
| 124 | + |
| 125 | +; Only execute .php files — blocks .phtml, .php3, .phar, etc. |
| 126 | +security.limit_extensions = .php |
| 127 | + |
| 128 | +; Disable dangerous functions at the pool level (not overridable by ini_set). |
| 129 | +php_admin_value[disable_functions] = exec,passthru,shell_exec,system,popen,proc_open,pcntl_exec |
| 130 | + |
| 131 | +; Restrict file operations to the Cacti tree. |
| 132 | +; NOTE: open_basedir disables the PHP realpath cache. Measure the |
| 133 | +; performance impact before enabling in production. It is an extra |
| 134 | +; safety net, not a full security boundary (PHP documentation). |
| 135 | +; php_admin_value[open_basedir] = /var/www/html/cacti:/tmp |
| 136 | + |
| 137 | +; Suppress PHP version disclosure. |
| 138 | +php_admin_flag[expose_php] = Off |
| 139 | +``` |
| 140 | + |
| 141 | +Reload php-fpm after changes: |
| 142 | + |
| 143 | +```bash |
| 144 | +systemctl reload php8.x-fpm # Debian/Ubuntu |
| 145 | +systemctl reload php-fpm # RHEL/Rocky/Alma |
| 146 | +``` |
| 147 | + |
| 148 | +--- |
| 149 | + |
| 150 | +## 4. SELinux (RHEL / AlmaLinux / Rocky Linux) |
| 151 | + |
| 152 | +SELinux ships enforcing on RHEL-family systems. These commands apply |
| 153 | +the correct file contexts so Apache can serve Cacti while limiting what |
| 154 | +the web process can write. |
| 155 | + |
| 156 | +### File Contexts |
| 157 | + |
| 158 | +```bash |
| 159 | +CACTI=/var/www/html/cacti |
| 160 | + |
| 161 | +# Read-only web content (Apache can read, not write) |
| 162 | +semanage fcontext -a -t httpd_sys_content_t "${CACTI}(/.*)?" |
| 163 | +semanage fcontext -a -t httpd_sys_content_t "${CACTI}/scripts(/.*)?" |
| 164 | +semanage fcontext -a -t httpd_sys_content_t "${CACTI}/resource(/.*)?" |
| 165 | + |
| 166 | +# Read-write content for runtime paths only |
| 167 | +semanage fcontext -a -t httpd_sys_rw_content_t "${CACTI}/rra(/.*)?" |
| 168 | +semanage fcontext -a -t httpd_sys_rw_content_t "${CACTI}/log(/.*)?" |
| 169 | +semanage fcontext -a -t httpd_sys_rw_content_t "${CACTI}/cache(/.*)?" |
| 170 | + |
| 171 | +# Apply the contexts |
| 172 | +restorecon -Rv "${CACTI}" |
| 173 | +``` |
| 174 | + |
| 175 | +### Database Network Access |
| 176 | + |
| 177 | +If your database server is reached over TCP (not a local Unix socket), |
| 178 | +enable the SELinux boolean that permits it: |
| 179 | + |
| 180 | +```bash |
| 181 | +setsebool -P httpd_can_network_connect_db on |
| 182 | +``` |
| 183 | + |
| 184 | +### Poller Systemd Drop-in |
| 185 | + |
| 186 | +The Cacti spine poller runs as a separate process and may need to connect |
| 187 | +outward for SNMP. Add a drop-in under |
| 188 | +`/etc/systemd/system/cactid.service.d/selinux.conf`: |
| 189 | + |
| 190 | +```ini |
| 191 | +[Service] |
| 192 | +# Allow spine to connect to arbitrary network hosts for SNMP polling. |
| 193 | +SELinuxContext=system_u:system_r:httpd_t:s0 |
| 194 | +``` |
| 195 | + |
| 196 | +Run `systemctl daemon-reload && systemctl restart cactid` after changes. |
| 197 | + |
| 198 | +> **Verify:** After applying contexts, confirm Apache can still serve pages |
| 199 | +> and the poller can poll. Use `audit2why` on any AVC denials in |
| 200 | +> `/var/log/audit/audit.log`. |
| 201 | +
|
| 202 | +--- |
| 203 | + |
| 204 | +## 5. AppArmor (Debian / Ubuntu) |
| 205 | + |
| 206 | +Ubuntu Server ships AppArmor enforcing by default. Create a profile for |
| 207 | +the Cacti php-fpm pool at `/etc/apparmor.d/cacti-fpm`: |
| 208 | + |
| 209 | +``` |
| 210 | +#include <tunables/global> |
| 211 | +
|
| 212 | +profile cacti-fpm /usr/sbin/php-fpm* { |
| 213 | + #include <abstractions/base> |
| 214 | + #include <abstractions/php> |
| 215 | + #include <abstractions/mysql> |
| 216 | +
|
| 217 | + # Cacti tree — read-only by default |
| 218 | + /var/www/html/cacti/ r, |
| 219 | + /var/www/html/cacti/** r, |
| 220 | +
|
| 221 | + # Allow execution of Cacti PHP files |
| 222 | + /var/www/html/cacti/*.php r, |
| 223 | + /var/www/html/cacti/**/*.php r, |
| 224 | +
|
| 225 | + # Runtime write paths |
| 226 | + /var/www/html/cacti/rra/** rw, |
| 227 | + /var/www/html/cacti/log/** rw, |
| 228 | + /var/www/html/cacti/cache/** rw, |
| 229 | +
|
| 230 | + # Temp files |
| 231 | + /tmp/** rw, |
| 232 | + /var/tmp/** rw, |
| 233 | +
|
| 234 | + # Deny write to scripts/ and resource/ |
| 235 | + deny /var/www/html/cacti/scripts/** w, |
| 236 | + deny /var/www/html/cacti/resource/** w, |
| 237 | +} |
| 238 | +``` |
| 239 | + |
| 240 | +Load and enforce the profile: |
| 241 | + |
| 242 | +```bash |
| 243 | +apparmor_parser -r /etc/apparmor.d/cacti-fpm |
| 244 | +aa-enforce /etc/apparmor.d/cacti-fpm |
| 245 | +``` |
| 246 | + |
| 247 | +### Attach the Profile to the php-fpm Service |
| 248 | + |
| 249 | +Add a drop-in at `/etc/systemd/system/php8.x-fpm.service.d/apparmor.conf`: |
| 250 | + |
| 251 | +```ini |
| 252 | +[Service] |
| 253 | +AppArmorProfile=cacti-fpm |
| 254 | +``` |
| 255 | + |
| 256 | +Reload and restart: |
| 257 | + |
| 258 | +```bash |
| 259 | +systemctl daemon-reload |
| 260 | +systemctl restart php8.x-fpm |
| 261 | +``` |
| 262 | + |
| 263 | +Use complain mode (`aa-complain`) first to test without blocking, then |
| 264 | +switch to enforce once logs show no legitimate denials: |
| 265 | + |
| 266 | +```bash |
| 267 | +# Test mode |
| 268 | +aa-complain /etc/apparmor.d/cacti-fpm |
| 269 | + |
| 270 | +# Enforce when ready |
| 271 | +aa-enforce /etc/apparmor.d/cacti-fpm |
| 272 | +``` |
| 273 | + |
| 274 | +--- |
| 275 | + |
| 276 | +## 6. Verification Checklist |
| 277 | + |
| 278 | +After applying all controls, confirm: |
| 279 | + |
| 280 | +- [ ] `curl -s http://localhost/cacti/scripts/ss_webseer.php` returns 403 |
| 281 | +- [ ] `curl -s http://localhost/cacti/resource/` returns 403 for any `.php` inside |
| 282 | +- [ ] The Cacti web UI loads and graphs render (rra/ is writable) |
| 283 | +- [ ] The poller runs without errors (`tail -f /var/www/html/cacti/log/cacti.log`) |
| 284 | +- [ ] On RHEL: no AVC denials in `ausearch -m avc -ts recent` |
| 285 | +- [ ] On Debian/Ubuntu: no AppArmor denials in `journalctl -k | grep apparmor` |
| 286 | + |
| 287 | +--- |
| 288 | + |
| 289 | +## See Also |
| 290 | + |
| 291 | +- [Installing Under Ubuntu/Debian](Installing-Under-Ubuntu-Debian.md) |
| 292 | +- [Installing Under CentOS (LAMP)](Install-Under-CentOS_LAMP.md) |
| 293 | +- [Standards Security](Standards-Security.md) |
0 commit comments