File Plugins¶
The file plugins report on files and directories: how large they are, how fast they grow, how old they are, how many there are, and who owns them. They read file metadata only, never the contents.
This page holds what the group has in common. Each plugin's README covers what is specific to it.
Plugins in this group¶
| Plugin | Reports | Selects files with |
|---|---|---|
file-age |
time since the last modification | --filename, SMB |
file-count |
number of matching files | --filename, SMB |
file-descriptors |
file descriptors in use against the system limit | reads /proc |
file-growth |
how fast files grow or shrink, as a rate per second | --filename, SMB |
file-ownership |
owner, group and permissions against what they should be | --filename |
file-size |
size of one or more files | --filename, SMB |
file-descriptors shares the name but not the mechanics: it reads kernel
counters instead of a path an operator points it at, so most of this page does
not apply to it.
Selecting files¶
--filename takes a path or a glob pattern, as documented for
Python's glob module:
./file-size --filename=/var/log/messages
./file-size --filename='/var/log/*.log'
./file-size --filename='/var/log/**/*.log'
Write the parameter with =, as above, or quote the pattern. Both keep the
shell from expanding it: with --filename=/var/log/*.log the shell would have
to match the whole word against a path, finds nothing, and passes it through
untouched.
Separating the parameter from its value by a space is what goes wrong.
--filename /var/log/*.log is expanded before the plugin ever sees it, so the
check reads only the first match and discards the rest without a word: a
directory of four log files is reported as "1 file checked", and nothing says
that three were dropped.
Points that apply to every plugin in the group:
- Recursive globs (
**) can use a lot of memory on large directory trees, because the whole match list is built before anything is checked. Prefer a pattern that names the directory level you mean. - Symbolic links are followed. A link is reported as whatever it points at.
- Directories are skipped by
file-sizeandfile-growth, because the size a filesystem reports for a directory says nothing about its contents and differs between filesystems.file-ageandfile-countcan include them, see their--only-dirsand--only-filesparameters. - Files that disappear mid-check are skipped rather than reported as an error. Temporary files come and go while the check runs.
- A pattern that matches nothing is UNKNOWN with
No files found., not OK. A check that silently reports success on a typo would be worse than useless.
SMB shares¶
file-age, file-count, file-growth and file-size read from an SMB share
with --url instead of --filename. The two are mutually exclusive:
./file-size --url=smb://server.example/share/logs --username=monitoring --password=linuxfabrik
--patternfilters the names on the share. It takes*and?as wildcards, and is not a regular expression.--timeoutbounds the connection, so an unreachable server fails the check instead of hanging until the monitoring system kills it.- This needs the optional
PySmbClientandsmbprotocolPython modules. They ship with the RPM and DEB packages; for a source install, add them to the venv of the user running the plugins:
python3 -m pip install PySmbClient smbprotocol
A password on the command line is visible in ps auxf to every user on the
host. Prefer a service account with read-only access to the share, and set the
password on the concrete Icinga service object rather than on a template or a
Service Set, so it is only distributed to the zone that runs the check.
Thresholds and units¶
--warning and --critical take Nagios ranges, which is what allows a bound on
one side only, or an alert on a value falling inside a range.
THRESHOLDS.md has the syntax and a table of worked examples.
Where a threshold is a size (file-size, file-growth), it is written with
the IEC qualifiers from UNITS.md, so KiB, MiB, GiB and so on,
always base 1024. Lowercase and the shorter spellings (k, kb, kib) are
accepted too. A value without a qualifier is a number of bytes.
A range bound includes its own value: a file growing at exactly the threshold is still within it and does not alert.
Aggregation across many files¶
file-age, file-growth and file-size take --perfdata-mode to collapse
the performance data of a glob that matches many files into a single series,
either mean or median. Without it, a glob over hundreds of files would
write hundreds of series into the time series database on every run.
The status message and the exit code are unaffected: every matched file is still checked against the thresholds and still shows up in the output table.
--perfdata-mode=None is the same as not passing the parameter. It exists so
the Icinga Director dropdown has an entry for "do not aggregate" rather than
only an empty field.
These plugins are not in the sudoers file¶
Linuxfabrik ships a sudoers file that lets the monitoring user run selected
plugins as root without a password (see assets/sudoers/). The file plugins
are deliberately not in it, and neither is a -sudo variant of their check
commands in the Icinga Director basket.
The reason is the shape of their parameters. --filename takes any path the
caller writes, including a glob. A plugin that accepts a free-form path and
runs as root hands whoever controls the monitoring account a way to ask about
any file on the system. For a plugin that prints file contents this is an
immediate privilege escalation, which is what happened to the logfile plugin
in GHSA-f54c-p5vg-mr5c.
The file plugins print metadata rather than contents, so the same mistake would
leak less, but it would still tell an attacker the size, age and existence of
every file on the host, including paths they have no business knowing about.
Rather than guess a list of directories that would be safe enough to allow, we leave the decision where it belongs: with the administrator, who knows which files on this host are worth monitoring and who is allowed near the monitoring account.
What to do when the plugin cannot read a file¶
A file the monitoring user may not open produces one of these:
No files found.
[Errno 13] Permission denied: '/var/log/audit/audit.log'
No files found. is the more confusing of the two, because it is also what an
empty glob looks like. A directory the monitoring user cannot traverse hides
its contents from the glob entirely, so a pattern that matches nothing and a
pattern the user may not look at give the same answer.
Confirm which of the two it is by looking as the monitoring user:
sudo --user=icinga ls -l /var/log/audit/audit.log
Then pick one of the following, in this order of preference.
Grant read access with an ACL. The cleanest fix, because it grants exactly one thing and survives a reboot:
setfacl --modify=u:icinga:r /var/log/audit/audit.log
For a whole directory whose files are recreated by log rotation, set a default ACL as well, so new files inherit it:
setfacl --modify=u:icinga:rx --modify=default:u:icinga:r /var/log/myapp/
Adjust the rotation config when a rotated file keeps losing its
permissions. logrotate recreates files with the mode from its own
configuration, so put the permission there instead of repairing it by hand
after every rotation:
/var/log/myapp/*.log {
create 0640 root icinga
}
Add your own sudoers entry when the file cannot be opened up, an audit log under a policy that forbids widening its permissions for example. This is a decision about your host, so it belongs in your own configuration and not in the package:
icinga ALL=(root) NOPASSWD: /usr/lib64/nagios/plugins/file-size --critical 200M --filename /var/log/myapp/big.log --warning 180M
Pin every argument, and use no wildcard anywhere. This is the part that is
easy to get wrong. An entry that ends at the plugin path allows every
--filename the caller cares to write, which is the thing this group avoids by
default. A * does not narrow that down the way it looks like it should,
because sudo does not match arguments one by one: it joins them into a single
string and matches that with fnmatch(), without FNM_PATHNAME. A *
therefore matches spaces and slashes and runs straight across argument
boundaries. So
icinga ALL=(root) NOPASSWD: /usr/lib64/nagios/plugins/file-size --critical * --filename /var/log/myapp/big.log --warning *
still permits
sudo /usr/lib64/nagios/plugins/file-size --critical 200M \
--filename /var/log/myapp/big.log --warning 180M --filename /root/.ssh/id_rsa
and reports on the key instead: the second --filename is swallowed by the
trailing *, and the check resolves the last one it is given. A * in the
middle of the line is no safer than one at the end.
Write the arguments out instead, in the order the check command generates them, which is alphabetical by parameter name. The cost is that changing a threshold in the Icinga Director means changing the sudoers entry as well, and the check reports a sudo error until you do.
sudo can match arguments as a POSIX extended regular expression when the pattern
starts with ^ and ends with $, which would allow a threshold to vary without
opening up the path. That needs sudo 1.9.10 or newer: RHEL 8 (1.9.5), Debian 11
(1.9.5) and Ubuntu 22.04 (1.9.9) read such an entry as a literal pattern
instead, so the check is denied and stops working. Only use it on a fleet where
every host is new enough.
Then point the service at a check command that prepends /usr/bin/sudo. None of
the file checks ship such a variant, for the same reason they are not in the
sudoers file, so create your own in the Icinga Director. Do not edit the shipped
basket files.
Worked example: a file only root can reach¶
Say you want to alert when /var/log/audit/audit.log grows past a size. The
audit package ships /var/log/audit as drwxr-x--- owned by root, so the
monitoring user cannot traverse into the directory, let alone stat the log. The
check reports a permission error, and none of the ACL approaches above are open
to you because the audit log is exactly the kind of file a policy tends to
forbid opening up.
The remaining route is a sudoers entry of your own, with every argument pinned:
icinga ALL=(root) NOPASSWD: /usr/lib64/nagios/plugins/file-size --critical 200M --filename /var/log/audit/audit.log --warning 10M
Create a check command in the Icinga Director that prepends /usr/bin/sudo to
cmd-check-file-size, point a service at it, and set the same thresholds on
that service that the sudoers entry pins. Change one and you have to change the
other, or sudo refuses the call.
Before you build it, check whether the alert can fire at all. auditd rotates
its own log: /etc/audit/auditd.conf ships max_log_file = 8 (megabytes),
num_logs = 5 and max_log_file_action = ROTATE, so on a default installation
the file never comes near the given threshold. A size
threshold on a self-rotating log is a check that can only ever report OK. Either
set the threshold just under max_log_file so it catches rotation having
stopped, or watch the thing you actually care about: file-count on
/var/log/audit/*.log to see whether rotation is keeping up, or
systemd-unit on auditd.service.