Securing NSClient++¶
Protocols¶
NSClient++ supports a multitude of protocols thus securing the server will depend on the protocol you are using.
NRPE¶
For details on setting up and using NRPE please see the Active Monitoring with NRPE.
In general when using NRPE do not use NRPE version 2 with the ADH key and do not rely on allowed hosts as the security mechanism. Instead, certificates and proper two-way TLS are preferred.
To set up NRPE with two-way TLS you need to:
- Create a CA (Certificate Authority) or use an existing one.
- Create a server certificate for the NSClient++ server signed by the CA.
- Create a client certificate for the monitoring server signed by the CA.
- Configure NSClient++ to use the server certificate and trust the CA.
- Configure the monitoring server to use the client certificate and trust the CA.
Step 1-3 will depend on your environment and is covered in the Active Monitoring with NRPE.
On Windows step 4 can also be done at install time: the MSI’s CERTIFICATE, CERTIFICATE_KEY and CERTIFICATE_CA
properties install your files into the security folder under the default names, so the agent serves your CA-signed
certificate from the first start instead of generating a self-signed one - see
Installing your own TLS certificates.
On an installed agent, step 4 can easily be setup from the command line like so:
$ nscp nrpe install ^
--allowed-hosts 127.0.0.1 ^
--insecure=false --verify=peer-cert ^
--certificate nsclient.pem ^
--certificate-key nsclient.key ^
--ca ca.pem
A quick breakdown of the options:
--allowed-hosts: List of allowed hosts to connect from.--insecure: If true, do not verify the server certificate.--verify: What to verify, can benone,peer-cert.--certificate: Path to the server certificate.--certificate-key: Path to the server certificate key.--ca: Path to the CA certificate.
NRDP¶
NRDP is an HTTP-based passive submission protocol (typically Nagios XI). The agent is the client — it does not accept incoming NRDP traffic. Securing NRDP therefore means:
- Make sure the agent talks to the NRDP server over HTTPS.
- Keep the shared token out of cleartext config.
- Restrict which checks may be relayed through NRDP using the Permission policy so a compromised caller cannot use the relay to flood the upstream monitor.
There is no nscp nrdp install command — NRDP is configured by editing the target block. A minimal hardened target:
[/modules]
NRDPClient = enabled
[/settings/NRDP/client/targets/default]
; Always use https - the token is sent on the wire.
address = https://nagios.example.com/nrdp/
ssl = true
; Token / password are the same thing in NRDP - either key works.
; Move this into credential manager (see Passwords above) so it is not in
; cleartext beside the config.
token = $CRED$
After editing, apply with:
$ nscp service --restart
If you want to verify a target before relying on it, submit a synthetic passive result with:
$ nscp nrdp --module submit --target default --host myhost --command check_ok --message "test"
Mod-Gearman¶
Mod-Gearman is a pull protocol: the agent connects out to a gearmand job
server, registers on a queue and runs whatever checks the monitoring core put
there. Nothing listens on the monitored host, which is the security win. What
you get in exchange is a shared-secret model with some sharp edges, because
the protocol has no better one to offer:
gearmandhas no authentication, and the payload envelope is AES-256 in ECB mode with the password used directly as the key. It hides content. It does not authenticate the sender and it does not prevent replay. Anyone holding the key can queue jobs onto a queue a worker listens on, and can forge results for any host the core monitors.
That is the protocol’s design (it is the same for mod_gearman’s own workers), and this module cannot fix it — only contain it. The containment:
- Never run without a key. The agent refuses to start with
encryption = true(the default) and nokey. Turning encryption off additionally requiresinsecure = true, and logs a warning on every start. Usekey fileto keep the secret out ofnsclient.ini, and restrict that file to the account the agent runs as — on Linux the module warns when it is group- or world-readable; on Windows, set the ACL yourself. - Register the fewest queues. Only
hostgroups/servicegroupsare registered by default.allow shared queuesadds the generichostandservicequeues, which carry every check in the installation — leave it off. - Prefer agent mode where it fits. In
mode = agenta job naming any other host is answered UNKNOWN rather than executed, so a leaked key buys an attacker only this host’s own command surface. - Treat a proxy as a privileged host. In
mode = proxya job can drive the proxy’sNRPEClient,NSCPClientandCheckWMIat any host those can reach, using any credentials stored in the proxy’s configuration. Give each proxy queue its own key, scope those credentials to monitoring, and put the proxy andgearmandon a network you control. - Expire replays. Set
max ageto a little more than the check interval. A job older than that is answered UNKNOWN instead of run, which both discards a replayed job and stops a post-outage backlog from being executed late. - Cap the command surface. A job can only invoke commands the agent
already exposes, and the
allow arguments/allow nasty charactersguards are NRPEServer’s. There is no shell fallback. Use the permission policy to restrictGearmanClientto the exact commands the core actually schedules.
A minimal hardened worker:
[/modules]
GearmanClient = enabled
[/settings/gearman/worker]
server = gearmand.example.com:4730
; Keep the secret out of the config file; lock the file to the service account.
key file = ${shared-path}/gearman.key
hostgroups = win-srv01
; Agent mode is the default: refuse a job that names another host.
mode = agent
; A little over the check interval, so a replayed or stale job is not run.
max age = 600
The passive channel (/settings/gearman/client) uses the same key and the
same reasoning — a forged result is a forged alert. It refuses to submit
encrypted without a key, and refuses encryption = false unless the target
also sets insecure = true.
For the full setup on either core see Mod-Gearman.
Http/Https/WebServer¶
The WEB module exposes the REST API and the web UI over HTTP or HTTPS on port 8443 by default. In any production
deployment you want HTTPS, a dedicated non-admin user for the monitoring server, and (where possible) the built-in
admin account disabled.
Install with HTTPS from the command line¶
The fastest way to get a hardened install is the WEB module’s own install command:
$ nscp web install ^
--allowed-hosts 10.0.0.0/24 ^
--certificate nsclient.pem ^
--certificate-key nsclient.key ^
--port 8443
A quick breakdown of the options:
--allowed-hosts: CIDR or comma-separated list of source IPs allowed to connect.--certificate/--certificate-key: TLS server cert and private key. HTTPS is the default; without these the command generates a self-signed certificate at${certificate-path}/certificate.pem(key and certificate in the one file) when it does not exist yet.--httpsis still accepted and means the same as leaving it out. On Linux, where the command runs undersudoand the packaged service runs asnsclient, a generated certificate is handed to the service account (the owner of${data-path}) so the service can load it; an existing certificate is left as is.--insecure: Serve cleartext HTTP instead — setsallow insecure = true, writes no certificate and moves the port from8443to8080when it is the default. Session keys and passwords then travel in clear, so only for loopback or behind a TLS-terminating proxy.--port: Listening port (default8443).--password: Admin password. If omitted, a random one is generated and printed. It is stored hashed, both as the shared/settings/default/passwordand in theadminuser’s row;nscp web password --setrotates it later (see Passwords).--disable-admin: Lock out the built-in admin user — no admin row is created, the REST script-upload endpoint becomes unreachable. Recommended for monitoring-only deployments. Mutually exclusive with--password; create a dedicated user separately (see below).
For a monitoring-only deployment (no remote reconfiguration through the WEB UI), combine --disable-admin with a
dedicated user:
$ nscp web install ^
--allowed-hosts 10.0.0.0/24 ^
--certificate nsclient.pem ^
--certificate-key nsclient.key ^
--disable-admin
$ nscp web add-user monitoring --role monitoring --password "<strong password>"
If the monitoring server only runs checks the agent already defines — no
thresholds or targets supplied in the request — use --role restricted
instead. It is the same role with arguments refused, the REST counterpart of
NRPE’s allow arguments = false. If it only scrapes metrics, use
--role metrics, which opens the two metrics endpoints and nothing else. See
the role list below.
Adding a dedicated user¶
nscp web add-user creates or updates a per-user row under [/settings/WEB/server/users/<name>]:
$ nscp web add-user monitoring --role monitoring --password "<strong password>"
$ nscp web add-user poller --role restricted --password "<strong password>"
$ nscp web add-user prometheus --role metrics --password "<strong password>"
$ nscp web add-user dashboard --role client
Options:
--user: The username (positional<name>also works).--password: Plaintext password. Hashed before being written to the config. If omitted, a random one is generated and printed once — copy it then.--role: Built-in roles shipped by the module:restricted—public, queries.execute.noargs, aliases.list, login.get. The tightest useful role: it can run the checks the agent defines but cannot pass arguments to them, which is the REST equivalent of the NRPE server’sallow arguments = false. A request that carries any query-string parameter is refused with403 Arguments are not allowed for this user. Give such a caller the checks it needs as aliases, so the arguments live in your configuration rather than in the request. Note that every query parameter counts, so this role must authenticate with a header rather than with a legacy?TOKEN=query parameter.metrics—public, metrics.list, openmetrics.list, login.get. Reads/api/v2/metricsand/api/v2/openmetricsand nothing else: the role for a Prometheus scraper, which needs no ability to run checks. See the Prometheus scenario.monitoring—public, queries.execute, aliases.list, login.get, metrics.list, openmetrics.list. Recommended for monitoring servers that need to pass arguments (thresholds, drives, service names) in the request, and that also scrape metrics. Where they don’t pass arguments,restrictedabove is the tighter choice — it is the same role with arguments refused; where they only scrape,metricsis.client— adds query listing; needed for the legacycheck_nscp_apiintegration.full— admin (settings, modules, scripts). Avoid for monitoring callers.legacy—legacy,login.get. Dangerous — do not use for normal clients. It unlocks the deprecatedGET /query/{name}endpoint, which dispatches through the same command registry as the versioned query API. Alegacy-only token can therefore run any registered check or command — including any configuredCheckExternalScriptscommand, which can amount to arbitrary command execution — even though it lacksqueries.execute. The danger is thelegacygrant token itself, not the role name: any role whose grant string includeslegacy(a custom role such asreporting = legacy,login.get, not just the built-in one) is equally powerful, and NSClient++ logs aSECURITYwarning at startup for each such role. Grantlegacyonly to a specific, trusted legacy system that cannot be upgraded, and constrain it with a permission policy. See the warning on the Web interface page.
--grant: Additional grants beyond the role (seeadd-roleto define new roles).
Move the per-user passwords into the credential manager rather than leaving them in the INI; see the Passwords section below.
Locking down which commands the web user can run¶
The role controls which REST endpoints a user can hit. To control which check commands the user is allowed to
invoke through /queries/{command}/commands/execute, layer the Permission policy on top:
[/settings/permissions/policies]
WEBServer:admin = *
WEBServer:monitoring = CheckSystem.check_cpu, CheckSystem.check_drivesize, CheckDisk.check_drivesize
The two controls are complementary and worth pairing: the policy decides which
commands the user may invoke, the restricted role decides whether it may
shape them. A restricted user pinned to a policy runs a fixed set of checks
exactly as you defined them — which, over REST, is what NRPE with
allow arguments = false gives you.
For deeper coverage of the WEB module’s attack surface — including the --disable-admin trade-off and the
scripts_controller endpoint — see the WEB module section further down.
check_nt (legacy NSClient protocol)¶
The NSClientServer module implements the original NSClient / check_nt protocol (TCP 12489 by default), spoken by the
Nagios check_nt plugin. It is a dead protocol and should be avoided. It predates modern transport security and
cannot be made genuinely secure. If you have any choice at all, monitor over NRPE with two-way TLS or the
REST API instead, and simply do not load NSClientServer.
Treat check_nt as unauthenticated cleartext. Anyone who can observe a single request on the wire learns the password and can replay it forever. Do not expose it on any network you do not fully trust, and never reuse the check_nt password anywhere else.
Why it is not secure:
- The password is sent in cleartext on every request. check_nt prepends the configured password as the first field
of every query. The server has no working TLS path in practice (the
ssltoggle exists, but virtually no check_nt client ever implemented it), so the password travels unencrypted. The module logs a warning to this effect on startup whenever a password is configured. - There is no replay protection. Unlike NSCA there is no timestamp, nonce or sequence number — a captured request can be replayed verbatim, indefinitely. Capturing the password once is equivalent to knowing it permanently.
- A single shared secret is the only authentication. There is no per-client identity, no certificate and no mutual
auth.
allowed hostsis source-IP filtering, not authentication, and is spoofable on an untrusted network.
What it does not expose, to be fair:
- It cannot run arbitrary commands. The protocol’s ten request codes map to a fixed set of read-only internal
checks (client version, CPU load, uptime, disk usage, memory, service/process state, performance counters, file age).
The caller cannot choose the command name, cannot inject extra arguments, and there is no path to a shell or to
CheckExternalScripts. The realistic risk is therefore credential capture and read access to system metrics, not remote code execution. The server-side authentication itself is sound (constant-time password compare, an empty password is refused, a single generic error string with no username/oracle) — it is the transport that is broken, and that cannot be fixed within the protocol. The stored value may be the clear text or the hashed formnscp web install/nscp web password --setwrite to[/settings/default]; the client sends the clear text either way, and the hash string itself does not authenticate.
If you must keep it running for a legacy monitoring system:
- Always configure a password (an empty password is refused outright) and firewall TCP
12489to the monitoring host only —allowed hostsis defence-in-depth, not a control. - Use a password dedicated to check_nt and nowhere else, since it is effectively public on any shared network segment.
- Restrict which commands are answered with the
allowsetting (see below) so that the effectively-public password can only read what you actually monitor. - Plan the migration to NRPE or REST; do not build new monitoring on check_nt.
Restricting which commands are answered (allow)¶
Because the password should be assumed compromised, the useful question is how much can a holder of it read? The
allow setting under [/settings/NSClient/server] caps that. It is a comma-separated list where each entry is a group,
the keyword any/all, or an individual command name:
| Group | Commands | Disclosure |
|---|---|---|
metrics |
cpuload, uptime, useddiskspace, memuse |
aggregate system metrics — harmless |
info |
clientversion |
agent version (minor recon) |
service |
servicestate |
service inventory (ShowAll lists all services) |
process |
procstate |
process inventory (ShowAll lists all processes) |
counters |
counter, instances |
arbitrary performance-counter read |
files |
fileage |
arbitrary file existence / last-modified time |
any/all |
everything | full check_nt compatibility (default) |
The default is any, which answers all ten commands (unchanged legacy behaviour). To expose only the harmless system
metrics — denying the arbitrary-read commands (counter, fileage, instances) and the service/process enumeration:
[/settings/NSClient/server]
; Only answer harmless system-metric queries plus the agent version.
allow = metrics, info
Mix groups and individual commands freely, e.g. allow = metrics, info, servicestate to also answer service-state
checks but nothing else. A request for a command outside the list is rejected (the server returns
ERROR: Command not allowed.); an empty/typo’d list logs a warning and rejects everything, so use allow = any to get
the default back.
To remove the attack surface entirely, just don’t load the module:
[/modules]
; NSClientServer = enabled ; leave disabled / removed - prefer NRPE or REST
User account¶
By default, NSClient++ is running as local system which is a easy as it is always available.
A more secure approach is to use a dedicated user account.
This user account should be a local user account with only relevant admin right given the things you check.
TODO: Add example of creating a user account and setting up NSClient++ to use it. TODO: Add example of permissions needed for various checks.
File layout (Windows)¶
Where NSClient++ keeps its files decides who can read them. On Windows there are two layouts, and the newer one exists for this reason.
Legacy (the default) keeps everything beside the executable, under
C:\Program Files\NSClient++\. That directory is not writable by ordinary
users, but it is readable by them — and it holds:
nsclient.ini, with the web/NRPE passwords and any credentials your checks use to reach a database or an API;security\, with the server’s TLS private key and, on a fleet-managed host,agent-state.json— the private key that identifies this machine to the fleet server;- the log, which quotes check arguments and configuration in its error text.
Any logged-in account can read all of it.
Modern (opt-in) moves that writable half to C:\ProgramData\NSClient++\
and restricts it to SYSTEM and Administrators, with inheritance broken so
%ProgramData%’s default Users: Read & Execute does not apply. The program
itself stays in Program Files. The practical effect:
| Legacy | Modern | |
|---|---|---|
nsclient.ini (passwords) |
readable by any user | SYSTEM + Administrators |
| TLS private key, fleet identity | readable by any user | SYSTEM + Administrators |
| Log file | readable by any user | SYSTEM + Administrators |
| Who can modify configuration and scripts | administrators | administrators |
It also stops the agent needing write access to its own install directory: a
non-administrator running nscp client ... no longer tries to open a log file
under Program Files.
Should you use it?¶
Yes for new installs. There is no migration to go wrong, and it closes the readable-secrets gap above:
msiexec /i NSCP-x64.msi LAYOUT=modern /qn
For existing installs, test before you roll it out. The layout itself is
sound, but the upgrade path has not been exercised at scale yet: an upgrade
with LAYOUT=modern moves your configuration, certificates and fleet identity
to the new location, and every environment has its own variations —
configuration management that writes nsclient.ini by path, backup jobs,
monitoring of the log file, custom scripts with hardcoded paths. Run it on a
representative host first, confirm the agent still answers checks and that
whatever else touches those files still finds them, and then roll it out.
Experimental
The modern layout is opt-in and marked experimental: upgrades in particular have not been tested at scale. Nothing changes unless you ask for it — an upgrade without the property keeps the layout the host already has.
An existing installation can also be switched after the fact, without reinstalling:
nscp settings --migrate-layout modern --dry-run # show exactly what will move
nscp settings --migrate-layout modern # move it, then restart the service
See File layout for what lives where in each layout, and what moves.
Passwords¶
The passwords the agent verifies are stored hashed (salted PBKDF2-SHA256, pbkdf2-sha256$… in the file): the per-user
web passwords that nscp web add-user writes, and the shared /settings/default/password that nscp web install
generates or nscp web password --set sets. The web admin seed and the check_nt server verify a login against either a
hash or a clear-text value, so a password written by hand keeps working; re-setting it hashes it in place:
$ nscp web password --set "<the password>"
nscp web password --display can only show a password while it is still in clear text; once hashed, set a new one if it
is lost. NSCAServer is deliberately not one of these servers: it never verifies a password, its shared secret is the
encryption key every submitting client has to know, so that key stays in clear text under [/settings/NSCA/server] and
is inherited from nowhere — not from [/settings/default], and not from NSCAClient, whose key is what this agent
submits to a remote daemon with. With encryption on and no key of its own the server refuses to start, because an empty
password is a well-known key. The Windows MSI hashes a password given on its command line or
typed into its configuration dialog; a value it merely found on disk, which is what pre-fills the dialog on an upgrade,
is left exactly as it is.
That leaves the secrets the agent has to use rather than verify — client-side passwords, tokens and keys for the protocols and checks that reach out — which a hash cannot protect. Out of the box those sit in clear text in the config file, which is not recommended. There are two simple ways to solve this:
- Store the config file in the profile of the user.
- Store secrets in credential manager.
Storing config in user profile¶
Default the service is using the local system account (if you are using a different account please update the paths in
below example).
To move the config file to C:\Windows\System32\Config\systemprofile\NSClient++.
First create the folder:
$ PsExec -i -s cmd /c mkdir "C:\Windows\System32\Config\systemprofile\NSClient++"
Then move the settings file:
$ PsExec -i -s "c:\program files\nsclient++\nscp" settings --migrate-to "ini:C:\Windows\System32\Config\systemprofile\NSClient++\nsclient.ini"
What this will do is update boot.ini to point to the new location and move all settings from the old config file to
the new one.
And finally delete the old config file:
$ del "C:\Program Files\NSClient++\nsclient.ini"
Restart NSClient++ to make sure the new config file is used.
Using credential manager¶
Using credential manager is easy you simply set:
[/settings]
; use credential manager - Store sensitive keys in use credential manager instead of ini file
use credential manager = true
And then save the config.
nscp service --restart
Now here is the first catch, credential manager is per account so likely you are not running NSClient++ as the same user as the one you are logged in as. Thus you can either need to first switch to that user or run the command as that user. The default account use is local system and to switch to local system you can use PsExec like this:
PsExec -i -s "c:\program files\nsclient++\nscp" settings --update
If you no open the ini file it will look like this:
[/settings/default]
; Password - Password used to authenticate against server
password = $CRED$; Se credential manager: NSClient++-/settings/default.password
Restart NSClient++ and you are good to go.
To restore passwords you can do the reverse:
PsExec -i -s "c:\program files\nsclient++\nscp" settings --path /settings --key "use credential manager" --set false
PsExec -i -s "c:\program files\nsclient++\nscp" settings --update
Permission policy¶
NSClient++ has a core-level permission layer that decides whether a given caller may run a given command. It is disabled by default for backwards compatibility, but in any deployment that exposes more than one transport — or that wants to limit what individual web users can do — enabling it is strongly recommended and is the single biggest hardening step you can take after TLS.
The rest of this page focuses on transport-level controls (who can connect, with what credential). The permission policy is the next layer: once the caller is authenticated, what commands are they allowed to run? Without it, any caller that can reach a transport can invoke every command that module exposes.
When enabled, the policy is a strict allow-list — calls that don’t match any rule are denied. Rules name a subject
(the calling module, optionally with a principal such as the authenticated web user) and a list of objects (the
module.command patterns they may invoke). Identity is stamped server-side by the trusted core; modules cannot forge
it. CheckHelpers forwards the original caller through wrap-and-dispatch commands so policies apply through proxy
chains, not just at the outermost hop.
A minimal hardened policy:
[/settings/permissions]
enabled = true
log denials = true
[/settings/permissions/policies]
; NRPE may run a small fixed set of checks.
NRPEServer = CheckHelpers.*, CheckSystem.check_cpu, CheckSystem.check_drivesize, CheckDisk.check_drivesize
; Scheduler runs whatever the operator configures locally.
Scheduler = *
; Web admin can do anything; web monitoring user can only run a fixed set.
WEBServer : admin = *
WEBServer : monitor = CheckSystem.check_cpu, CheckSystem.check_drivesize, CheckDisk.check_drivesize
Recommended rollout¶
- Enable in observe mode first: set
enabled = true, add a single permissive rule* = *, and setlog allows = true. NSClient++ now logs every call without blocking anything — use this to inventory what actually runs. - Replace
* = *with rules that mirror the observed traffic. - Turn
log allowsback off (it is noisy) and keeplog denials = true. - Test through proxy commands such as
check_multiandcheck_always_okto confirm policies still apply to the wrapped command (they do —CheckHelpersforwards the original caller).
See Permissions for the full reference: identity model, which modules stamp what,
pattern syntax, the worked CheckHelpers example, and the detailed step-by-step setup guide.
Data disclosure: restricting what a check may read¶
Code execution is the risk people look for first, but a handful of checks take an argument which decides what data is
read, and the agent reads it with its own privileges - SYSTEM on Windows, root or nsclient on Linux:
| Check | Argument | What an unrestricted argument reaches |
|---|---|---|
check_logfile |
file= |
any file the agent can open; ${line} returns its contents |
check_wmi |
query= |
any WMI class, the filesystem included (CIM_DataFile, Win32_Directory) |
check_pdh / check_counter |
counter= |
any performance object on the machine |
check_files, check_single_file |
path= / file= |
any directory tree: every name, size and timestamp, plus a checksum of any file |
check_disk_write |
file= |
creates and deletes a test file at any writable path |
check_registry_key, check_registry_value |
key= |
any registry key, and the value data itself (binary as hex) |
check_eventlog |
file= / log= |
any event log channel, and the event text itself |
That is what those checks are for, so it is not a defect, and where only your configuration decides what runs it does
not matter. It matters where the caller picks the argument: NRPE with allow arguments = true, or the REST API. A
caller who can reach check_logfile with an arbitrary file= can read /etc/shadow, a private key or a registry hive
backup, and gets the contents back in the check output.
Each of these modules has an access mode which narrows this. They all default to any - the behaviour of every
release before 0.21.0 - so this is opt-in and an upgrade changes nothing:
| Check | Section | Mode setting | Allow list |
|---|---|---|---|
check_logfile |
[/settings/logfile] |
file access |
allowed files |
check_wmi |
[/settings/wmi] |
query access |
allowed classes, allowed namespaces |
check_pdh |
[/settings/system/windows] |
counter access |
allowed counters |
check_files, check_single_file, check_disk_write |
[/settings/disk] |
file access |
allowed files |
check_registry_key, check_registry_value |
[/settings/system/windows] |
registry access |
allowed registry keys |
check_eventlog |
[/settings/eventlog] |
log access |
allowed logs |
If you set only one of these, set registry access. check_registry_value returns value data with binary rendered as hex,
in its default syntax, and recursive=true walks a whole subtree — the registry is where autologon passwords, product keys
and stored connection settings live. check_eventlog is the next widest: event text comes back in its default syntax too, from
any channel the agent can read, Security included.
The disk checks never return file contents, but they enumerate whole trees and can report a checksum of any readable
file, which confirms known content and for a short file effectively recovers it. Narrower than check_logfile, but much
wider reach.
The modes are any (anything the caller names), allowed (only what matches the list) and predefined (only names you
configured). allowed is experimental: it parses and matches what the caller sent, and a parser is a place where the
gate and the operating system can disagree about what a string means. predefined only looks a name up, so it is the
secure option and the one to use wherever the data behind a check matters. Names you configure resolve in every
mode, so you can name your checks first, confirm the monitoring server still works, and tighten the mode afterwards:
[/settings/logfile]
file access = predefined
[/settings/logfile/files]
app = C:/logs/app.log
iis = C:/inetpub/logs/LogFiles/W3SVC1/u_ex.log
The monitoring server then runs check_logfile file=app, and a caller asking for anything else is refused.
Recommended posture. If no caller can pass arguments at all - allow arguments off for NRPE, and every web user on
the restricted role or the REST API not exposed - any costs you nothing; your configuration already decides
everything. Otherwise set predefined on whichever of the modules you have enabled;
allowed is the middle ground when you want a whole directory, key subtree or performance object without enumerating
each entry.
Refusing arguments is the other way to close this, and it is now available on both doors: allow arguments = false for
NRPE, and the restricted web role (queries.execute.noargs) for REST. Note what still
differs, because it decides whether you need an access mode as well: refusing arguments is set per transport, an access
mode is set per check. You have to remember both doors, and a third added later; an access mode covers every transport
at once. The concepts page has a
table comparing the four approaches - refusing arguments, allowing a
folder, allowing specific items, and predefined names only - with what each costs and where each falls short.
File paths are resolved before they are matched, so .. and symbolic links or junctions cannot widen an allowed
directory, and a misspelled mode is refused rather than ignored. The full reference - entry syntax, the WMI query forms
which can and cannot be checked by class, and what a refusal looks like - is in
Restricting what a check may read.
This is the companion to the permission policy above: that one restricts which checks a caller may run, this one restricts what those checks may reach.
Checks that connect somewhere else¶
A separate group of checks does not read this host at all - it connects to another one, with the destination and the credentials in the arguments:
| Check | Arguments | What an unrestricted argument decides |
|---|---|---|
check_wmi |
target=, user=, password=, namespace= |
which machine is queried over DCOM, and with which account |
check_mysql |
host=, port=, user=, password= |
which MySQL/MariaDB server is connected to, and with which account |
check_mssql |
the ODBC connection string | the driver, the server, the credentials and every other connection attribute |
check_uncpath |
path=, user=, password= |
which SMB share this host authenticates to |
Read these as what they are: remote-connection primitives, run from the agent’s network position and with the agent’s privileges. A caller who can pass arguments to them gets two things. First, server-side request forgery - the agent will connect to any host named, from inside whatever network it lives in, which on a monitoring host is often a better vantage point than wherever the caller sits. Second, an outbound authentication attempt this host makes on the caller’s behalf, with credentials the caller supplied.
Only check_wmi has a gate of its own: with query access set to anything but any, target= must name an entry in
[/settings/wmi/targets]. For the other three the argument controls are the whole answer, so:
- keep
allow arguments = falseon the NRPE, NSCA and check_nt listeners; - give REST users the
restrictedrole or a command allow-list rather thanqueries.executeat large; - put the host and the credentials in a command definition and let the caller name the definition - the difference between “run this check” and “connect wherever you like”.
Remote code execution: understanding the attack surface¶
NSClient++ is, by design, a remote-administration agent. Several modules can ultimately cause arbitrary code to run on the host. Whether that is acceptable depends on who can reach the agent, how they authenticate, and what is in the configuration. The risk lives at the intersection of those three — not in any single module.
CheckExternalScripts¶
This is the module most often flagged in security reviews because the name is self-describing. The short answer:
Enabling
CheckExternalScriptsdoes not let a remote caller run arbitrary commands. It only exposes scripts that an administrator has already declared in the configuration. A monitoring client cannot say “runevil.ps1” — they can only say “run the aliascheck_this”, and only if that alias points at something the local administrator wired up.
In detail, when the monitoring server asks the agent to run check_this, the agent will only run it if:
- There is an entry
check_this = <path> <args>under[/settings/external scripts/scripts](or/wrapped scripts, or/alias), and - The configured script lives under the configured
script root(default${scripts}, i.e. the install’sscripts\folder), and - If the request includes arguments,
allow arguments = trueis also set (defaultfalse), and - If those arguments contain shell metacharacters,
allow nasty characters = trueis also set (defaultfalse).
The default posture is that arguments and shell metacharacters are both rejected. Even with
allow arguments = true, the agent only substitutes arguments into the already-configured command line — the command
itself is not user-controllable.
This gate is per-transport. allow arguments covers callers arriving over NRPE; the equivalent for the REST API is the
restricted web role (queries.execute.noargs), which refuses any request carrying arguments. If you rely on
allow arguments = false for your NRPE clients, give your REST clients restricted rather than monitoring so the
same rule holds on both doors.
[/settings/external scripts]
allow arguments = false ; default; clients cannot pass extra args
allow nasty characters = false ; default; reject | & < > ` ' " \ [ ] { }
script root = ${scripts} ; scripts must live here
[/settings/external scripts/scripts]
check_this = scripts\my_script.ps1 ; declared by the admin; required for it to be callable
Disabling specific wrappings¶
Disabling wrapping will not really impact anything as they do not do anything unless a script with the given wrapping is
added. That said, they can be set to "" if you want to remove them.
Whether you want to do this depends on what you actually run. If your monitoring playbooks only use PowerShell, removing
the bat and vbs wrappings might theoretically shrinks the attack surface at zero cost. If you don’t use any shipped
wrapping at all, you can clear ps1 too — [/wrapped scripts] entries with no matching wrapping will simply fail to
execute.
Direct script entries under [/settings/external scripts/scripts/scripts] are unaffected by clearing the wrappings
map — those use the literal command line you declared.
Aliases — also available in CheckHelpers¶
The alias mechanism that lets you build composite commands like
[/settings/external scripts/alias]
my_check_cpu = check_cpu warn=load>87% crit=load>92%
These commands are internal and cannot execute scripts (unless they are an alias for another script definition).
So using alias by themselves does not impact security.
Historically aliases were avalible via CheckExternalScripts since 0.12.4 CheckHelpers also provide the same
functionalty under its own independent section [/settings/check helpers/alias]:
[/settings/check helpers/alias]
my_check_cpu = check_cpu warn=load>87% crit=load>92%
The two sections are independent and you can use either one where CheckHelpers can be used without enabling
ExternalScripts.
The reason this matters for security: CheckHelpers only runs internal commands. It has no script path, no wrappings,
no allow arguments flag, no shell-execution code. Its attack surface is meaningfully smaller than
CheckExternalScripts. For environments where CheckExternalScripts is not apoproved, this is the recommended home
for aliases.
Aliases call internal commands (check_cpu etc.) with fixed arguments, which is how they avoid needing
allow arguments = true — the arguments are baked into the alias definition, not supplied by the client. $ARG1$ /
%ARG1% substitution into the alias’s pre-declared argument list is supported, but the alias’s command name and
template are not caller-controllable.
Practical guidance:
- Want aliases only? Enable
CheckHelpers, disableCheckExternalScripts, define aliases under[/settings/check helpers/alias]. Copy across any existing aliases from[/settings/external scripts/alias]by hand. - Want aliases plus external scripts? Enable both. Each module reads its own section; pick one as the home for new aliases and stick with it to avoid duplicate definitions.
When is CheckExternalScripts actually risky?¶
The module becomes a real risk in these scenarios — none of which are about the module itself:
- Anyone with write access to
nsclient.inican declare new commands. Treat the INI as a privileged file; lock its NTFS ACLs to the service user and administrators only. See “Storing config in user profile” above. - Anyone with write access to the
scripts\folder can change what runs even for pre-declared commands. Lock that folder the same way. - The admin password can give equivalent power via the WEB module’s script-upload endpoint (see the WEB section
below). If you want WEB ui for monitoring but not for administration, set
disable admin user = trueunder[/settings/WEB/server]— the admin account is then never created or activated, and an attacker who recovers the password hash cannot use it to log in. See Disabling the admin user below. allow arguments = true+ a script that doesn’t validate its input is the classic injection vector. Leaveallow arguments = falseunless there’s a concrete reason to enable it, and never combine withallow nasty characters = trueunless you fully trust both the calling monitoring server and every script the agent might invoke.
Enabling the permission policy is an effective additional layer here: even if a caller can reach
the transport, the policy decides whether CheckExternalScripts.* is callable from that subject at all, and you can
limit specific subjects to a small fixed set of script aliases.
WEB module¶
The WEB module serves two distinct purposes, and the security guidance depends on which one you actually need:
- As a monitoring endpoint — exposing queries, metrics, and the OpenMetrics scrape target over HTTPS. No host-changing operations required.
- As an administration UI — letting an authenticated operator reconfigure the agent, including uploading new scripts
that get executed by
CheckExternalScripts/PythonScript(thescripts_controllerendpoint atPUT /api/v1/scripts/{runtime}/{name}).
In monitoring-only deployments — by far the more common use — you can get the first set of capabilities without the second.
Recommended for monitoring-only deployments¶
The fastest way to set this up is the WEB module’s own install command, with the --disable-admin flag:
$ nscp web install ^
--allowed-hosts 10.0.0.0/24 ^
--certificate nsclient.pem ^
--certificate-key nsclient.key ^
--disable-admin
$ nscp web add-user monitoring ^
--role monitoring ^
--password "$(openssl rand -base64 32)"
Swap --role monitoring for --role restricted if the monitoring server only
needs to run the checks this agent defines: the restricted role refuses any
request that carries arguments, which is the REST equivalent of NRPE’s
allow arguments = false. Checks that do need arguments are then defined as
aliases on the agent, so the arguments live in your configuration rather than in
whatever the caller sends. A caller that only scrapes metrics — a Prometheus
server — wants --role metrics, which reads the metrics endpoints and cannot
run checks at all.
With --disable-admin, the install command does three things differently:
- Sets
disable admin user = trueunder[/settings/WEB/server]. - Skips writing the per-user
adminrow. - Creates a
monitoringuser with rolemonitoringand the supplied (or generated) password.
The equivalent INI written by hand:
[/settings/WEB/server]
disable admin user = true
[/settings/WEB/server/users/monitoring]
role = monitoring
password = <hash>
The monitoring role is registered by the WEB module at startup and grants only
public,queries.execute,aliases.list,login.get,metrics.list,openmetrics.list — enough for a monitoring server to log
in, run queries and scrape metrics, and nothing else. No settings.*, no modules.*, no scripts.*. If you need more
(e.g. the legacy check_nscp_api integration that lists queries), prefer the client role over full. If you need
less, the restricted role (public,queries.execute.noargs,aliases.list,login.get) runs the same checks but refuses
any request carrying arguments, and the metrics role (public,metrics.list,openmetrics.list,login.get) only reads
the metrics endpoints.
With disable admin user = true, the agent never creates or activates the admin account. The script-upload path is
still wired up in the code, but no account can authenticate to it — so even an attacker who recovers the
/settings/default/password cannot turn it into remote code execution. See Disabling the admin user below for what
the flag does and doesn’t do.
Apply the usual transport / network hygiene on top:
- Use a real TLS certificate (do not let the agent silently fall back to HTTP on port 8080).
- Firewall the WEB port (
8443) to your monitoring network only. - The
/settings/default/passwordis stored hashed oncenscp web installornscp web password --sethas written it; move the remaining clear-text secrets into the credential manager (see the Passwords section above).
When you actually need administration over WEB¶
If you genuinely want to administer the agent through the web UI — push config changes, upload scripts, restart modules — keep the admin user enabled and treat its password the way you would a local-administrator credential on the box:
- Rotate it on a defined schedule.
- Store the hash in the credential manager, not the INI.
- Restrict source IPs to a small set of administration jump hosts.
- Pair with reverse-proxy authentication (mTLS, SSO header, etc.) for defence in depth — the agent’s own auth becomes the second factor.
The honest framing is: anyone with that password can run arbitrary code as LocalSystem. Manage it accordingly, or
remove the capability with the flag above.
When you don’t need WEB at all¶
NRPE / NSCA / Graphite / check_mk monitoring flows do not require the WEB module. If your monitoring server doesn’t poll
the REST API and you don’t use the web UI, don’t load WEBServer. The smallest attack surface is one that doesn’t
exist.
NRPE without two-way TLS¶
NRPE is the most common attack surface because it is the most commonly exposed. The default configuration is *
TLS-encrypted but client-unauthenticated — the agent verifies its own identity to the caller (via DH or the agent
certificate) but does not verify the caller’s identity*. Combined with allow arguments = true, this means:
Anyone who can reach TCP/5666 can invoke any command the agent is configured to expose, with whatever arguments they choose (subject to
allow nasty characters).
allowed hosts is network filtering, not authentication. It checks the source IP, which is trivially spoofable on
untrusted networks and trivially bypassable from any host inside the allowed range.
The robust answer is two-way TLS, where the agent verifies the client’s certificate against a CA you control. See the NRPE section earlier on this page for the command-line setup.
If two-way TLS is not yet in place, the compensating controls are:
- Keep
allow arguments = falseso callers can only invoke commands as declared, with the arguments declared. - Use aliases (above) for any check that needs varying thresholds, instead of accepting arguments from the network.
- Restrict the network path to the agent (host firewall, dedicated VLAN, jump host).
- Treat
allowed hostsas a defence-in-depth aid, not a security control. - Enable the permission policy and restrict
NRPEServerto the exact list of commands the monitoring server actually invokes — this caps the blast radius even if a caller bypasses transport controls.
Quick checklist¶
| Concern | Default | Recommended posture |
|---|---|---|
CheckExternalScripts allow arguments |
false |
leave false unless you have a specific need |
CheckExternalScripts allow nasty characters |
false |
leave false |
Unused wrappings (bat, vbs, etc.) |
shipped | clear (bat =) if you don’t use them |
nsclient.ini ACLs |
install dir | lock to service user + admins; or move under systemprofile |
scripts\ folder ACLs |
install dir | lock to service user + admins |
| Admin password storage | plain in INI | move to credential manager |
NRPE verify mode |
none |
peer-cert with a CA you control |
NRPE allowed hosts |
broad | tighten, but do not rely on it as the only control |
WEBServer module |
optional | leave disabled if not actively used; tight ACLs and TLS if used |
WEBServer disable admin user |
false |
true if the WEB module is used only for monitoring, never for admin |
check_nt (NSClientServer) protocol |
optional | avoid; leave disabled. If required, firewall to the monitor, treat the password as public, and set allow = metrics, info |
| Service account | LocalSystem |
dedicated low-privilege account with only the access your checks require |
Permission policy (/settings/permissions) |
disabled | enable in observe mode, lock down to per-subject allow-list |
check_logfile file access |
any |
predefined (or allowed) wherever callers may pass arguments |
check_wmi query access |
any |
predefined (or allowed) wherever callers may pass arguments |
check_pdh counter access |
any |
predefined (or allowed) wherever callers may pass arguments |
CheckDisk file access |
any |
predefined (or allowed) wherever callers may pass arguments |
check_registry_* registry access |
any |
predefined (or allowed) wherever callers may pass arguments |
check_eventlog log access |
any |
predefined (or allowed) wherever callers may pass arguments |
GearmanClient encryption / key |
on / empty | always set a key; never encryption = false outside a lab |
GearmanClient mode |
agent |
keep agent unless a proxy is needed; treat a proxy as a privileged host |
GearmanClient allow shared queues |
false |
leave false — the generic queues carry every host’s checks |