Check nginx-security¶
Overview¶
Checks the local security posture of an NGINX installation: which dynamic modules it loads, which account its worker processes run under, whether that account can be logged into, and the ownership and permissions of the configuration directory and the process ID file. Every path is taken from the values the binary itself reports, so a setting left at its compiled-in default is checked just like one written into the configuration. Each finding maps to a copy-pasteable recommendation. Alerts when a dynamic module widens the attack surface without being needed, when the worker account is privileged or can be logged into, or when a file the server relies on is readable or writable beyond root. Individual checks can be excluded with --ignore. Requires root or sudo.
The checks follow the "Minimize NGINX Modules" and "Permissions and Ownership" controls of the CIS NGINX Benchmark.
Important Notes:
- The check is part of the Nginx Service Set, where it runs through the
-sudocheck command. A host that has not deployed the sudoers file makes the service report UNKNOWN until it has. - Requires root or sudo.
nginx -Tcreates the runtime directory while parsing, which an unprivileged account may not do, and the shadow database is unreadable without it. - The check re-parses the configuration from disk. A change that has been written but not reloaded is therefore reported as if it were already in force. The worker account is the exception: it is additionally compared against the accounts the running processes actually use.
- The configuration tree check fails on a stock installation by design. The distributions ship
0644for files and0755for directories, and the benchmark's hardening target is0640and0750, so world-readable is a finding. World-writable and world-readable are reported apart, because only the first is a defect on any system. Exclude the check with--ignore=^Config tree access$if the site accepts the distribution default. - A web server running in a container shows up in the host's process list under a mapped user id. Only processes sharing this host's mount namespace are counted, so a containerised NGINX does not make the host's check report a stray account.
- Whether a loaded dynamic module is really needed is a judgement only the operator can make. The check lists what is loaded and leaves the decision;
--ignore=^Dynamic modules$records that it has been made.
Data Collection¶
Two invocations of the NGINX binary per run, both of which only read: -V for the compile-time paths and accounts (prefix, configuration path, process ID path, user, group, modules path), which is what every setting the configuration leaves out falls back to, and -T for the fully parsed configuration including every included file behind its own marker. The configuration directory is then walked and inspected with stat, as is the process ID file. UID_MIN comes from /etc/login.defs, the account's shell and groups from the local user database, its lock state from /etc/shadow, and the running worker processes from psutil. Nothing is stored between runs.
Symbolic links are followed, the way the benchmark's own audit does. A configuration directory that links to a modules directory elsewhere is therefore reported with the permissions of the target, which is what actually applies.
Fact Sheet¶
| Fact | Value |
|---|---|
| Check Plugin Download | https://github.com/Linuxfabrik/monitoring-plugins/tree/main/check-plugins/nginx-security |
| Nagios/Icinga Check Name | check_nginx_security |
| Check Interval Recommendation | Every day |
| Can be called without parameters | Yes |
| Runs on | Linux |
| Compiled for Windows | No |
| Requirements | command-line tool nginx; root or sudo |
| 3rd Party Python modules | psutil |
| Perfdata compatible with Prometheus | Yes |
Help¶
usage: nginx-security [-h] [-V] [--always-ok] [--brief] [--command COMMAND]
[--ignore IGNORE] [--match MATCH]
[--no-match-severity {ok,warn,crit,unknown}]
[--no-perfdata] [--severity {warn,crit}]
[--timeout TIMEOUT]
Checks the local security posture of an NGINX installation: which dynamic
modules it loads, which account its worker processes run under, whether that
account can be logged into, and the ownership and permissions of the
configuration directory and the process ID file. Every path is taken from the
values the binary itself reports, so a setting left at its compiled-in default
is checked just like one written into the configuration. Each finding maps to
a copy-pasteable recommendation. Alerts when a dynamic module widens the
attack surface without being needed, when the worker account is privileged or
can be logged into, or when a file the server relies on is readable or
writable beyond root. Individual checks can be excluded with --ignore.
Requires root or sudo.
options:
-h, --help show this help message and exit
-V, --version show program's version number and exit
--always-ok Always returns OK.
--brief Hide the rows that are within the thresholds and show
only those in a WARN or CRIT state. Perfdata and
alerting are unaffected: every item still emits
performance data and still drives the overall check
state, so this is safe to leave on.
--command COMMAND Path to the NGINX binary. Probed automatically in the
PATH if not given. Example:
`--command=/usr/local/nginx/sbin/nginx`
--ignore IGNORE Any check whose name matches this Python regex will be
dropped from the report. Use it for a finding the site
knowingly accepts, for example a dynamic module the
service needs. Case-sensitive by default; use `(?i)`
for case-insensitive matching. Can be specified
multiple times. Example: `--ignore=^Dynamic modules$`
--match MATCH Only report the checks whose name matches this Python
regex. Filter by this Python regular expression. Case-
sensitive by default; use `(?i)` for case-insensitive
matching. Can be specified multiple times. If both
`--match` and `--ignore` are given, an item must match
`--match` AND not match `--ignore` to be reported
(include first, exclude second). Examples:
`(?i)example` to match "example" regardless of case.
`^(?!.*example).*$` to match any string except
"example" (negative lookahead).
--no-match-severity {ok,warn,crit,unknown}
State to report when no item matches the filters and
nothing is checked. Default: ok
--no-perfdata Suppress the performance data section from the output.
The status message and the exit code are unaffected,
so alerting keeps working while trending data is
dropped.
--severity {warn,crit}
State to report for a failed check. One of `warn` or
`crit`. Default: warn
--timeout TIMEOUT Network timeout in seconds. Default: 8 (seconds)
Documentation:
https://linuxfabrik.github.io/monitoring-plugins/check-plugins/nginx-security/
Usage Examples¶
A stock installation. The account side is clean out of the box, the configuration tree carries the distribution defaults:
sudo ./nginx-security
1 of 7 checks failed.
Recommendations:
* Config tree access: `chmod o= /etc/nginx /etc/nginx/conf.d ...` (currently /etc/nginx (0755), /etc/nginx/conf.d (0755), ...)
Check ! Result ! Detail ! State
-------------------+-----------------------+--------------------------------------------------+----------
Dynamic modules ! none loaded ! No dynamic module adds to the attack surface. ! [OK]
Worker account ! nginx (uid 101) ! Dedicated unprivileged system account. ! [OK]
Account shell ! nginx: `/bin/false` ! Cannot be logged into. ! [OK]
Account locked ! nginx: locked ! The password is locked. ! [OK]
Config tree owner ! 9 paths ! Everything belongs to `root:root`. ! [OK]
Config tree access ! 9 of 9 paths ! 9 paths readable by other. ! [WARNING]
Pid file ! /run/nginx.pid (0644) ! Owned by `root:root` and not writable by others. ! [OK]
The same host with the distribution default accepted:
sudo ./nginx-security --ignore=^Config
Everything is ok. All 5 checks passed.
A host whose worker runs as root, whose account can be logged into, and whose configuration anybody may rewrite:
sudo ./nginx-security --brief
3 of 7 checks failed.
Check ! Result ! Detail ! State
-------------------+-------------------+-------------------------------------------------------+----------
Worker account ! root (uid 0) ! runs as root; member of root; processes run as nginx. ! [WARNING]
Account shell ! root: `/bin/bash` ! An interactive login shell. ! [WARNING]
Config tree access ! 9 of 9 paths ! 1 path writable by other, 8 paths readable by other. ! [WARNING]
States¶
- Returns OK if every check that could be carried out passed.
- Returns WARN (or CRIT with
--severity=crit) if at least one check failed:- a dynamic module is loaded, which the operator has to confirm is needed,
- there is no
userdirective, or the worker account runs as root, is an account shared with other daemons (daemon,nfsnobody,nobody,nogroup), has a uid at or aboveUID_MIN, is a member of a privileged group (adm,root,sudo,wheel), or a running process uses an account other than the configured one, - the worker account has a shell the system lists in
/etc/shells, - the worker account carries a password that can be used, or none at all,
- something below the configuration directory is not owned by
root:root, or is readable or writable by other, - the process ID file is not owned by
root:root, or its mode is wider than0644.
- Returns UNKNOWN if
nginxis not found, if the binary given via--commanddoes not exist, or if the configuration does not parse, in which casenginx -Tproduces no dump at all. - A check that cannot be carried out is reported as not evaluated and neither counts nor drives the state. That covers an account served by a directory service rather than by local files, a shadow database the check may not read, and a process ID file that does not exist because the server is not running.
- If
--matchand--ignorebetween them exclude every check, the plugin prints "Nothing checked." and returns the state given by--no-match-severity(OK by default). --always-okmasks a WARN or CRIT as OK.
Perfdata / Metrics¶
| Name | Type | Description |
|---|---|---|
| nginx_checks_evaluated | Number | Number of checks that could be carried out on this run. |
| nginx_checks_failed | Number | Number of checks that failed. |
| nginx_dynamic_modules_loaded | Number | Number of dynamic modules the configuration loads. |
Troubleshooting¶
returned nothing ... The server configuration does not parse.¶
nginx -T prints no dump when the configuration test fails. Run nginx -t by hand to see the syntax error. The server keeps running on its last good configuration while this is the case, so the check failing here says nothing about availability.
NGINX does not seem to be installed¶
nginx was not found in the PATH of the account running the check. On a host where the binary lives outside the usual locations, for example a source build under /usr/local/nginx, point at it with --command=/usr/local/nginx/sbin/nginx.
The configuration tree check fails on a fresh installation¶
Expected. Every distribution ships 0644 for files and 0755 for directories, while the benchmark wants 0640 and 0750. Tighten the tree, or exclude the check with --ignore=^Config tree access$. Look at the Detail column first: world-writable is a defect worth fixing on any system, world-readable is the hardening target.
The modules directory keeps being reported after chmod -R o= /etc/nginx¶
/etc/nginx/modules is usually a symlink to a directory elsewhere, and chmod -R does not follow it. Tighten the target instead, or accept it: the module files themselves are shipped by the package and readable anyway.
Account locked is not evaluated¶
Either the check is not running as root, in which case /etc/shadow cannot be read, or the worker account is served by a directory service and has no local shadow entry. The message says which of the two it is.
Credits, License¶
- Authors: Linuxfabrik GmbH, Zurich
- License: The Unlicense, see LICENSE file.