Upgrading¶
What to do when upgrading NSClient++, newest release first. Most upgrades are in place โ defaults are preserved and the default install is usually unaffected โ but the items below change observable behaviour or want a configuration touch. Read the entries between the version you are on and the version you are moving to.
Each entry carries an icon for the area it touches. ๐ is the one that matters: those entries are security-relevant, and the Security notices page tracks them in one place. An entry badged Action required needs a change on every installation that runs the module; one badged Check your setup only if you use the feature it describes; the rest are informational. Full per-release detail lives in each GitHub release.
Use the filter below to narrow the list to your setup: pick the version you
are coming from and tick the modules your nsclient.ini enables, and only the
entries that concern that installation stay visible. The selection is shared
with the Security notices page, remembered in your browser and reflected in
the page address, so a filtered view can be bookmarked or shared.
Modules: all
Tick the modules your nsclient.ini enables. Entries that touch none of the ticked modules are hidden; service covers the agent itself, which every installation runs. The selection is shared between the Upgrading and Security notices pages.
0.25.0¶
- ๐ The bundled OpenSSL is updated from 3.5.8 to 3.5.9 in the Windows
builds, picking up the thirteen fixes of the 29 September 2026 upstream
security release on the 3.5 LTS line. The only one on a code path
NSClient++ calls is
CVE-2026-35189, a
memory-pressure denial of service from a crafted peer certificate; the rest
are in DTLS, QUIC, CMP and SM2 code the agent never uses. The update also
clears the version-based finding that vulnerability scanners raise against
libssl-3-x64.dll/libcrypto-3-x64.dll3.5.8 (#1665). No configuration change is needed. The Linux packages link the distribution’s OpenSSL and are unaffected; the macOS tarball carries the Homebrewopenssl@3current at build time. See the security notice.
- ๐ Listing scripts over REST no longer runs every check. Nothing to do.
GET /api/v2/scripts/luaandGET /api/v2/scripts/py(andnscp lua list --query/nscp py list --querybehind them) asked the core for each query’s parameters, which it collects by running the query - so listing the scripts ran every check on the agent once, each Lua and Python handler included, side effects and all. They now ask for the names only.GET /api/v2/queries/<name>/helpfor a script’s query no longer runs the handler either. See the security notice.
- ๐ LUAScript: a Lua check that runs twice at once no longer crashes the agent.
Nothing to do. Two concurrent calls into the same Lua script, where the first
was parked in
nscp.sleepor aCorecall, shared one Lua stack and could terminatenscp; that needed only two overlapping NRPE requests for such a check, or a scheduler running it on two threads. Every call now runs on a Lua thread of its own. See the security notice.
-
๐ง ๐ Check your setup LUAScript: one broken script no longer takes the others down, and the documented API works as written. Nothing to do on a default install. A test of every documented Lua API turned up these, now fixed; check your scripts against the ones marked check:
- A script that did not parse, or whose top-level code raised an error, failed the whole module: the
core reported
Plugin refused to load: LUAScriptand no Lua script on the host ran. The error is now logged and the other scripts load. Anon_startthat raises no longer stops the scripts after it from starting. - An
on_start- or top-level code - that called the Core API (Core():simple_query(...)and the like) left the Lua lock held for good, and every Lua check after it waited forever. A script whose top-level code raised is no longer started: itson_startdoes not run. - Handlers registered with
Registry:simple_cmdlinenever ran - neither fromnscp client --module LUAScript --exec <name>nor fromCore:simple_exec. They now do. - A reload of LUAScript now runs
on_startagain for the freshly loaded scripts (it ran only once per agent lifetime), and takes back the queries and channels of a script that was removed. Check a script whoseon_startmust only ever run once. A handler registered withsimple_cmdlineunder one of the module’s own verbs (help,execute,add,list, …) is never reached; Lua scripting lists them. - Check:
Settings:get_boolreadstrue,1andyesastrue; it used to read the value as an integer, sotruecame back as the default.Settings:set_boolwritestrue/false. - Check:
Core:simple_execreturns"unknown"andFailed to execute <command> on <target>when nothing could run the command; it returned"warning"andCommand failed.. - Check:
Core:simple_submitreturnsfalsewith the handler’s message when a handler rejects the result; it returnedtruewhenever the channel had a listener. On a channel nobody listens to it returnsfalseandFailed to submit message: <channel>, and an answer from several listeners no longer raises a Lua error. - Lua checks kept answering
Unknown commandfor as long as a reload of LUAScript took: their registrations were withdrawn before the reloaded scripts made them again. They stay registered now, and a check that arrives during the reload waits for it. mainthat returns only a status no longer printsNILfor the missing message.- A check handler that returns no status answers
unknownwithInvalid return from <command>: expected (code, message, perf); a missing message or performance data is empty, where it used to readNIL. A numeric status outside0-3reads asunknown. - A call into the API with one argument too few raises
Incorrect syntax: ...; the check counted the object itself as an argument, so the call went ahead with the arguments shifted by one. - A subscription handler that raises fails the submission with
Failed to handle channel: <channel>: <error>rather than with no answer at all. Core:exec,Core:submit,Registry:cmdlineandRegistry:subscriptionare still not implemented; the error they raise now names the call (Unsupported API called: Core:exec).nscp lua show --script <name>prints the script andnscp lua delete --script <name>deletes it and removes every/settings/lua/scriptsentry that loads it - for a symlink, the entries that load the link, never those of the file it points at. Both answered with nothing before, soGETandDELETEon/api/v2/scripts/lua/<name>returned an empty200and changed nothing. They act on files under${scripts}/luaonly, symlinks resolved. Review custom roles that holdscripts.get.LUAScript,scripts.delete.LUAScriptorscripts.*; see the security notice.nscp lua add --importasked for--overwritewhen the file already existed; the option is--replace.nscp luawith no verb printed thenscp pyusage.
Lua scripting now documents
Core:query_targetandCore:query_forward, what each handler answers when it fails, and the calls that are not implemented. - A script that did not parse, or whose top-level code raised an error, failed the whole module: the
core reported
-
๐ง ๐ Check your setup PythonScript:
plugin_alias,Registry.event, and showing or deleting a script. Nothing to do on a default install. A test of every documented scripting API turned up these, now fixed:init()receivespythonasplugin_aliaswhen the module is loaded without an alias (PythonScript = enabled), as Python scripting documents. It used to receive an empty string. Check your scripts if one comparesplugin_aliaswith""or builds a settings path from it.- A script that registered a handler with
Registry.eventcrashed the agent on the first event it subscribed to.Registry.event_pbhandlers never ran at all; they now run, once per message, and receive it asbytes. - A
Registry.simple_subscriptionhandler is given the submission’s source (for examplecheck_and_forward’ssource=); it used to receive an empty string unless the result carried one of its own. Core.simple_execreports a call nothing could run - an unknown module, or a command the module does not have - as(status.UNKNOWN, ["Failed to execute <command> on <target>"]). It used to return(0, []), which reads as success, or(False, <text>)when the call threw. Check your scripts if one tests the code for0orFalse. The docs also wrongly offered"local"as a target; use a module name,"any", or"all".nscp py add --importasked for--overwritewhen the file already existed. The option is--replace, and the message now says so.nscp py show --script <name>prints the script andnscp py delete --script <name>deletes it and removes every/settings/python/scriptsentry that loads it, however the entry is written - for a symlink, the entries that load the link, never those of the file it points at. Both answered with nothing before, soGETandDELETEon/api/v2/scripts/py/<name>returned an empty200and changed nothing. They now act, on files under${scripts}/pythononly, symlinks resolved. Review custom roles that holdscripts.get.PythonScript,scripts.delete.PythonScriptorscripts.*if those grants were not meant to read or remove scripts; see the security notice.
- ๐ Check your setup A permission policy reload that fails part-way now denies exec too, and a failure to register the settings keys no longer counts.
Nothing to do unless
[/settings/permissions] enabled = true. A reload that threw part-way used to leave whatever it had got to: usually an emptied rule table, withenabledandallow execkept as they were unless already re-read, or the whole previous table when the failure came before the reload started (registering the settings keys). Now the rules read before the failure are enforced, every other call is denied, and exec is denied unlessallow execwas read, until a later reload loads the policy completely. A failure beforeenabledis read leaves the policy on or off as it was, so a host that never enabled it is unaffected; at boot that means the policy stays off until a reload reads it. Registering the settings keys (documentation metadata) can no longer fail the load. Look forpermissions: failed to loadin the log for the cause. See the security notice.
- ๐ Check your setup The docs now name the web grants, the installer property and the check
keyword that hand out more than they look like, and the dead root
/metricsroute is gone. See the security notice. Nothing changes on a running agent. Check your setup if you installed withUSER_WRITABLE_CONFIG=1(every local user can then run code asSYSTEM), or if a web role carriessettings.put,settings.delete,console.exec,modules.postorscripts.add.*. A client that requestedGET /metricsreceived the web UI’s HTML before and still does; use/api/v2/metrics.
- ๐ NRPE escapes the command and arguments in its log lines, check_nt reports a missing
password once, and the About page only links to
http(s)homepages. Three small hardening fixes from the source review; see the security notice. Nothing to do: a control character in an NRPE command or its arguments now shows up in the log as\xNNinstead of a line break, the “no password configured” error for check_nt is logged when the module loads instead of on every request, and a bundled web dependency whose homepage is not anhttp(s)URL is linked to its npmjs.com page instead.
-
๐ Check your setup Undefined-behaviour sweep: overflowing
convert()/neg()values are errors, and multi-letter units scale the same everywhere. From the security notice; nothing to do unless you relied on one of these:- A
convert()orneg()whose result does not fit 64 bits (size > convert('9000000', 't'),written > convert(1.5e17, 'w')) is reported as an evaluation error on the check instead of comparing against a wrapped or unspecified value. convert()reads its unit from the first letter for whole numbers too, as it already did for fractions:convert(2, 'min')is 120 seconds andconvert(2, 'kb')is 2048 bytes. Both used to leave the number unscaled.- On Linux,
check_processreportserror=Cannot read statmfor a process that exits while it is being read, instead of a random memory size.
- A
-
๐ Undefined-behaviour sweep: whitespace-only perfdata parses as empty,
filter_perfsorts non-numeric entries last, and out-of-range numbers saturate. From the security notice; nothing to do, but three results can differ from before:- Performance data consisting only of spaces no longer fails the check with an “out of range” error; it is treated as no performance data.
filter_perf sort=normalandsort=reversenow place every entry without a numeric value after the numeric ones, in every case. Mixed lists used to come out in an order that depended on where the non-numeric entries sat.- A floating-point keyword read as an integer, a
check_memory/check_pagefilepercentage threshold on Linux, and a Docker or Kubernetes JSON number beyond the 64-bit range now saturate at the integer limit instead of comparing against a wrapped or unspecified value.
-
๐ง Check your setup The agent now reports configuration it ignores, and
nscp settings --validateexits with 1 when it finds any. Nothing to do unless a script runsnscp settings --validate. A section written without its leading/([settings/external scripts]), a misspelled section or key, or a key above the first section is never read, and used to be ignored without a word - a script defined in such a section was simplyUnknown command(#647). The agent now logs a warning naming each one at startup and on every reload, the web UI shows a Configuration problems banner (and a Problems button on the Settings page), andGET /api/v2/settings/problemsreturns the list. Sections and keys that only a module which is not loaded would use are listed as info and raise nothing.nscp settings --validateprints the same report and now exits with1when it contains a warning; it used to exit with0whatever it found, and never reported a key at all. Add--load-allto have every installed module register its settings first, so anything still unclaimed is reported as a warning too.The check found one of its own: the core registered its section as
settingsrather than/settings, sonscp settings --updatewrote an empty[settings]into every file it touched. It no longer does; an existing empty[settings]is harmless, listed as info, and can be deleted.
-
โฑ๏ธ Collector-backed checks can choose the status they report while warming up. Nothing to do. Right after the agent or the module starts, the background collector has not taken its first sample yet, and
check_cpu,check_load(Windows) and, on Linux and macOS,check_memory,check_pagefileandcheck_networkanswer UNKNOWN “โฆ not available yet (collector still initializing)”. A newwarmup-stateoption (ok,warning,criticalorunknown, defaultunknown) picks that status instead, for instancewarmup-state=okto keep a restart from raising alerts; the message still says the collector is initializing. It only ever covers a collector that has not tried yet: one that is disabled, busy or failing stays UNKNOWN whatever the option says (see Collector-backed checks and warm-up).Telling those apart changes a few results even without the option. Each is still UNKNOWN unless noted, only with a message that says what is wrong:
- Linux and macOS - a source the collector cannot read (an empty or
unreadable
/proc/stat,/proc/meminfoor/proc/net/devin a locked-down container) used to be stored as an all-zero sample, socheck_cpureported 0% load andcheck_memorya zero total as if measured.check_cpu,check_memory,check_pagefileandcheck_networknow answer UNKNOWN “the collector failed to sample it” with the reason, and each source is judged on its own, so an unreadable/proc/statdoes not affectcheck_memory. A source that stops being readable later is reported the same way after five failed samples in a row, instead of the averages of the samples taken before it failed being reported as current (Windowscheck_cputoo). - Windows
check_load-disable = loadnow reports “Load average sampling is disabled”, and a collector whose lock could not be had reports “the collector is busy”; both used to share the warm-up message. - Windows
check_cpu- a tick that failed to read the CPU load is no longer stored as an idle sample, so it cannot pull the averages down, anddisable = metricsno longer stops the CPU samples (check_cpuused to report “collector still initializing” for good with it set). Withuse pdh for cpuenabled, an explicitwarmup-stateis refused, as that source has no warm-up to report. - Both - an invalid
time=oncheck_cpuis reported right away instead of after the warm-up, and Linux and macOS now reject a zero window (time=0s) as Windows already did.check_cpu cores=trueis now accepted over REST, where it used to fail with “does not take any arguments”.
- Linux and macOS - a source the collector cannot read (an empty or
unreadable
-
๐ง Check your setup
kill treenow works on Linux, and unloading the module ends running scripts. The setting was accepted on Linux but did nothing: on a timeout only the script itself was signalled, so a helper it had backgrounded kept running and, because it still held the script’s output pipe, the check only came back when the timeout expired. Unloading the module did not touch running scripts at all. Now:- With
kill tree = truethe script runs in a session of its own (so also a process group of its own, with no controlling terminal), and the timeout’sSIGTERMandSIGKILLgo to the whole group: everything the script started dies with it. - Unloading the module kills every running script. With
kill tree = truethat includes its helpers. Without it only the script itself is killed: a helper it backgrounded keeps running, and the check waits for its timeout. - A script ended by an unload reports
was killed: the module is unloading, and one ended by a signal reports which (was terminated by signal 9 (SIGKILL)), instead of an emptyUNKNOWN. - If reading a script’s output fails, the script is now ended the same way
as on a timeout and reports
failed while its output was being read; killed. Before, the agent waited for it with no deadline. - Every script reads stdin from
/dev/null. Under the service that is what it already had; undernscp testa script no longer reads the terminal.
Check your scripts only if you set
kill tree = trueon Linux. A helper that must outlive its check has to leave the group (start it with thesetsidutility). A script that callssetsid()itself now getsEPERM, because it already leads its session; thesetsidutility forks first and is unaffected. - With
- โฑ๏ธ NRPE, NSCA, check_mk and NSClient servers: a request no longer waits for an unrelated slow check.
Nothing to do. Each connection serialised its work on an asio strand, and the
kind of strand the socket servers used shares a fixed pool of 193
implementations between all connections - so a new request could land on the
strand of one whose check was still running and wait for that check to finish
before it was even read. About one request in a few hundred did, whenever a
long-running check was in flight, which a monitoring server sees as an
occasional
Socket timeouton a check that normally answers at once. The accept loop could be held up the same way. Every connection, and the accept loop, now has a strand of its own.
- โฑ๏ธ Check your setup A reload starts modules once everything it loads is loaded. Nothing to do unless a Lua
on_startor a Schedulerrun on startupschedule depends on when a reload runs it. A module’s start hook - LUAScript’son_start, Scheduler’s run-on-startup schedules - is now run after a reload the way it is at boot: once every module the reload loads is loaded. On a reload of the service they used to run from inside the module’s own reload, before any module the same reload enabled, so anon_startor a startup schedule querying such a module gotUnknown command. A module that a reload of the service loads for the first time is now started too; it used to be loaded and never started, so its start hook did not run until the next restart.
- ๐ The log is written as it happens, and log handlers are fed from one
thread. Nothing to do. The core used to pick one of three log backends
(
console,file,threaded-file); it now has a single logger that writes the console and the log file directly, so a line is on disk before the call that logged it returns, and the last lines before a crash are no longer lost in a queue. Log-handler modules (the web UI’s live log,check_nscp’s error count, ElasticClient, scripts) are fed from one background thread in every mode, so a slow handler holds up no thread that logs - before, innscp testandnscp client, every log call waited for every handler.--log-backend fileand--log-backend threaded-fileboth still turn on the log file, so installed service units keep working unchanged;filenow behaves exactly likethreaded-file, which means it also writes the console and feeds the log handlers, where the oldfilebackend did neither. The file is still opened and closed for every line, so log shippers and rotation tools never find it locked. Withmax sizeset, the cut back to the newest 70% now runs on the thread that crossed the limit, which waits for it. At most 10 000 lines wait for the log handlers; when a handler falls that far behind, the oldest lines are dropped for the handlers only (never for the console or the file), andnsclient.fatalrecords when that starts and how many were dropped.
-
๐ง Unloading a module waits for the calls still inside it, and can be refused. Nothing to do. Unloading a module - from the web UI, over REST, or from a script - now waits for a log line it is handling, and for a metrics or facts round that is inside it, before the module is torn down, as it already waited for checks in flight. Only calls inside that module are waited for: a stuck handler in one module does not hold up the unload of another. A call that is still inside after the wait refuses the unload with
Refused to unload <module>: a log line is still being handled by it after 5 sorโฆ: a metrics or facts round is still running inside it after 10 s; the module stays loaded and serving, in its place, and the unload can be retried. A module whose reload failed while such a call was still inside it is taken out of service and unloaded at shutdown instead, withRemoving <module> after its failed reload; it is unloaded at shutdown, โฆin the log; at shutdown a module a call is still inside is left loaded rather than torn down under it, withLeaving <module> loaded at shutdown: โฆ.A line a log-handler module writes from inside its handler is no longer handed to any log handler, so a handler that logs once per line it receives no longer feeds itself or another handler; it still reaches the console and the log file. A module that hands lines to a thread of its own and logs from there is not covered, and has to filter its own lines by sender, as ElasticClient and DotnetPlugins do.
- ๐ง Check your setup A fleet server’s host override now wins over the host’s bundles. The
desired state carries a host-specific override alongside the bundle list. The
agent used to merge the override first and every bundle’s
config.jsonon top of it, so a key that both set took the bundle’s value and the override only applied to keys no bundle touched. The agent now merges the override last, and says so in every state report (host_override_last), so the fleet server can tell which hosts apply it. If you have a host override that sets a key a bundle also sets, the host will switch to the override’s value when it updates. Check your host overrides before upgrading if you relied on a bundle winning.
-
๐ง Check your setup A rejected connection now logs one line that says why and where to fix it. When an address is not in
allowed hosts, the server logs which address was rejected, what the list holds and which settings section sets it:Rejected connection from: 192.168.5.7, not in allowed hosts (127.0.0.1); set `allowed hosts` in /settings/NRPE/server, or in /settings/default if that section does not set itNothing to do unless you match these lines in a log monitor or a fail2ban-style filter. The
Rejected connection from: <address>prefix is unchanged, but the separateIP address not allowedandallowed_hosts is empty - rejecting all connections ...lines are gone (their content is now part of the one line), and an IPv4 client on a dual-stack listener is logged by its IPv4 address (10.1.2.3) rather than as::ffff:10.1.2.3. See the FAQ.
- ๐ง Check your setup An alias containing
\nis now refused instead of getting a newline. Alias definitions treat\as an escape character, and only\\,\"and\<space>(a backslash before a space) are escapes. Every other backslash already made NSClient++ refuse the alias, except\n, which was turned into a real newline:"path=C:\nscp"silently becameC:+ newline +scp. Those aliases are now refused too, and the log line (Refusing alias 'โฆ' - command line did not parse cleanly) names the argument and the fix. If an alias uses\non purpose, for example"list-separator=\n", write it as"list-separator=\\n"; write Windows paths with/or doubled backslashes. After upgrading, check the log for refused aliases.
-
๐ Check your setup PDH checks retry a briefly missing counter instance, and
check_pdherrors name the counter. A counter whose instance does not exist at the moment of the sample (typically a\PhysicalDisk(...)instance whose name changed, or a RAID volume that dropped out for a moment) madecheck_pdhgo UNKNOWN straight away withFailed to add counter: PdhCollectQueryData failed: : 800007d5: ...(#648). Now:check_pdh(also withaverages=true) takes one more sample a second later before reporting a missing instance. The state is still UNKNOWN when it stays missing. The background collector behindcheck_cpuand named counters does not retry; its next tick is the next sample.check_pdhreports a failure after the counters were added asFailed to poll counter: No data for <counter path> (does the instance exist right now?): PDH 0x800007D5: ..., and a failure to open the query or add a counter to it asFailed to open counter query: .... A counter path that cannot be resolved at all still reportsFailed to add counter.ignore-errors=truenow does what it says forcheck_pdh: a counter with no value this tick is skipped instead of failing the check, and the missing instance is not retried. Before, onlyinstances=trueskipped failing counters, andignore-errorsonly hid the failure of the whole check.
Check your setup only if you alert on, or filter, the text
Failed to add counter: matchFailed to poll counterandFailed to open counter queryas well.
- ๐ง check_nt’s “No performance data” error now says why. When a check behind a
check_nt request (
CPULOAD,UPTIME,MEMUSE,USEDDISKSPACE,COUNTER,FILEAGE) answers without a value, the reply used to be justERROR: No performance data from command: check_cpu. It now carries the check’s own explanation, e.g.ERROR: No performance data from command: check_cpu: Failed to read CPU data: the collector is busy (timed out waiting for its lock). A monitoring rule that matches the old text exactly needs to match the prefix instead. The same sanitising now applies to every message check_nt embeds in a reply:&(the field separator) becomes+and line breaks become a single space in theFILEAGE,SERVICESTATEandPROCSTATEreplies too, so a file name or service name containing&no longer splits the reply into the wrong fields.
- ๐ง Check your setup
check_os_versionno longer swaps Windows 7 and Server 2008 R2, and names Windows 11 25H2. Since 0.6.15 a Windows 7 machine was reported asWindows Server 2008 R2and a Server 2008 R2 machine asWindows 7(in the check’s message and in theos_namesystem fact). Both are now named correctly, and Windows 11 build 26200 and later reportsWindows 11 25H2instead of24H2. If a filter or a downstream rule matches on these names, check it after upgrading.
-
๐ง Check your setup Lua
require()and CheckMKClient scripts resolve from${scripts}. Nothing to do on a default install.require()in a Lua script now searches${scripts}/lua/lib/?.luaand${scripts}/lua/?.lua, as Lua scripting always documented. It used to search${base-path}/scripts/lua/lib, which is the same folder on a default Windows install but not on a Linux package, where${base-path}is the directory holding the binary: thererequire()of a shared helper inscripts/lua/libcould not load before. CheckMKServer searched${scripts}/scripts/lua/lib, and now uses the same two paths.CheckMKClient’s
scriptsentries are now looked up under${scripts}(${scripts}/lua/<entry>, then${scripts}/<entry>) instead of under the install base. An absolute path, or one relative to the working directory, is found as before. Check your setup if:- you override
${scripts}and keep Lua helpers under<install base>/scripts/lua/lib- move them to${scripts}/lua/lib; - a CheckMKClient
scriptsentry is a path relative to the install base, such asscripts/lua/my_check.lua- write it relative to${scripts}(my_check.lua) or as an absolute path. An entry that is not found is now logged asFailed to find script: <entry>; it used to be skipped silently.
- you override
- ๐ง
nscp lua listandnscp py listlist the scripts on Linux packages. Nothing to do. Both left out every file whose path containedlibanywhere, meant for helper folders - and on a DEB or RPM install${scripts}is/usr/lib/nsclient/scripts, so the listing, andGET /api/v2/scripts/lua?all=true//api/v2/scripts/py?all=truebehind it, was always empty. Only a folder namedlibbelow${scripts}/luaor${scripts}/pythonis left out now, and--include-lib, which was accepted but did nothing, lists those helpers too.
- ๐ค A request that selects a configured target sends its payload once. Nothing
to do. Since 0.20.0, a client command run as a query (over REST, NRPE or from a
script) with
target=<name>naming a configured target sent everything the request built twice:nrpe_query target=x command=check_foo argument=areached the remote host ascheck_foo!a!a,submit_*sent its message as two lines, and everybatch=record was submitted twice. The--targetoption on the command line had the same problem for exec commands. Each now goes out once. A remote check that took the repeated arguments in its stride needs nothing; one that failed on them, or a passive check that showed every batched result twice, now behaves as configured.
- ๐ค Each event record is delivered once. Nothing to do. An event message can
carry several records - a real-time CPU filter sends one per core - and the
core handed the whole message to each subscriber once for every record in it.
Every subscriber walks all the records itself, so a message of N records
arrived N times over:
/api/v2/eventsstored each record N times, ElasticClient sent N copies of each document, and a script’sRegistry.eventhandler saw each record N times. Each subscriber now gets each message once. Expect fewer events and documents where real-time filters match more than one record at a time; the ones that remain are the real ones.
0.24.1¶
- ๐ ๐ฆ Check your setup Windows: the installer no longer ships
NSCANgClient. A few antivirus engines reportNSCANgClient.dllas malware with generic machine-learning verdicts, which made the whole MSI look infected. The detections are likely false positives but unconfirmed until the vendors have analysed the file, so the module now ships in the Windows zip only. An MSI upgrade removes the copy an earlier installer put there, and a service configured withNSCANgClient = enabledthen logs that the module was not found. If you submit results over NSCA-NG from Windows, copymodules\NSCANgClient.dllfrom the zip of the same version and platform into the installation’smodulesfolder after upgrading, and again after every later upgrade: the installer does not own a file copied this way, so it never replaces it. The steps are on the NSCANgClient reference page. The Linux and macOS packages are unchanged. See the security notice.
0.24.0¶
-
๐ ๐ง Check your setup Three new limits on what one request may cost. Nothing to do on a default install; each is far above anything a real check does.
Limit Value What happens past it Command nesting depth 16 The query is refused with a structured error. A check that runs other checks ( check_multi,check_and_forward,check_timeout) re-enters the core on the same thread, and an unbounded nesting exhausted the thread stack โ which is not a catchable failure on Windows.check_multicommands per call128 The check returns UNKNOWN naming the count. Regular-expression matching per check 30 s total, 1 MiB per subject Further matches are refused and reported, so the check comes back UNKNOWN rather than quietly under-matching. A pattern that backtracks catastrophically no longer pins a worker thread for the length of a large event log. Compiled patterns are now cached, so ordinary filters get faster. check_logfilealso gained amax-sizeoption, default64m, bounding what one call reads into memory:- With a bookmark nothing is lost โ the check consumes up to that much per run, the position advances over what it read, and the next run continues, so a large backlog is worked through over several checks.
- Without a bookmark there is no position to resume from, so a file over the
limit is reported as UNKNOWN rather than silently half-read. Add
bookmark=auto(which is what you want anyway: it reports each line once), addmax-lines, or raisemax-size.
See the security notice.
- ๐ Check your setup The bundle signing key cannot rotate without the old key’s endorsement. A
renewal response that changes
bundle_signing_pub_pemmust also carrybundle_signing_pub_sig, a detached Ed25519 signature over the new key made with the old one; without it the rotation is refused and the existing key is kept, so re-enroll the host instead. Only the key is refused: the certificate, the CA and the server pin in the same response are still taken, so the host keeps renewing and the refusal appears in the log as an error rather than as a renewal failure. Keys are compared as keys, so a server that re-serialises the same key (different line wrapping, a trailing newline) is not mistaken for a rotation. A host whose state carries no signing key at all does not get one from a renewal either โ a key is adopted at enrollment, so re-enroll such a host. The fleet guide gained a section stating what a fleet server can do to an enrolled host, which is worth reading once: the managed configuration is an ordinary settings include, so a fleet server is an administrator of every host enrolled with it. See the security notice.
-
๐ Check your setup The shared
/settings/default/passwordis stored hashed whennscp web installornscp web password --setwrites it, andNSCAServerno longer reads that section at all. Nothing to do on a default install: the WEB admin seed and the check_nt server verify a password against either form, and a clear-text value you wrote by hand keeps working until you re-set it (nscp web password --set <the same value>hashes an existing password in place).Which direction of NSCA you run decides whether the second half touches you at all. Sending passive results from this agent to an NSCA daemon โ
NSCAClient, which is how NSCA is used in almost every installation โ is unaffected: its key has always lived on the client target (/settings/NSCA/client/targets/default/password) and was never read from the shared section. OnlyNSCAServer, the rarely used listener that lets this agent receive NSCA submissions from somewhere else, ever inherited the shared password. If you run that listener and its key came from the shared default, you do have to move it โ see the second point. Three things change:nscp web password --displaycannot show a hashed password. The clear text is printed once, by the command that set it; if it is lost, set a new one.--setnow also updates theadminuser’s row, which is what the web login verifies against once the server has booted;--only-webchanges that row alone.NSCAServerโ the listener, not the client โ no longer reads/settings/default/password. That section is the password inbound protocols (the web UI and check_nt) verify a caller against, and it is now hashed; NSCA does not verify a password, it encrypts with it, so it could never use a hash. It is not inherited from anywhere else either โ not fromNSCAClient, whose key is what this agent submits to a remote daemon with. If you runNSCAServerand its key came from the shared default, set it under its own section before upgrading โ otherwise the module refuses to start, loggingRefusing to start NSCA server: encryption is enabled โฆ but no password is set. Only a start refuses:nscp settings,nscp client --module NSCAServerand the documentation build still load the module without its listener, so the section can be listed and corrected in place:
[/settings/NSCA/server] password = <the NSCA key>or, equivalently:
nscp nsca install --server --password <the key every submitting host uses>A host that also submits NSCA configures that separately, because the two keys are shared with different peers โ the listening key with the hosts submitting here, the client key with the daemon this agent submits to:
nscp nsca install --host <nsca-server> --password <the key from that daemon's nsca.cfg> --encryption aes256nsca installis a new command. Without--serverit configures submission only โ it writes[/settings/NSCA/client/targets/default]and enablesNSCAClient, never touching the server’s section, and warns when an enabledNSCAServeris still missing a key. With--serverit configures the listener instead:[/settings/NSCA/server]andNSCAServer. It refuses to write a listener with a cipher and no key, since that one cannot start. The Windows installer takes the same values asNSCA_SERVER,NSCA_PORT,NSCA_PASSWORD,NSCA_ENCRYPTIONandNSCA_HOSTNAME. Re-runningnscp web installwithout--passwordnow leaves an existing clear-text shared value alone, so a certificate rotation cannot migrate a credential behind your back.- Tooling that read the shared password back out of
nsclient.ini(or the Windows installer’s configuration dialog on an upgrade, which pre-fills the field from the file) sees thepbkdf2-sha256$โฆstring. The installer hashes a password you give it โNSCLIENT_PWDon the command line, or the dialog field โ and leaves a value it only found on disk alone, so leaving the field unchanged keeps what is there and typing a new one stores it hashed. A password the installer generates for itself, when a fresh install is given none, also stays in clear text: nobody has seen it, so a hash of it would lock the new agent’s web UI and check_nt with nothing left to recover. Rotate it withnscp web password --set <password>. In none of these cases does the password reach the MSI log: the installer logs the settings key and not the value, and the properties carrying it are declared hidden so Windows Installer does not log them either.
See the security notice.
-
๐ ๐ง Check your setup
nscp web installsets up HTTPS and generates the self-signed certificate. The command used to enable HTTPS only when given--https; without the flag it blanked thecertificatesetting and generated nothing, which โ since the web server no longer falls back to cleartext on its own โ left a web server that refused to start. It now configures HTTPS by default and, when the default${certificate-path}/certificate.pemdoes not exist, generates a self-signed certificate there (key and certificate in the one file, readable only by the account running the agent), so the server starts. Re-running it on a host whose earlier install leftcertificateempty repairs that configuration.--httpsis still accepted and means the default. On Linux, where the command runs undersudowhile the packaged service runs asnsclient, a certificate generated bynscp web installornscp nrpe installis handed to the service account (the owner of${data-path}, asnscp enrollalready does for the fleet identity), so the service can load it. See the security notice.If you want plain HTTP, ask for it:
nscp web install --insecurewritesallow insecure = true, no certificate, and uses port8080in place of the HTTPS default8443โ the same port the server itself moves to when it starts in cleartext. Nothing changes for a configuration written by hand.
-
๐ A setting whose name reads as a credential is redacted whether or not its module is loaded. Redaction used to cover only what the modules currently running had registered, so a password left in
nsclient.inifor a disabled or uninstalled module โ an NRPE client target password, a WEB password withWEBServeroff, an NRDP token โ came back in clear fromGET /api/v2/settingsand fromnscp settings --listwithout--load-all. The core now also matches on the key name (password,passwd,passphrase,token,secret,api key,credential, and a barekeyon a target). The NRDPkeyalias, the one spelling of that token which was not registered as a password, is now registered as one. Nothing to do; expect***where a settings dump used to print a value.The name is only used for masking. Moving a value into the Windows Credential Manager (
use credential manager = true) still follows what the owning module declared, so a key that merely reads like a credential is never rewritten innsclient.inion the strength of its name โ the core’s ownuse credential managerboolean is one of those, and it is excluded from the match outright. See the security notice.
-
๐ Check your setup A module is named by a file name, and settings are never migrated to a remote store. A
[/modules]entry (or aControl.LOADregistry request) whose right-hand side is a path rather than a single file name is refused with an error in the log: an absolute value used to replace the module path outright and..walked out of it, which meant naming a module was also naming any file on the host. The./modulesfallback now resolves against${exe-path}instead of the process’s current directory.Migrating to an
http://orhttps://context is refused โnscp settings --migrate-toand aControl.SAVEnaming one. That direction sends this host’s configuration, with the NRPE and NSCA keys, the WEB password and every module’s credentials in it, to whatever the context names; the HTTP backend never supported saving anyway, so this only replaces “Cannot save settings over HTTP” with a refusal that says why.Importing is unchanged.
nscp settings --migrate-from <url>, the MSI’s configuration import and aControl.LOADnaming a remote store all still read a configuration in. Whether that source may be plainhttp://is unchanged too, and is still decided by[tls] allow plaintextinboot.ini(notice 280).Nothing to do unless a
[/modules]entry points outside the module path, in which case move the module there and name it. See the security notice.
-
๐ Check your setup The Linux systemd unit is hardened, the service runs under its own group, the DEB no longer depends on
sudo, andCauseCrashesis not in any package. The unit setsUMask=0027,ProtectKernelTunables=yes,ProtectKernelModules=yes,ProtectControlGroups=yes,RestrictSUIDSGID=yes,RestrictRealtime=yesandLockPersonality=yes. No drive, mount or disk-free answer changes as a result โ the three directives that would do that are deliberately left out, below โ but two consequences are worth knowing about:UMask=0027is inherited by external scripts. It applies to the agent’s own writes (nsclient.db,fleet.ini,applied-state.jsonand the unsealed contents of a bundle go from 0644 to 0640, which is the point), and equally to every command the agent spawns. A script that writes a file for another account or a web server to read will now create it without the group-write and world bits. If you have one, set the mode in the script โinstall -m,chmod, or aumaskline of its own โ rather than relying on the service’s.- The kernel protections do remount
/proc/sys,/sysand/usr/lib/modulesread-only inside the service. Every one of those is on a filesystemcheck_drivesizeand the disk-free collector skip by type, so no drive row appears and no metric changes, andcheck_mountskips them too when listing all mounts. Naming one explicitly is the only way to see the difference:check_mount mount=/sys options=rwnow reportsro.
Group=nsclientcloses a disclosure on Debian and Ubuntu. Without it the service ran under the account’s primary group, and the postinst creates the account withadduser --systemand addsnsclientonly as a supplementary group โ so the primary group stayednogroup. WithUMask=0027that left every file the service wrote at runtime readable bynogroup, which many unprivileged daemons share, and/var/lib/nsclientis0755from the postinst’smkdir -p, sonsclient.dband thefleet/tree โ including the enrollment key โ were not covered by the directory either. Upgrading fixes existing installs; no action is needed beyond that. If you created the service account by hand, make sure annsclientgroup exists, since the unit now names it.The unit also gains
Documentation=andAfter=network.target. The ordering guarantees little at start-up, but at shutdown it stops the agent before the network goes down, which is what a daemon holding client connections and submitting results wants.ProtectSystem,ProtectHomeandPrivateTmpare deliberately not set, and the unit explains why at length. They are the three directives that give a service a mount namespace of its own, and systemd implements each of them as bind mounts that appear in the service’s own/proc/mounts. For most daemons that only restricts what the process may do; for a monitoring agent the mount table is part of what it reports, so those directives would change its answers โ in exactly the direction the checks exist to detect:check what a mount namespace does to it check_drivesize(nodrive=)/usr,/etcand/bootappear as extra drives, each reporting/’s usage, so a threshold on/fires several times overcheck_drivesize, keywordwritablereads 0for every one of those rows, and for/boot/efidisk-free metrics the collector applies the same filter, so the phantom rows reach perfdata and /api/v2/openmetricscheck_mount,options=rwreports missing options: rw, exceeding options: ro, and a filesystem that genuinely went read-only can no longer be told apartcheck_disk_writefails with read-only file systemfor any target outsideReadWritePathsโ the same message as the fault it looks forcheck_files,check_logfileover/tmpreport on the private tree, and a plugin keeping state there loses it on every restart If you accept that for your own setup, add them back with a drop-in rather than by editing the unit, which an upgrade replaces:
systemctl edit nsclient[Service] ProtectSystem=fullNoNewPrivilegesis deliberately not set either, and for a different reason: the Unix script launcher tells operators to sandbox a script withsudo -n -u <account>, which needs setuid.CapabilityBoundingSet=and a restrictiveSystemCallFilter=break it the same way. Turn any of them on with a drop-in if nothing on the host escalates.The unit no longer sets
PIDFile=, andExecStartno longer passes--pid: withType=simplesystemd tracks the main process itself and nothing read the file.A note on systemd versions, since an unknown key or an unparsable value is ignored with a warning rather than failing the unit โ so a host too old for one of these loses it silently.
ProtectKernel*andProtectControlGroupsneed 232,LockPersonality235,RestrictSUIDSGID242;UMaskhas always been there. Every official package target has all of them (Ubuntu 24.04 ships systemd 255, Rocky 9 ships 252). A source build on RHEL 8 (239) losesRestrictSUIDSGID, and on RHEL 7 (219) onlyUMasksurvives.The DEB drops its
sudodependency โ the agent never calls it, and it was there for a documentation example. If an external script escalates withsudo, make sure the package stays installed.CauseCrashes, whose only command deliberately crashes the daemon, is now a diagnostic module behind-DBUILD_TESTING_MODULES=ONand is no longer shipped.See the security notice.
-
๐ ๐ฅ Check your setup A failing background task can no longer take the whole agent down. Check this if you alert on the NSClient++ process disappearing. This is not the fix for a crash anyone has reported: most of the agent’s background work was already wrapped in its own
try/catch, and there is no known input or condition that took an agent down this way. What changes is the guarantee. An exception that escaped a worker thread calledstd::terminate()and ended the entire process, and whether one could escape came down to each thread body remembering to catch - a convention, not a rule. A few did not: the CheckSystem auxiliary collector and the Gearman job runner had no top-level catch at all, and several places caughtstd::exceptionbut not everything. Starting a thread now comes with the guard, so such a failure is contained and logged and costs one piece of work rather than the service.Three things change in ways worth knowing about:
-
A worker that dies now leaves the agent up in a reduced state. It keeps answering everything else, but whatever that thread was doing has stopped - a collector that dies takes its checks’ data with it, and they go stale while the agent looks healthy. If a disappearing process was your signal (a systemd/SCM restart, a “service is down” alert), it no longer fires for this class of failure. Alert on the log instead; every one of these reports reads
Thread '<name>': terminated by an uncaught exception: <details>with
<name>naming the worker (checkdisk collector,scheduler worker 100,socket server worker 0, โฆ). Match on that text rather than on a severity - modules log it ascriticaland the core logs it aserror. -
A scheduler worker that dies is no longer silent. The thread pool behind scheduled checks used to catch and discard the exception, leaving the pool quietly short of workers with nothing in the log to say why. It reports now.
-
Crash reports land next to your log file.
nsclient.fatal- the file the agent writes to when it is going down and cannot use the normal log - is written to the same folder asnsclient.log(${log-path}) instead of whatever directory the service happened to be started from. The folder is created if it is not there when a report has to be written, and if it cannot be written to (a read-only path, a full disk) the report goes to a private folder under the system temp directory rather than being lost:%TEMP%\nsclient++\on Windows,/tmp/nsclient++-<uid>/on Linux. The startup log says which file is in use. If an agent has been dying without explanation, that file is now the first place to look.
Only the service does this.
nscpcommand line invocations keep the old behaviour, so running one undersudocannot leave a root-owned log folder behind for the service account to trip over.See the security notice.
-
- ๐ Every build dependency is verified against a recorded digest. Nothing to do: this affects the release build, not a running agent. Each third-party file the release builds download is checked against a SHA-256 or git commit pinned in the repository before anything is built from it, and a missing entry fails the build. See the security notice.
- ๐ Every DLL the Windows installer ships is Authenticode-signed. Nothing to
do. The modules, the NSClient++ libraries and the bundled third-party libraries
now carry the same signature as
nscp.exe, so an AppLocker or App Control publisher rule for NSClient++ covers them too, and endpoint protection sees signed files instead of unsigned ones. See the security notice.
- ๐ NPS and RADIUS monitoring commands.
CheckWindowsAppsadds experimentalcheck_nps_auth,check_nps_accounting, andcheck_nps_counterscommands.CheckNetadds experimentalcheck_radiusfor PAP authentication, expected rejection, or explicitly enabled Status-Server probes. Existing checks and configuration remain unchanged. The RADIUS probe requires protected credential files and validates both response authenticators; see the security notice.
-
๐ท๏ธ CheckSystem describes the host it runs on: three more selector tags, and the first two fact sets. Nothing to do.
os_family,archandvirtualizationjoin the tags CheckSystem already published, and the inventory โ the vendor, the model, the size and the DNS domain โ is available as theosandhardwarefact sets, which collect nothing until you turn them on.The module already published the OS version and, where configured, a tag per running service. It knew a good deal more about the machine โ every value below is read by one of its checks โ and published none of it, so nothing could tell a 64-core Linux VM from a laptop without running a check against the host. The unix module published no facts at all.
Tags: the three that select a group¶
Tag Windows Linux os_familywindowslinuxarchx86_64,arm64,x86, โฆthe same vocabulary virtualizationsee below see below They join the existing
os_nameandos_versionand are published at start, as those are. A tag is matched whole by a fleet group selector and is uploaded on every state report, which is what these three are for and what the inventory below is deliberately kept out of.The values mean the same thing on every platform: Windows spells 64-bit Intel
AMD64and unixx86_64, and both publishx86_64. That is the point โ one expression selects a mixed fleet.Fact sets: the inventory, opt-in¶
[/settings/system/windows/facts] os = true hardware = true[/settings/system/unix/facts]on Linux, with the same two keys. Both default to false: an inventory is data an operator did not necessarily agree to ship, so nothing is collected until a set is enabled.nscp testthen shows what the core holds:nscp> facts hardware: cpu_cores: 20 manufacturer: Webhallen memory_gb: 32 model: Gaming PC os: arch: x86_64 family: windows name: Windows 11 24H2 version: 10.0.26200 virtualization: noneSet Fields osfamily,name,version,arch,virtualization,domainhardwaremanufacturer,model,cpu_cores,memory_gbA field that could not be determined is omitted, never written empty โ a consumer reads an absent key as unknown, where an empty string reads as an answer. So a VM with no SMBIOS strings has no
manufacturer, and a host whose name is not qualified has nodomain.os_nameis the product name (Windows Server 2022,Ubuntu 24.04.1 LTS) andos_versionthe kernel version on both platforms (10.0.20348,6.8.0-45-generic), so “which hosts still run the old kernel” is one query across the fleet.Two things that surprise¶
virtualizationreadsnoneon a Windows host running Hyper-V, WSL2, Windows Sandbox or virtualization-based security. Those move Windows into the hypervisor’s root partition, where it reads its own hypervisor bit โ so a naive check calls every modern desktop a VM. What the firmware says outranks the bit: a machine whose SMBIOS names an OEM and a product is the physical host. The vocabulary isvmware,hyperv,kvm,xen,virtualbox,qemu,parallels,bhyve,acrn, plusvirtual(a hypervisor is present but neither the CPU nor SMBIOS names it โ this is what WSL2 reports) andnone.domainon unix comes from the configured host name, not the resolver. Nothing in the gather forks a process, queries WMI or calls the resolver, so it cannot hold up a service start on a host whose nameserver is unreachable. The cost is that a Linux host with a short name and its domain only in DNS publishes nodomain, where a Windows host in the same domain does.
Turning a fact set off takes effect on the next settings reload โ the core drops a set its producer stops returning. The tags are gathered once, at start, because nothing they read can change without the host restarting.
-
๐ท๏ธ Three more fact sets: the network interfaces, the volumes and the agent itself. Nothing to do. Like
osandhardware, each collects nothing until you turn it on.; CheckSystem. On Linux: [/settings/system/unix/facts] [/settings/system/windows/facts] network.interfaces = true [/settings/disk/facts] storage.volumes = true [/settings/facts] agent = trueSet Produced by Fields network.interfacesCheckSystem per interface: id,display_name(Windows),mac,status,speed_bps,addressesstorage.volumesCheckDisk per volume: id,device,filesystem,type,label,size_bytesagentthe core version,modules,enrolledA record’s
idis the name the matching check already uses for that instance:storage.volumesids are whatcheck_drivesizecallsdrive, andnetwork.interfacesids are whatcheck_networkcallsname.These sets describe the host, not how busy it is. They carry no free space and no traffic counters, which change every round and belong to the checks. A remote volume is listed without a size, so a dead share cannot stall the round.
enrolledis yes or no, never which server or which identity.network.interfacesandstorage.volumesare re-read every facts round, because addresses and mounts change while the agent runs. See Host Facts for every set, its fields and its cost.
-
๐ฆ A software inventory: the
software.installedfact set. Nothing to do. Like every other fact set it collects nothing until you turn it on.; CheckSystem. On Linux: [/settings/system/unix/facts] [/settings/system/windows/facts] software.installed = trueOne record per installed program, with the name as its id - the same value
check_installed_softwarecallsname:Field Example Meaning id/nameGoogle Chrome,bashwhat the platform calls it version129.0.6668.101as recorded, never parsed publisherGoogle LLCpublisher (Windows), maintainer or vendor (unix) architecturex86_64,noarchone vocabulary on every platform sourceregistry,dpkg,rpm,pacmanwhich database the record came from scopemachine,userWindows only: which hive it was installed into install_date2026-09-09a date, where the platform records one size_bytes5904384installed size, where it is recorded The list comes from the same source
check_installed_softwarereads: the registry’s Uninstall hives on Windows (neverWin32_Product), and the host’s own package manager on unix. Windows entries hidden from Programs and Features (SystemComponent) are left out, and one product installed into several user hives is one record.This is the largest and most expensive set: a few hundred records on a Windows host, thousands on a Linux one, re-read on every facts round because software is installed and removed while the agent runs. A host with more packages than fit the facts document reports the first 2500 and says so under
errors, rather than having the whole set rejected over the size budget. See Host Facts for every set, its fields and its cost.
-
๐ท๏ธ Service inventories: the
docker,mysqlandmssqlfact sets. Nothing to do. Like every other fact set, each part collects nothing until you turn it on, in the module that already checks the service.[/settings/docker/facts] docker = true docker.containers = true docker.images = true [/settings/mysql/facts] mysql = true mysql.databases = true [/settings/mssql/facts] mssql = true mssql.databases = trueSet Produced by Fields dockerCheckDocker the daemon: version,os,os_type,architecture,kernel_version,storage_driver,cgroup_driver,cgroup_version,cpus,memory_bytes,swarmdocker.containersCheckDocker per container, stopped ones included: id,container_id,image,image_id,created,ports,compose_project,compose_servicedocker.imagesCheckDocker per image, keyed on the image id: id,tags,created,size_bytesmysqlCheckMySQL the server: flavor,version,version_comment,hostname,port,server_id,character_set,collation,os,architecturemysql.databasesCheckMySQL per database: id,character_set,collationmssqlCheckMSSQL the instance: server_name,machine_name,instance_name,version,product_level,product_update_level,edition,engine_edition,collation,authentication,clustered,always_onmssql.databasesCheckMSSQL per database: id,recovery_model,collation,compatibility_level,create_date,read_onlyA record’s
idis the name the matching check already uses for that instance:docker.containersids are whatcheck_dockercallsnames,mssql.databasesids are whatcheck_mssql_databasescallsname, and adockerrecord’sversionis whatcheck_docker_infocallsversion.These sets describe the service, not how busy it is. A container is listed with its image and never with its state; the daemon record carries no container or image counts; the server records carry no uptime, connection count or database size. All of that changes every round and belongs to the checks. And they describe the service, not the agent’s configuration: the
mysqlandmssqlrecords carry the names the server reports about itself, never the configured target, and no set carries a login, a password, a driver or a command line.CheckMySQLandCheckMSSQLconnect with what is configured in their settings section (userandpassword, adefaults file, or Windows authentication), the same connection the checks use when a check does not pass its own.CheckDockertalks to the configuredendpoint. All three are re-read every facts round, because containers and databases come and go while the agent runs; a round that cannot reach the service reports why undererrorsand keeps the last good set. Every list stops at 2500 records and says undererrorshow many there were, assoftware.installeddoes, so a host with more never loses the whole set to the document’s size budget. None of them is read on the startup round, which runs on the thread that starts the service: a daemon or server that does not answer would hold the start for its full timeout, so at startup the set says so undererrorsand is collected from the first scheduled round on, or right away with a manual refresh. See Host Facts for every set, its fields and its cost.
-
๐ท๏ธ Service and scheduled-task inventory. Nothing to do on existing installations; both fact sets remain off until enabled.
CheckSystem can now publish
services.installed: local Windows services or Linux systemd service units, including stopped and disabled services. Records carry the name used bycheck_service, display name and native startup type. Linux includes never-loaded installed units, templates and loaded instances. The inventory is not yet supported on macOS; enabling it reports an explicit error while the existing launchd checks remain available.CheckTaskSched can publish
tasks.scheduled: local tasks, including hidden and disabled tasks in subfolders. The agent’s account must be able to enumerate every folder and read each returned task; access denied fails collection and retains the last successful inventory. Full paths distinguish tasks with the same title; records also contain name, folder, enabled and hidden flags. Legacy Task Scheduler omits the unknown hidden flag.Both are off by default:
; Linux: [/settings/system/unix/facts] [/settings/system/windows/facts] services.installed = true [/settings/task schedule/facts] tasks.scheduled = trueLoad CheckSystem and, on Windows, CheckTaskSched. Run
facts refreshto collect immediately, or wait for the next scheduled facts round. Neither set is collected on the startup thread. Failed collection retains the last inventory and reports an error; lists are sorted and capped at 2500 records with a truncation error. Disabling a set takes effect on settings reload. No process metrics, task run results, accounts, actions or command lines are published.
-
๐ท๏ธ Check your setup Host facts over REST and in the web UI, behind two new grants. Nothing to do unless a non-admin web account should see the inventory. The collected fact sets are served at
GET /api/v2/facts(the whole document, or one subtree with?path=os), andPOST /api/v2/facts/commands/refreshcollects a round now and answers with the result. The web UI gains a Facts page that shows the document and turns a fact set on through the settings API.nscp testhas the same view asfactsandfacts refresh.The two endpoints need their own grants,
facts.getandfacts.refresh. Only the built-infullrole (*) carries them;client,monitoring,metricsandrestricteddo not, so an inventory is never readable by an account that was only meant to run checks or scrape metrics. To let one read it, add the grant to its role:[/settings/WEB/server/roles] monitoring = public,queries.execute,aliases.list,login.get,metrics.list,openmetrics.list,facts.getfacts.refreshis separate because it makes every producer collect at once; give it only to accounts that should be able to trigger that. See Facts.
-
๐ค Check your setup An enrolled agent now sends its host facts to the fleet server. Nothing to do on a default install: no fact set is enabled by default, and a host with none enabled sends only the hash of the empty document. If you have enabled fact sets on a host that is enrolled with a fleet server, that inventory now leaves the host.
- Every desired-state poll (
facts_hash=query parameter) and every state report (facts_hashmember) carries the SHA-256 of the facts document, never the document itself. - The server answers with the hash it holds in an
X-Facts-Hashresponse header. The document is uploaded on its own call,POST /agent/v1/facts, only when that answer differs from the agent’s hash. A server whose poll answers carry no such header is never sent the document. The server sends the header on its answer to an upload too. - A rejected upload (any error status but 413) is retried after 1 minute, then
2, doubling up to once an hour. No upload goes out in a poll cycle whose
poll or report got an error answer, and a
Retry-Afteron a report or an upload holds every upload until it passes. A 429 on a poll is waited out before the agent calls again; any other failed poll, a 503 included, is logged and backed off with itsRetry-Afteras the shortest wait. An upload cut by the network while the polls get through is paced like a rejection. A document the server confirmed holding and then reports missing is re-sent at once the first time (logged at info), then after 1 minute, doubling up to once an hour (logged as an error when it gets there); one it has not confirmed yet is not re-sent for a minute. A 413 is not retried for a day or until the document changes, and the log names the largest sets. - The server must hash the
factsvalue exactly as it received it. A server that answers an upload with a new hash of its own, or that acknowledges one document three times without ever reporting holding it (repeating what it held before the upload counts as not reporting), is not sent that document again until it changes, the server’s reported hash changes (other than by another upload of the agent’s), or a day has passed, and the agent logs an error. [/settings/facts] max sizeis re-read on every settings reload, a settings-only one included. Lowering it under the current document drops the largest sets (each is logged) until the rest fits.
To keep a set on the host but off the server, there is no separate switch: turn the set off, or do not enroll the host. See Host Facts.
- Every desired-state poll (
-
๐ NSClient++ now ships an experimental macOS (Apple silicon) package. Nothing changes for existing Windows or Linux installations. Experimental in the sense a young check command is: it works, but the packaging - the install layout, the service account, the launchd job - is not settled, so read this note again on the next upgrade rather than applying it blind. Releases now also carry
NSCP-<version>-macos-arm64.pkgand a plainNSCP-<version>-macos-arm64.tar.gzof the same tree. Both are self-contained - they bundle the libraries they link against, so the target Mac needs no Homebrew. The.pkginstalls under/usr/local, creates a hidden_nsclientservice account, and registers the agent as a launchd daemon (/Library/LaunchDaemons/com.nsclient.nscp.plist) that starts at boot. It is managed withlaunchctlrather thansystemctl:sudo installer -pkg NSCP-<version>-macos-arm64.pkg -target / sudo launchctl print system/com.nsclient.nscp # status sudo launchctl kickstart -k system/com.nsclient.nscp # restart sudo /usr/local/sbin/uninstall-nsclient --purge # removeThree caveats for this first release:
- Apple silicon only. There is no Intel build, and Rosetta 2 does not substitute for one; the installer refuses an Intel Mac rather than installing something that cannot run.
- Not signed or notarized. Gatekeeper refuses the package with “cannot be
opened because Apple cannot check it for malicious software”. Install from a
terminal instead, which does not go through that check:
sudo installer -pkg NSCP-<version>-macos-arm64.pkg -target /. To install from the Finder you have to clear the quarantine flag your browser set (xattr -d com.apple.quarantine <pkg>) or approve it under System Settings โ Privacy & Security after the first attempt. CheckDiskandCheckLogFileare not in the macOS build. Their data sources are Linux kernel interfaces (mntent,inotify), so there are no disk or log-file checks on macOS yet. Everything else -CheckSystem(see the note on it below), the REST API and web UI, the NRPE/NSCA/NSCP/check_mk listeners and clients, external scripts, the Lua script engine, the network and security checks, the scheduler and the forwarders - is present. A macOS configuration that enables one of those two modules logs a “module not found” error on startup, which is worth checking for if you are reusing a Linuxnsclient.ini.
See Installing on macOS and Supported platforms.
-
๐
CheckSystemruns on macOS. Nothing to do on Linux or Windows. The macOS package now carries all 21 of the module’s Linux commands -check_cpu,check_memory,check_process,check_service,check_uptime,check_network,check_load,check_os_version,check_os_updates,check_installed_software,check_batteryand the rest - plus the real-time filters, the system metrics on the dashboard and the host, network and software fact sets. A Linuxnsclient.iniworks as it is: the section is still/settings/system/unixand the keywords are the same.The data comes from sysctl, the Mach host statistics, libproc, IOKit and
launchctlinstead of procfs, and a few values have no Darwin counterpart. Those are reported as absent - the keyword rendersunknown, never satisfies a threshold and emits no performance data - or the check returns UNKNOWN, never a zero that was not measured:check_processcannot read the memory, fault and CPU counters or the command line of another user’s process unless the agent runs as root, and macOS keeps no peak sizes. Those keywords are optional now on every platform; Linux fills them as before.check_servicechecks launchd jobs by label (service=com.apple.logd). An idle job launchd starts on demand isstart_typeon-demandand statestatic, so it passes the default thresholds. A newhas_metricskeyword says whetherrss,vms,cpuandtaskswere measured.check_installed_softwarelists installer receipts, application bundles and Homebrew kegs; themanagerkeyword says which (pkgutil,bundle,homebrew).check_os_updatesreads the list macOS cached at its last background check. The newlive=trueoption asks Apple’s update server instead, which takes 10 to 60 seconds. The newlast_checkedkeyword is when the list was refreshed.check_temperatureandcheck_cpu_frequencyare always UNKNOWN, andcheck_kernel_statshas only itsthreadsrow.check_kernel_memoryhas no slab, and reports the newwiredandcompressedkeywords instead; on Linux those two areunknown.
The full list is in Installing on macOS.
One fix reaches Linux too: on a machine with more than ten cores,
check_cpu, the real-time CPU filters and the per-core metrics reported the load of core 10 and up under the wrong core numbers. Each core now keeps its own number.
- ๐งช ๐ท๏ธ New
CheckActiveDirectorymodule (experimental). Nothing to do on an upgrade: the module is not enabled by default. On Windows,CheckActiveDirectory = enabledunder[/modules]addscheck_ad_replication(inbound replication links on a domain controller: consecutive failures and last success),check_secure_channel(the machine-account secure channel, verified through netlogon likeTest-ComputerSecureChannel) andcheck_kdc(a real Kerberos AS-REQ against each KDC, so a KDC that accepts connections but no longer issues tickets is caught). The KDC probe names this machine’s own account, so a healthy KDC answersKDC_ERR_PREAUTH_REQUIREDand the DC logs no failed ticket request for an unknown account; to probe a realm this machine is not joined to, name an existing account withprincipal=. The module is marked experimental: its options, keywords and output may still change. See the Active Directory & Identity scenario.
-
๐งช ๐ท๏ธ New
CheckHyperVmodule (experimental). Nothing to do on an upgrade: the module is not enabled by default. On a Hyper-V host,CheckHyperV = enabledunder[/modules]addscheck_hyperv_host(virtual machine health summary and hypervisor capacity),check_hyperv_cpu(logical processor load, which the ordinary CPU counters cannot see on a host) andcheck_hyperv_vms(one record per virtual machine: state, heartbeat, health, uptime, memory, processors, checkpoints and Hyper-V Replica status), plus ahypervmetrics bundle and thehyperv.vmsfact set. Reading the virtual machines needs local administrator or Hyper-V Administrators rights for the service account. The module is marked experimental: its options, keywords and output may still change.Like every fact set,
hyperv.vmscollects nothing until you turn it on:[/settings/hyperv/facts] hyperv.vms = trueOne record per virtual machine, with the name as its id - the same value
check_hyperv_vmscallsvm- carrying the GUID, generation, configuration version, configured processors and memory, dynamic memory, checkpoint count and Hyper-V Replica role. The state, heartbeat, load and assigned memory stay with the check. The set is collected from the first scheduled, reload or manual round on, not at startup, so the boot thread never waits on the virtualization namespace. See Host Facts for every set, its fields and its cost.
- โธ๏ธ New experimental
CheckKubernetesmodule. Nothing to do on an existing install: the module is not enabled by default. It monitors a Kubernetes cluster through its API server with the agent’s own HTTPS client and a service account token - nokubectlon the host - and brings four checks:check_kubernetes(API reachable and ready, node counts),check_pods(thekubectl get podsSTATUS column, readiness, restarts),check_nodes(readiness, pressure conditions, cordons, capacity) andcheck_workloads(desired versus available replicas of deployments, statefulsets and daemonsets). The cluster is configured once under[/settings/kubernetes](api serverplustokenortoken file, a JSONkubeconfig, or nothing at all when the agent runs in-cluster with a service account); no check accepts aurl=,host=ortoken=argument, so a REST caller cannot redirect the token to another server. Every command is marked experimental until the keywords and output have settled against real clusters. Building from source needs Boost 1.77 or later for this module; with an older Boost (RHEL 9 ships 1.75) it is skipped and the rest of the agent builds as before. See the Kubernetes scenario.
-
๐งช Windows Failover Cluster checks in
CheckWindowsApps(experimental). Nothing to do on an upgrade. On a cluster member, four new commands read the local cluster through ClusAPI, without PowerShell or WMI, and never move or restart anything:Command Checks check_cluster_groupsclustered roles and their current owner node check_cluster_resourcesresources, their type, group and owner check_cluster_nodesnode states check_cluster_networkscluster network states The service account needs cluster read access, and the host must run Windows Server 2008 R2 or later with Failover Clustering. A missing cluster, an access error or a partial read returns UNKNOWN, never an empty OK. By default an offline or failed group is CRITICAL and a pending or partially online one is WARNING, so a role that is offline on purpose (an unused storage group, for example) alerts until you filter it out: select the roles you expect to run (
name=SQL) or exclude the rest. Like the rest of the module, the commands are experimental: options, keywords and output may still change.
-
๐งช Eight more SQL Server checks, and growth headroom in
check_mssql_databases(experimental). Nothing to do on an upgrade: existing checks keep their defaults. The new commands use the module’s existing connection settings:Command Checks Rights check_mssql_sessionssession and connection counts per database and login VIEW SERVER STATEcheck_mssql_blockingblocked sessions and blocking chains VIEW SERVER STATEcheck_mssql_transactionsold or leaked open transactions and long-running requests VIEW SERVER STATEcheck_mssql_countersbuffer cache, page life expectancy, batch and lock rates VIEW SERVER STATEcheck_mssql_waitswait statistics by category and scheduler pressure VIEW SERVER STATEcheck_mssql_tempdbtempdb space by consumer and volume headroom VIEW SERVER STATEcheck_mssql_availability_groupsAlways On replica and database health VIEW SERVER STATEcheck_mssql_integritysuspect pages and the age of the last successful CHECKDB SELECTonmsdb.dbo.suspect_pages;sysadminforcheckdb_agecheck_mssql_counterssamples twice, one second apart, so it takes about a second longer than the others.check_mssql_integrityreads when CHECKDB last succeeded; it never runs CHECKDB itself. A missing permission degrades a keyword to a-1/-2sentinel, or returns UNKNOWN, rather than raising an alert.check_mssql_databasesgainsdata_headroomandlog_headroom: how much further a database can grow before it reaches a file’smax_sizeor fills the volume it grows into. Files that share a volume share its free space, so the headroom is not counted once per file:check_mssql_databases "critical=data_headroom < 1G and data_headroom >= 0"One fix reaches every check: an integer keyword compared against a size (
1G) or another integer is no longer converted through a floating-point number on the way, so values above 2^53 bytes compare exactly.
-
๐งช New
check_domaininCheckNet: domain expiration over RDAP (experimental). Nothing to do on an upgrade.check_domain domain=example.comreads the registered domain’s expiration date over RDAP and, by default, warns below 30 days and goes critical below 10 (expires_in, in whole days, also emitted as performance data). Pass the registered domain (example.co.uk), not a host under it; the check does not guess one from the other.It makes a live request on every run, so schedule it daily from one location per domain. The default endpoint is the public
https://rdap.org/domain/{domain}redirector, which means the domain name you check is sent to a third party and the registry it redirects to.rdap-url=points it at a provider of your choice. TLS verification is always on (ca=selects a trust bundle), redirects from HTTPS to HTTP are refused, and the check connects directly, without an HTTP proxy.whois-fallback=true whois-server=<server>falls back to WHOIS on TCP port 43 when RDAP fails. WHOIS is unencrypted and unauthenticated, and the fallback also applies after a TLS verification failure, so enable it only with a server you trust. Malformed or conflicting answers, HTTP errors (including 404 and 429) and timeouts return UNKNOWN.
- ๐ฆ Windows downloads carry a software bill of materials, and every release
asset is attested. Nothing to do. Each Windows release now publishes
NSCP-<version>-<platform>.cdx.jsonnext to its MSI and zip, and the zip holds the same file assbom.cdx.jsonplus aSHA256SUMSof its contents. The SBOM is CycloneDX 1.6 JSON and lists every third-party component the build compiled or bundled, with the upstream URL and the SHA-256 or git commit the build verified it against. The bundled check_nsclient brings its own SBOM, nested under its entry. Release assets are attested by the release workflow, sogh attestation verify <file> --repo mickem/nscpshows which workflow run and commit produced a file. See Verifying the download.
- ๐ Check your setup Performance data no longer reports a missing threshold as
0. When a check’swarningorcriticalexpression did not bound a performance-data keyword, the missing field used to be rendered as a literal0('time'=2ms;1000;0), which graphing tools such as PNP4Nagios, Grafana and Icinga read as “critical above 0”. The field is now left empty ('time'=2ms;1000). A bound that really is0is still written as0. Only if you parse the warn/crit fields of the perfdata yourself and relied on the0placeholder do you need to treat an empty field as “no threshold”.
-
๐
check_cpuanswers from the samples it has, instead of reporting an idle machine for the first minutes after a start or a reload. Nothing to do. The collector’s sample buffers start out full of empty slots, so a5mwindow (the default) asked for two minutes after the agent started - or after any settings reload, which replaces the collector - averaged two real minutes with three empty ones, and a saturated host read as 40 % or less until the buffer had filled;warning=load>80could not fire in that time. A window longer than what has been sampled so far is now averaged over the samples there are.Before the very first sample - the first second or so after a start or a reload - the checks now answer UNKNOWN instead of reporting zeros:
check_cpuon Windows and Linux saysNo CPU data available yet (collector still initializing).- On Linux,
check_memoryandcheck_pagefiledo the same (No memory data available yet ...,No pagefile/swap data available yet ...).
The Linux checks already carried that message, but it could never be reached: the “has data” test asked whether the buffer was non-empty, and the buffer is created full, so it was always true. On Windows, a collector too busy to be read within five seconds is reported as such (
Failed to read CPU data: the collector is busy ...) rather than as a fresh start.
-
โฑ๏ธ Check your setup A reload requested from inside a check is applied after the check returns. Nothing to do on a default install. A script that calls
core.reload(...)for the whole service or for any module from inside a check now hands the reload to the service’s scheduler instead of running it on the thread serving the check:reload()returns as soon as the reload is queued, and the new configuration applies moments later. That thread can belong to a module other than the one running the script - a Lua check served over NRPE runs on an NRPE thread - so reloading the service, orNRPEServeritself, that way previously restarted the listener that was serving the check from one of that listener’s own threads, which left it dead:check_nrpetimed out from then on. A reload requested from anywhere else, includingreload("settings")from a script, still applies before the call returns. A script that reads its own settings straight after the call should reload them a moment later, or requestdelayed,serviceand poll.Where there is no scheduler to hand it to -
nscp unit, andnscp clientwithout--boot- a script that reloads the whole service or the module it is running in is refused with an error, as that would unload the script it returns into. Reloading some other module from a script, which is how the unit-test scripts apply the configuration they set, still runs immediately there.
-
๐ง A settings reload no longer piles up scripts, plugins, aliases and syntax. Nothing to do; the difference shows on agents that reload often (fleet-managed hosts, operators saving settings from the web UI).
LUAScriptunloads the previous generation of scripts before loading them again; every reload used to leak the old scripts and their Lua states, and to register every command a second time.DotnetPluginsunloads the managed plugin instances from the previous load before loading the configured ones again, and a plugin removed from thepluginssection is no longer loaded again on reload. Every reload used to add another copy of each plugin, and queries were answered by whichever copy came first. The commands and channels the previous instances registered are taken back too, so the commands of a plugin removed from[plugins]are no longer listed (they used to fail withNo .NET plugin loaded).CheckHelpersrebuilds its alias table on reload and unregisters the aliases that were removed from[/settings/check helpers/alias]; a removed alias used to keep resolving until the service was restarted.SimpleFileWriterreplaces its line syntax on reload instead of appending to it: after N reloads every line written carried N copies of the syntax. A changedchannelnow moves the subscription instead of adding a second one; both channels used to write to the file.CheckMKClientandCheckMKServerrelease the previous generation of their Lua scripts on reload, which now runs each script’s unload hook on every reload rather than only at shutdown. The bundleddefault_check_mk.luadoes nothing there; a custom script with an unload hook that assumes it runs once, at shutdown, should expect it to run on every reload.
0.23.0¶
- ๐ ๐ฅ Action required A listener
verify modeit does not recognise now refuses to start. Check every listener you have configured for mutual TLS before upgrading. The parser used to drop a token it did not know, andfail-if-no-peer-certโ the spelling the documentation tells you to write โ was one of those tokens. Soverify mode = peer,fail-if-no-peer-certresolved to barepeer: the listener asked for a client certificate and completed the handshake when none arrived, leaving an “NRPE with mutual TLS” that was really NRPE with an IP filter. Both spellings are now accepted, along withcertificateforpeerandclient-certificate, and whitespace around a token is ignored. Anything else makes the listener log the offending token and refuse to start, which is the loud version of the same mistake. See the security notice.
-
๐ ๐ฅ Check your setup The NSCP and check_mk clients, and
check_nsclient_web_online, now verify the server certificate by default. All three encrypted their connection but never authenticated the peer:verify modedefaulted tononeon anNSCPClienttarget, was left unset on aCheckMKClienttarget (an empty verify mode parses the same asnone), andcheck_nsclient_web_onlinedefaultedverifytonone.verify mode/verifynow defaults topeerandcato the agent’s own trust bundle (${ca-path}โ the auto-generated ROOT store export on Windows, the distribution bundle elsewhere).Because an NSClient++ agent generates a self-signed certificate on first start, a relay or REST check pointed at a default agent will now fail the handshake where it used to connect. Pick one per target:
Situation Configuration The remote agent uses a certificate from your own CA ca = <the CA>(the defaultverify mode = peerthen works)The remote agent still uses its self-signed certificate ca = <that certificate>andverify mode = peer-certYou accept an unauthenticated link verify mode = none(encrypted, but an on-path attacker can impersonate the remote)[/settings/NSCP/client/targets/web01] address = nscp://192.168.56.103:8443 password = <the remote's admin password> ca = /etc/nsclient/agent-web01.pem verify mode = peer-certFor
check_nsclient_web_onlinethe same choice is made per check withca=andverify=:check_nsclient_web_online host=agent.example.com password=... \ ca=/etc/nsclient/agent.pem verify=peer-certCheckMKClienttargets are only affected when they enable TLS (use ssl = true); a stock check_mk agent listens in plain text and is unchanged.See the security notice.
- ๐ ๐ฅ Check your setup The op5 installer profile defaults to secure NRPE. Picking op5 on the
MSI’s monitoring-tool page used to force NRPE into legacy mode โ anonymous
Diffie-Hellman, no peer verification โ hide the mode radio group so the choice
could not be seen or changed, and enable
allow nasty charactersfor every NRPE-reachable check. The profile now defaults to the same secure mode the generic profile uses (verify mode = peer-cert,insecure = false), shows the NRPE mode radio group so legacy is a visible decision, and no longer setsallow nasty characters.allow argumentsstays on, because op5’s check commands need it; what changes is that a caller has to present a certificate first. An op5 host monitored by acheck_nrpewith no client certificate needs either a certificate or Insecure mode picked explicitly on that page. See the security notice.
- ๐ Check your setup A fleet management url that is not
https://is now refused. Nothing to do for a host enrolled against an https fleet server. Themtls_urlin the enrollment manifest is where desired state and signed bundles come from, and on a plain socket neither the agent’s client certificate nor the pinned server certificate does anything โ the whole management channel ran unauthenticated, silently. Enrollment now refuses a non-https management url, the sync loop refuses to start on a stored one, and the http client refuses to attach a client certificate or a pinned CA to a non-TLS transport at all. A url with no scheme counts as plaintext, because it is opened on a plain socket just the same. Where plaintext is what you want,nscp enroll --insecurerecords the decision in the manifest and[tls] allow plaintext = trueinboot.iniallows it for an older manifest; the sync then logsINSECUREon every start. See the security notice.
-
๐ Check your setup You stay logged in to the web UI across a restart of the agent. Restarting the service, or upgrading it, no longer logs every web user out: a browser tab, and a script holding a key from
/api/v2/login, keeps working. Sessions still expire eight hours after login, and only a hash of each session key is ever kept, in memory and on disk; see the security notice.To end a session, use the log out button in the web UI, or call
DELETE /api/v2/loginwith the key. It stops working immediately and does not come back, not even if the agent is killed rather than stopped cleanly.Changing a user’s password or role also ends that user’s sessions, and removing the user ends theirs โ but only from the next start of the agent. The running service keeps the users it read when it started, so until you restart it the new password does not work and the old sessions keep working. Reapplying settings from the UI is not enough; restart the service, or log the sessions out.
If you relied on a restart to log everyone out, set
persist sessions = falseunder[/settings/WEB/server]: sessions are then kept in memory only, as before, and every restart ends every session. Use the same switch to invalidate every session at once after a suspected leak (restart once with it off; switch it back on afterwards if you want sessions to survive the next restart).[/settings/WEB/server] ; Keep sessions in memory only, as before: every restart ends every session. persist sessions = falseOne limitation: a user whose password is written in cleartext in
[/settings/WEB/server/users/<user>]still has to log in again after a restart. Passwords stored in their hashed form (pbkdf2-sha256$โฆ, which is what the agent writes foradmin) are not affected. To convert a cleartext password, runnscp web add-user --user <user> --role <role>without--password: it keeps the password the user already has and stores it hashed.
- ๐ The Windows release build now verifies what it downloads. Nothing to do:
this affects the release build, not a running agent. The checksum gate added
last release was in place but every line in
.github/dependency-checksums.txtreadunrecorded, so each download warned and continued. The digests are recorded now for OpenSSL (cross-checked against OpenSSL’s own published.sha256), protobuf, Crypto++, miniz and the prebuiltcheck_nsclient.exe. TinyXML2, Mongoose and MariaDB Connector/C are no longer fetched as GitHub-generated tag archives but cloned and verified against a recorded commit, the way googletest already was. Lua stays unrecorded until its digest can be taken against the checksums lua.org publishes. Bumping a dependency version now means adding its line to that file first, because a missing line fails the build. See the security notice.
-
๐ง Check your setup An unrecognised
${...}path token is now an error, and a[paths]override has to name an absolute location. Nothing to do if your paths are spelled correctly and your overrides are absolute โ which is the normal case. If not, the agent now says so in the log instead of quietly using the wrong folder.Two related changes:
- An unknown token is reported and the setting that carried it is skipped.
It used to resolve to the installation directory, so a mistyped
${scripst}/check.batwas not rejected but turned into a real path under the install folder โ and whatever depended on it went somewhere nobody was looking. This is the same silent failure that made a pre-0.17${host}in a path produce a mangled name rather than an error. The tokens are still open ended: anything you define inboot.ini’s[paths]section counts as known. Note that${appdata}and${common-appdata}are Windows-only and are now rejected on unix, where they previously resolved to the install directory. - A
[paths]or--path-overrideentry that does not resolve to a location of its own is ignored, with an error naming it, and the built-in default applies. An override may still be written in terms of other tokens (scripts = ${shared-path}/mine); it is the resolved value that is judged. “A location of its own” is slightly wider than “absolute”: on Windows a drive-relativeC:mineand a root-relative\mineare accepted, because the operator plainly named a drive or a root and it is not ours to move them โ but note they still resolve against the process’s current drive or directory, so prefer a fully absolute path. A relative override would be read and written relative to the service’s working directory โC:\Windows\System32for a Windows service,/under a bare init script, the package directory under the shipped systemd unit โ so what it meant depended on how the agent happened to be started.
If you scripted
--path-override log-path=.or similar against a build tree, give it an absolute path instead.Check the log after upgrading: every rejected token and override is reported with the key that carried it.
- An unknown token is reported and the setting that carried it is skipped.
It used to resolve to the installation directory, so a mistyped
-
๐ง Check your setup A relative path in a setting that writes files is now taken relative to that setting’s own folder, not to the service’s working directory. Nothing to do if your paths are absolute or written with
${...}tokens. If any of the settings below is a bare relative name, the file it produces moves โ on unix usually not at all, on Windows usually into the folder it should always have been in.Expanding a path substitutes
${tokens}; it never made anything absolute. A value with neither a token nor a leading/was therefore resolved against whatever the service’s working directory happened to be, which isC:\Windows\System32for a Windows service,/under a bare init script, the package directory under the shipped systemd unit, and the shell’s directory fornscp test. Four different answers from the same configuration file.Settings whose consumers own a folder now say so, and a relative value lands there instead:
Setting Relative values now land in [/attachments]target${shared-path}[/settings/log] file name${log-path}[/settings/crash] archive folder${crash-folder}[/settings/fleet] managed path${fleet-folder}[/settings/filewriter] file${log-path}An absolute path, a UNC path or anything written with a token is used exactly as given โ pointing one of these somewhere specific is still yours to decide.
nonestill means “no file” wherever it was already accepted.One Windows value that did not used to be taken at its word now is. If
[/settings/log] file nameis literally/nsclient.logโ a value carried forward from a version where a leading slash meant the installation directory โ it was quietly rewritten to${exe-path}/nsclient.logand the log appeared beside the executable. It now names a root like any other absolute path, so the log lands atC:\nsclient.log, on the root of the system drive, where the service may well not be permitted to write. The compiled default was never this value, so only a configuration that sets it explicitly is affected.Set it to what you actually want โ
${log-path}/nsclient.logfor the normal location, or a barensclient.log, which now means the same thing:[/settings/log] file name = ${log-path}/nsclient.logOn Linux the attachment case generally does not move: the shipped systemd unit sets
WorkingDirectoryto the package directory, which is what${shared-path}resolves to, so relative attachments were already landing in the right place โ by coincidence rather than by design. Windows services and any non-systemd launch are where this changes something.SimpleFileWriter’s defaultfile = output.txtis affected by the same rule and now writes to${log-path}/output.txtrather than to the working directory.
-
๐ง Check your setup
file name = nonenow really switches the log off on Windows. Nothing to do unless[/settings/log] file nameis set tonone. Unix installations already behaved correctly.noneis documented as “no log file”, but the name was joined to the installation directory before the sentinel was tested. On Windows, where that directory is not empty,nonebecame a real file calledC:\Program Files\NSClient++noneand file logging stayed on. On unix the join contributed nothing, so the sentinel survived and the setting worked.If this applied to you, the stray file is left where it is โ delete it once you have checked you do not need its contents.
noneis now recognised by the path expander itself, so it is equally safe in every setting that accepts it, including everycaoption where it means “use the TLS library’s own trust store”.Where a bare log file name ends up is a separate change with its own note โ see the one about relative paths being taken relative to the folder their setting owns, which covers the log file on both platforms.
-
๐ง Check your setup Windows: a relative external-script command is resolved against the installation directory.
[/settings/external scripts/scripts]entries that carry a folder โ the conventionalcheck_foo = scripts\check_foo.bat, and whatnscp ext-scr add --importwrites โ are now rooted at${base-path}before they are launched. On Windows that is the same folder${scripts}points at, so the script is found wherever the agent was started from.Previously such a command was measured against the working directory of the process, which is
C:\Windows\System32for the service and the shell’s directory fornscp test. The effect was easy to miss because it depended on how the command happened to be spelled: a single backslash (scripts\check_foo.bat) is not tokenisable as an argument vector, so it fell back to the legacy single-string launcher, where the lookup already honoured the installation directory and the command worked. A command that was argv-safe โ a doubled backslash, a forward slash, a quoted path โ ran with argv-isolation, where the executable is named separately and was resolved against the working directory, and it failed for the service with “the system cannot find the path specified”.Check your configuration if either applies:
- You worked around this with an absolute path. Nothing to do โ an absolute path,
a UNC path, a drive-relative
C:check.exeand a root-relative\tools\check.exeare all still used exactly as written. - You relied on the command resolving against the working directory โ a relative command
pointing at a folder outside the installation directory, with the agent started from somewhere
that made it resolve. That no longer works; name the script with an absolute path, or with
${scripts}\<name>if it lives in the script folder.
A command with no folder at all (
cmd.exe,powershell.exe,cscript.exeโ what the shipped[/settings/external scripts/wrappings]use) is not rooted; rooting it at the installation directory would point every one of them at a file that is not there. It now gets the system’s own executable search instead โ the directory the agent loaded from, the working directory, the system and Windows directories, thenPATH. On the argv-isolated path it previously got no search at all and resolved against the working directory alone, socommand = cmd.exe /c โฆonly ran when the agent happened to be started from a directory containing a copy ofcmd.exe. The shipped wrappings were not affected, because their backslash paths put them on the legacy launcher, which has always done the search.Linux is unchanged: the unix launcher sets no working directory for the child and does not root the command, so a relative command still resolves against the agent’s working directory โ which the shipped systemd unit sets to
${shared-path}. - You worked around this with an absolute path. Nothing to do โ an absolute path,
a UNC path, a drive-relative
-
๐ง Check your setup
nscp luaandnscp pynow look in, and import into, the folder scripts are actually loaded from. Nothing to do unless you use those subcommands to manage scripts; loading scripts configured innsclient.iniis unchanged, and existing configuration entries keep working.Both ext-scr CLIs derived their folder by appending a literal
scriptssegment to a root, rather than naming${scripts}โ the token that already means that folder. The two are the same thing only on Windows, where${scripts}is${exe-path}/scripts:Looked in / imported into (before) Now nscp lua list/show/add/delete${base-path}/scripts/luaโ right on Windows,/usr/sbin/scripts/luaon Linux${scripts}/luanscp py list/show/add/delete${scripts}/scripts/pythonโ a folder nothing creates${scripts}/pythonSo on Linux the Lua subcommands were looking beside the binary rather than in the package’s script folder, and the Python ones imported into a doubled path on every platform.
nscpfor external scripts (CheckExternalScripts) was already correct and is unchanged.add --importalso records a different value in the configuration. It used to writescripts\python\<name>/scripts\lua\<name>, which paired with the old folders and only resolved on Windows, where a backslash is a separator. New imports recordpython/<name>/lua/<name>, relative to${scripts}and resolving the same way on both platforms.list(and the script list inshowand the web UI) names its entries the same way, relative to${scripts}:lua/mycheck.luarather than an absolute path or ascripts\lua\mycheck.luathat only resolved from the installation directory. So whatlistprints can be handed straight back toaddfrom anywhere, which is what it is for.Entries written by an older version are not rewritten and keep resolving as before โ the file they point at has not moved.
-
๐ง Remote
[/includes]and[/attachments]are now refreshed on their own. An agent configured from anhttp(s)://settings url re-downloads its whole configuration every settings maintenance interval (/settings/coresettings maintenance interval, default5m). Until now that pass stopped at the top-level file: a file pulled in by[/includes], and every[/attachments]target, was only re-fetched when the top-level file itself happened to change โ so on a server wherensclient.iniis the stable part and the included file is the one that moves, the include stayed pinned to whatever it held when the agent started. Both are now fetched on every pass, and a change in either triggers the same reload a change in the top-level file does.Nothing to configure. Two things to be aware of:
- The settings server now sees one request per included file and per attachment on every interval, not just one for the top-level file. The responses are hash-compared, so an unchanged file still costs nothing beyond the request itself โ but if you serve a large attachment to a large fleet, size the interval accordingly.
- A configuration change made only in an included file now takes effect within one interval instead of requiring a service restart. If you were touching the top-level file to force includes through, that workaround is no longer needed.
-
๐ง The Windows installer creates
scripts\custom\again, and always. Nothing to do โ it is a folder to put your own scripts in, created empty.It used to exist and was lost when a batch of outdated sample scripts was removed. Unlike the shipped examples it is not part of the Scripts (
SampleScripts) installer feature, soREMOVE=SampleScriptsno longer leaves an installation with noscripts\directory at all โ which previously meant an[/attachments]entry, or anything else writing a script, had nowhere to land.Nothing is installed into it, so an uninstall removes it again only if you have left it empty.
-
๐
ext-scr,pyandluaadd --importnow work on a fresh install, and what they record is a path that resolves. Nothing to change: existing configuration is read exactly as before, and this only affects what the import writes from now on.Three things were wrong with importing a script, and each of them was silent in its own way:
Symptom Was Now Import fails outright copy_filenever created the destination folder, so the import died with a bare No such file or directory namingโฆ/scripts/pythonthe folder is created Imported command exits 127 on Linux ext-scrrecordedscripts\<name>โ a Windows spelling of a path this module never resolves anywaythe destination’s absolute path is recorded add --importwith no--scriptthe destination collapsed onto the script folder itself and the copy overwrote that path with a file refused, naming the missing option ${scripts}/pythonand${scripts}/luaonly exist on Windows when the sample scripts feature is installed, and a${scripts}override points wherever an operator chose, so “import into a folder that is not there” was the normal case, not the corner case. The Web UI’s script upload goes through the sameadd --import, so it failed the same way.The command
ext-scr add --importwrites on Linux is now absolute:[/settings/external scripts/scripts] imported = /usr/lib/nsclient/scripts/imported.shThat is not a style preference.
CheckExternalScriptshands the value to the shell verbatim โ no${...}expansion, no search of the script folder โ and on Linux the launcher does not set a working directory for the child at all, so only an absolute path can work. On Windows the child does start in${base-path}with${scripts}directly below it, so the relativescripts\<name>still resolves and is still what gets recorded there.ext-scr list,py listandlua listhad a matching defect: a path that did not begin with${base-path}still had its leading separator sliced off, producing a rootlessusr/lib/nsclient/scripts/check_x.shthat named no file. Those entries are what the Web UI’s script list shows and whatshowis called back with, so they now stay absolute when they are not below the install base.
0.22.0¶
-
๐ ๐ฅ Action required Fleet bundle signatures changed shape: upgrade the fleet server before the agents. Only affects hosts enrolled against a fleet server (
nscp enroll); an installation that does not use fleet management is unaffected.A bundle’s Ed25519 signature used to cover the bare SHA-256 digest of its bytes, which bound nothing about which bundle those bytes were โ an old signed blob could be re-advertised under a new id, name or version and still verify. It now covers a canonical descriptor of the bundle’s identity:
nsclient-fleet/bundle-sig/v2 \0 tenant_id \0 id \0 name \0 version \0 format \0 sha256signed directly (Ed25519 hashes internally), with the fields taken verbatim from the desired-state response. The response therefore carries a new top-level
tenant_id, which the agent needs to rebuild the descriptor and cannot derive from its certificate.The two sides must agree, so:
- Upgrade the fleet server first, or at the same time. A server that signs the descriptor is already re-signing its stored bundles on startup, so no bundle needs re-uploading.
- An agent on this version talking to an older server logs
Bundle <id> failed verification: signature verification failedon every poll and keeps the configuration it last applied. Nothing unverified is applied and nothing already applied is removed, so the host keeps working โ it simply stops picking up changes until the server is upgraded. - An older agent talking to an upgraded server fails the same way, which is the case to watch for if you upgrade the server first and the agents later.
A desired-state response that carries bundles but no
tenant_idis now rejected as malformed rather than failing later as a bad signature, so the log names the real problem.See the security notice.
-
๐ ๐ฅ Check your setup A settings source, include or attachment that is not
https://is now refused. Nothing to do if your agents read a local configuration or use anhttps://settings server. The remote store is the agent’s whole configuration โ[/modules], the external script definitions, the submit clients’ credentials โ re-read at boot and on every housekeeping pass, and over plainhttp://nothing authenticates the server, so anyone on the network path decides what every agent pointed at it runs. All three paths that fetch one (the[settings]url inboot.ini, an[/includes]entry inside a fetched file, and an[/attachments]source) now refuse a url that is nothttps://โ a url with no scheme included โ and log why. An agent that already has a cached copy keeps running on it rather than booting empty. Where plain http is what you want, opt in per host inboot.ini:[tls] allow plaintext = trueEvery plaintext fetch is then logged as
INSECURE.On Windows,
boot.ini’s location is now the${boot-conf}path token rather than a literal baked into the build, so--path-override boot-conf=...relocates it the way the command line already documented and the way it has always worked on Linux. The default is unchanged: next to the executable.See http settings and the security notice.
-
๐ ๐ฅ Check your setup The raw protobuf web API is gone, and agent-to-agent checking now uses the versioned REST API โ which means it works.
POST /query.pbandPOST /settings/query.pbhave been removed. The first handed the agent’s core a message whose header the caller wrote, and the core reads the calling module and user out of that header, so a caller could pick the identity its request was attributed to; the second had been unreachable for several releases. Nothing that talks to an agent over HTTP used them: Icinga’scheck_nscp_apiasks forGET /query/{name}, which is untouched, and the web UI uses/api/v2.Their only consumer was NSClient++ itself, through
NSCPClientโ and that never worked: it passed the serialized message as the HTTP request target, so every request it made was malformed, and the password it read from the configuration was never sent at all.check_remote_nscp,remote_nscp_queryandremote_nscpforwardnow run the command on the remote agent throughGET /api/v2/queries/{command}/commands/execute, authenticating with the samepasswordheadercheck_nscp_apiuses. If you have anNSCPtarget configured, check three things:Setting Was Now path/query.pb(a single endpoint)/api/v2/queries(the collection the command is appended to)TLS off unless ssl = trueon unless no ssl = trueโ the REST API listens on TLSpasswordread, never sent sent, and the remote’s adminuser needs thequeries.executegrantIf you set
pathexplicitly, remove it or point it at/api/v2/queries.verify modestill defaults tonone, because an agent generates a self-signed certificate on first start; pointcaat the issuing certificate and setverify mode = peerwhere you can.exec_remote_nscpandsubmit_remote_nscpare removed rather than ported: both posted an NRPE-stylecommand!arg!argstring to the protobuf route, which the remote parsed as an empty message and answered with nothing โ and the submit path then reported success regardless. The REST API has no endpoint for either, so they now refuse with an explanation instead of discarding results silently. Use NSCA, NRDP or another submit client to send passive results.See the security notice.
-
๐ ๐ฅ Check your setup
check_tcpandcheck_sshnow verify the server certificate by default.verifydefaulted tonone, so a TLS check completed its handshake against any certificate at all and reportedok. It now defaults topeer, andca=defaults to the agent’s own trust bundle (${ca-path}, the same onecheck_httphas always used), falling back to the trust store OpenSSL was built with when that is empty. Checks against publicly issued certificates start validating out of the box; checks against a self-signed or internal certificate that used to reportoknow reporttls_handshake_failed.- If you check an internal or self-signed service, either point
ca=at the issuing CA โ a PEM bundle or a hashed directory both work โ or addverify=noneto keep the previous behaviour. The certificate keywords (ssl_expiry_days,cert_cn,cert_sans,cert_verify, โฆ) are readable either way: they are properties of what the peer served, not a trust decision. - Only TLS checks are affected, which means
ssl=true, astarttls=protocol, or one of the implicit-TLSservice=presets (spop,simap,ssmtp). A plaincheck_tcpis unchanged. sni=is now rejected when the connection is not TLS, instead of being silently ignored. It named the certificate to ask for and to verify against, so without TLS it asserted nothing while still reportingok.sans=on a plain connection now reportssan_missingrather thanok. A connection that served no certificate covers no required name โ which is what forgettingstarttls=on a submission port looks like.
Both commands are marked experimental, which is the flag that reserves exactly this: their options, keywords and output may still change while the shape of the check settles.
See the security notice.
- If you check an internal or self-signed service, either point
- ๐ Check your setup A request may no longer weaken the transport security of a credentialed
target, or re-address
submit_smtpmail. The client override guard now covers the keys that decide how a connection is protected โverify,insecure,insecure-skip-verify,no-psk,security,ssl,no ssl,tls-version,ca,certificate,certificate-key,allowed-ciphers,dhโ and, for SMTP,recipientandsender. Nothing to do unless callers pass one of those to a target that carries a password or token; repeating what the target configured is still fine, and the remedies are the same as forhost=: supply the credentials with the request, configure the variant as its own target and select it withtarget=, or setallow host override = true. The addressing keys have their own, narrower opt-in,allow recipient override = true.payload-length/buffer-lengthare clamped to what each protocol accepts, 65536 for NSCA and 1 MiB for NRPE, so an absurd value can no longer allocate gigabytes while every supported payload size keeps working. A failed CA load reports a generic message with the path and reason in the log. See the security notice.
- ๐ Check your setup A request may no longer reroute a credentialed NRDP target through its own proxy.
The client host-override guard now treats
proxy=andno-proxy=as moving the request, since a caller-chosen proxy receives it whole, configured token included (see the security notice). Nothing to do unless callers passproxy=onsubmit_nrdpagainst a target that carries a token; repeating the configured proxy is still fine, and the remedies are those ofhost=โ pass the token with the request, configure the proxied route as its own target and select it withtarget=, or setallow host override = true.
- ๐ Check your setup The fleet certificate pin is enforced as a pin. A pinned PEM that is the
server’s leaf certificate is now matched by its public key at every handshake;
one that is itself a CA keeps hostname verification, because a CA certificate
cannot identify a server on its own โ previously either kind was simply trusted
for any name. If enrollment handed out a CA rather than the leaf, that
connection now requires the certificate to match the name in
mtls_url. A pinned PEM that does not parse is refused rather than quietly falling back to ordinary verification. See the security notice.
- ๐ Check your setup
host=on a docker check must now match the configured endpoint. Which daemon the agent talks to is decided byendpointunder[/settings/docker]; a request repeating that value is accepted, one naming a different socket or pipe is refused. The agent would otherwise connect to any local socket or\\.\pipe\<name>a caller named and report back what happened, which is a read-only probe of the host asSYSTEMorroot. Check definitions that spell outhost=keep working as long as they name the configured endpoint; otherwise drop the argument or change the setting. See the security notice.
- ๐ Check your setup A NUL in a script argument is refused, and
ext-scr add --importreads only from the script folders.CreateProcessWreads the command line as a C string, so an argument containing a NUL truncated it and silently dropped every operator-fixed argument after the substitution point; that is refused now whateverallow nasty characterssays, because it changes what the launcher was asked to run rather than what the script sees.add --importcopied from any path the service account could read into the script folder, whereshowthen returned the bytes โ which made the show/delete sandbox hold only until someone carried a file inside it. Import sources are confined to the script root,${shared-path}and the upload staging area; copy a script into one of those first if a workflow relied on importing from elsewhere.PUT /api/v2/scriptsis unaffected. On Windows, a script that prints in exact buffer-sized chunks no longer parks a worker thread past the timeout, and a timed-out or forked script no longer leaks a process handle. See the security notice.
- ๐ External scripts no longer inherit the service’s handles. A script now receives only its own stdin and stdout/stderr pipe ends: on Windows through an explicit inherit list (spawns are serialised on the XP build, which has no such list), on Unix through close-on-exec pipes and a close of every descriptor above stderr in the child. Previously a concurrently running script’s output pipe, and every other inheritable handle of the service, crossed into each child (see the security notice). Nothing to do.
-
๐ Check your setup External scripts with
user,domainorpasswordset are refused on Linux. The run-as keys of a script section are implemented by the Windows launcher only; the Linux launcher ignored them and ran the script as the service account (root on a manualnscp servicerun), so a script sandboxed withuser = nobodywas not sandboxed at all. Such a command now returns UNKNOWN with a message pointing atsudoand the script does not run. If you set these keys on Linux, remove them and put the identity change in the command itself, granting it insudoers:[/settings/external scripts/scripts/check_as_nobody] command = sudo -n -u nobody /usr/lib/nagios/plugins/check_somethingWindows is unaffected. See the security notice.
- ๐ Check your setup The web server sends browser hardening headers, can negotiate TLS 1.3 on
Linux, and stops minting a session token per request. Every response now
carries
X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: no-referrerand a content security policy, plusStrict-Transport-Securityover TLS โ so a page that embeds the agent’s UI in a frame will stop working, which is the point.[/settings/WEB/server]gainstls version(default1.2+) andallowed ciphers, honoured on the beast backend that every Linux package uses; it was pinned to TLS 1.2 only before, and ignored both keys. The vocabulary is the one the NRPE and NSCA listeners use, with one exception:tls version = sslv3is refused rather than started, because this listener never serves SSL 3.0 and pinning the range to it would mean a listener that accepts no handshake at all. A value this listener cannot honour โ an unknown version, or a cipher list OpenSSL rejects โ stops it starting rather than silently leaving the defaults in place. The mongoose backend cannot apply either key and logs that it is ignoring a value the operator set; it stays quiet about one left at its default. A token is issued only by the login routes now, so a monitoring poll authenticating with Basic auth no longer fills the 4096-entry store and evicts live UI sessions โ a script that logs in through Basic auth and reuses a token must take it fromGET /api/v2/login. The session credential is no longer echoed back as a cookie nothing read, and the UI keeps its bearer insessionStorage, so a new browser tab asks for credentials again./api/v2/scripts/<runtime>accepts onlyext,pyandlua(which now works); anything else is 400. See the security notice.
- ๐ The web server’s in-memory log buffer is capped, logging out revokes the
session token, and
nscp web install-uino longer stages its download in${temp}. Nothing to do. The log view keeps the newest 1000 entries instead of growing without bound (an unauthenticated peer could previously add one entry per rejected request, for the life of the process); the error count on the badge still counts every error the agent reported. Logging out of the web UI now callsDELETE /api/v2/login, so the bearer token stops working immediately rather than at the end of its eight-hour life โ worth knowing if you have a script that logs in, logs out, and then reuses the token.nscp web install-uiverifies the bundle in memory and writes it once into a private directory under the web path, so nothing lands in the shared temp directory. See the security notice.
- ๐ง ๐ Check your setup
nscp nrpe installwritesuse ssl, and the insecure preset’s cipher string says what it is. The install command used to writessl = true, a key the server never reads: on a host whereuse ssl = falsehad been set previously, it claimed encryption and client-certificate authentication while the listener stayed in plaintext. Check that value if you ran the command on such a host. The legacy (insecure = true) cipher default drops its!ADH, which never excluded the anonymous elliptic-curve suites and could not have excluded them without breaking a mode that loads no certificate; the string now matches whatnrpe install --insecurewrites. The secure default gains!aNULLbeside!ADH, which changes nothing in practice โ OpenSSL’s default security level already refuses anonymous suites. See the security notice.
- ๐ง ๐
allowed hostsfinally understands the*ranges it has always advertised.192.168.1.*means192.168.1.0/24,192.168.*means/16,10.*means/8, and a bare*is every address. Until now such an entry threw out of the address parser and โ with the defaultcache allowed hosts = trueโ took the whole module with it, so the listener never started. A*in the middle of an address (192.*.1.1), or one combined with an explicit/mask, is reported as a configuration error, and an unparseable numeric entry is now an error beside the others rather than an exception. Nothing to do; if you have been avoiding*because it stopped a listener, it works. See the security notice.
-
๐ Check your setup The shared
/settings/default/passwordis always redacted, and followsuse credentials. Nothing to do on a default install. The key is now registered sensitive by the core rather than by whichever ofWEBServer,NSCAServerandNSClientServerhappens to be loaded, sonscp test’ssettingsdump,nscp settings --list/--showand the REST read paths answer***for it on every agent instead of only those running one of those modules. Read the value out ofnsclient.iniwhen you need the secret itself. Two consequences if you rely on the old behaviour:- Tooling that scraped the shared password back out of one of those listings
now receives
***. - On Windows with
use credentials = true, the next write of the key (anscp settings --update, or any change made through the REST or CLI settings writers) moves it into the Credential Manager and leaves a reference innsclient.ini, the same as every other sensitive key. Agents that do not enable credentials are unaffected, and the key is not rewritten until something writes it.
See the security notice.
- Tooling that scraped the shared password back out of one of those listings
now receives
-
๐ ๐ง Check your setup A round-robin counter with a zero buffer size is now refused. From the security notice; nothing to do unless you have one of these:
- A Windows counter with
collection strategy = rrdandbuffer size = 0, or abuffer sizethat does not parse, is skipped and named in the log instead of being loaded with a buffer that holds nothing. Remove the setting to get the 60m default, or set a real window. - A Lua binding given a word where it expects a number reads it as
0rather than terminating the agent.Settings():get_int(path, key, "n/a")returns the key’s value with0as the default, where it used to be fatal. - A Python string that cannot be encoded as UTF-8 comes back with the offending characters replaced instead of crashing the agent.
- A Windows counter with
- ๐ An Elastic or Op5 submission over an unverified
httpslink now says so in the log. When the endpoint’sverify moderesolves to no peer verification and credentials are configured, both modules log a message naming the endpoint โ the credentials then go to whichever server answers โ matching what the Icinga, NRDP and NRPE clients already do. It is logged once per module load, and the connection itself is unchanged. The Icinga client’s own warning was repeated on every submission and is now logged once per target instead. The shared--verify,--ca,--certificate-keyand--allowed-ciphershelp texts (NRPEClient,NSCPClient,NSCAClient,CheckMKClient,NSCANgClient) were corrected at the same time: three of them read “Client certificate format”. See the security notice.
- ๐ Check your setup The DEB and RPM packages now install
nsclient.iniand the log directory owner-readable./etc/nsclientbecomesroot:nsclient 0750,/etc/nsclient/nsclient.iniroot:nsclient 0640,/var/log/nsclientnsclient:nsclient 0750and the log file0640; they were world-readable, and the configuration file holds the web admin password, the NRPE/NSCA passwords and the submit-client tokens in plaintext. The post-install scripts apply this on upgrade as well, so an existing host is fixed by upgrading. If a local script or a monitoring user reads either path without being root or in thensclientgroup, add it to that group first โ otherwise it will start getting permission denied. Windows is unaffected. See the security notice.
- ๐ The Windows release build pins every third-party action to a commit and
verifies what it downloads. Nothing to do โ this concerns how the released
artifacts are built, not an installed agent. Third-party actions are pinned to
commit SHAs (one ran from a branch, in the same job that holds the code-signing
credentials), Google Test is checked against the commit its tag names, and every
downloaded dependency goes through
.github/dependency-checksums.txt: a mismatched digest fails the build, and so does bumping a version without recording one. See the security notice.
-
๐ฅ ๐ Check your setup
/api/v2/openmetricsnow serves a conformant, self-describing OpenMetrics document, and every metric name changes. Only affects scrapes of/api/v2/openmetrics. The JSON endpoints (/api/v2/metrics,/metrics), the web UI dashboard, Graphite, collectd and Pythonsubmit_metricsreport the same keys and the same values as before โ the metric key is untouched and stays authoritative for all of them.The endpoint used to paste the JSON keys into the exposition verbatim, so names carried
.,%, spaces and colons (system_mem_commited.avail,system_cpu_core 0.idle,disk_free_C:.total), there was no# TYPEand no# EOF, values were truncated to six significant digits โ 16 GB of memory scraped as1.6554e+10โ no module declared what any of its readings meant, monotonic counts were typed as gauges sorate()was unsafe on them, and string-valued metrics (uptime, boot time, MAC address, power source) were dropped entirely. A strict parser rejected the body, and the scenario page told you to repair the names withmetric_relabel_configs.Three things changed, and each of them renames families.
1. Names are rewritten to the OpenMetrics grammar.
JSON key Metric name system.mem.physical.%system_mem_physical_percentsystem.cpu.core 0.idlesystem_cpu_core_0_idledisk.free.C:.totaldisk_free_C_total%becomes the wordpercent, everything else outside[a-zA-Z0-9_]becomes_, runs collapse to one, and a name that would not start with a letter borrows ametric_prefix (a leading underscore is reserved). Every family carries a# TYPEline, the body ends with# EOF, values keep their full precision, and the response is typedapplication/openmetrics-text; version=1.0.0when the scraper asks for it.2. Every metric carries a description, a type and a unit, and declaring a unit renames its family again:
# HELPon every built-in family, and# UNITwherever the value is measured in something.- Counters are typed as counters โ
workers.{jobs,submitted,errors},scheduler.{jobs,submitted,errors},system.process_history.<exe>.times_seenand the real-time filter counts. Their sample carries the_totalsuffix the specification reserves for them. - Strings come back as an
_infofamily, thenode_uname_infoshape:system_uptime_info{uptime="1d 12:30",boot="2026-09-13 01:15"} 1.
A family that declares a unit has to end in it, so the name the key alone would give gains a suffix:
Name from the key alone Name it is served under system_mem_physical_totalsystem_mem_physical_total_bytessystem_cpu_total_idlesystem_cpu_total_idle_percentsystem_uptime_ticks_rawsystem_uptime_ticks_raw_secondsdisk_free_C_totaldisk_free_C_total_bytesworkers_jobsworkers_jobs_totalA name that already ends in its unit keeps it, which covers every
.%key (system_mem_physical_percent) and the clock frequencies. A per-second rate declares no unit and gains no suffix either (system_network_eth0_received,disk_io_sda_read_bytes_per_sec).3. Per-instance metrics are one family with a label, so every per-core, per-NIC, per-drive and per-process family name changes again. Rewriting the key alone would leave one family per core, which is what the endpoint has always served: the family names then depend on how many cores a host has,
sum by (core)has nothing to group on, and a Grafana variable has no label to bind to.# one family per core, as a name-only rewrite would leave it system_cpu_core_0_idle_percent 93 system_cpu_core_1_idle_percent 91 system_cpu_total_idle_percent 95 # what is served # TYPE system_cpu_idle_percent gauge system_cpu_idle_percent{core="0"} 93 system_cpu_idle_percent{core="1"} 91 system_cpu_idle_percent{core="total"} 95The same move applies to every producer with an instance in its key:
Bundle Label Value system.cpucore0,1, โฆ andtotalfor the aggregatesystem.cpu_frequencycputhe sysfs core on Linux; the processor DeviceID(CPU0) on Windowssystem.networknicthe interface as the OS names it โ the adapter description on Windows system.temperaturezonethe thermal zone or sensor system.batterybatterythe battery; absent for a single unnamed battery system.process_historyexethe executable name system.metricspdh_instancethe PDH instance, for a counter configured with instances disk.iodiskthe device disk.freedrivethe drive or mount point pdh_instancerather thaninstance: Prometheus attaches its owninstancelabel (the scrape target) to every sample, and under the defaulthonor_labels: falsean exportedinstanceis renamedexported_instance, so a query againstinstancewould match the host rather than the counter.The
_infofamilies gain the same labels, which is what splits them per instance: one NIC’s link state, MAC address and speed now share a series keyed bynic="โฆ", instead of every NIC’s strings piling onto one series under label names likeeth0_status. Windows and Linux spell a CPU core differently in the JSON key (core 0andcore_0); neither spelling reaches the label, which is the bare0on both, so one query works across a mixed fleet.What to do:
- Drop any
metric_relabel_configsblock that rewrote dots to underscores. The agent does exactly that itself now, so the rule no longer matches. - Update dashboards, recording rules and alerts that name the old series.
Every name moves at least once, and a per-instance one turns into a label:
system_cpu_core 0.idlebecomessystem_cpu_idle_percent{core="0"},disk_free_C:.totalbecomesdisk_free_total_bytes{drive="C:"}andworkers_jobsbecomesworkers_jobs_total. Most of them get shorter, and a query that used to enumerate instances can now aggregate. - Exclude
core="total"from anything that aggregates over cores. It is the all-cores aggregate, mirroring thesystem.cpu.total.*JSON key, sosum without (core) (system_cpu_idle_percent)double-counts. Writesystem_cpu_idle_percent{core!="total"}instead. - Check what you are taking a
rate()of. Only the counters listed above are monotonic.system_network_<nic>_totalis a per-second rate andsystem_os_updates_countis how many updates are pending right now; both go down again, and arate()of either was always nonsense โ it is just visible now that the honest counters are labelled as such. -
If dashboards cannot be updated before the upgrade, set
[/settings/WEB/server] openmetrics format = legacywhich reproduces the pre-0.21 body byte for byte, with no metadata at all. It is deprecated and will be removed in a future release, so use it as a migration window rather than a setting to leave in place.
Two keys can now want the same metric name (
mem.used.%andmem.used percentboth render asmem_used_percent). The first metric of the snapshot keeps the name and the rest are dropped โ emitting both would be the same series twice, which costs the scraper the whole body rather than one metric. Each distinct collision is logged once (again after a settings reload) naming the metric that was dropped and the name it collided on. Nothing shipped produces such a pair; a predefined PDH counter or a Python script can.Smaller changes that come with it:
-
Windows only:
system.mem.page.%andsystem.mem.physical.%report different numbers now, because they were reporting the wrong thing. Both divided the commit charge by the commit limit while checking their own total for zero, so they published the commit figure under a page-file and a physical-memory name โ and divided by zero on a machine that reported a page file or physical memory but no commit limit. Each reads its own numbers now. The JSON keys and the metric names are unchanged; only the values are, and they were wrong before. An alert threshold tuned against the old reading needs re-checking. -
The two negotiated bodies are no longer identical. The endpoint already answered
application/openmetrics-text; version=1.0.0ortext/plain; version=0.0.4depending on the request’sAcceptheader. The two specifications disagree about what a counter’s metadata lines name โ the family in OpenMetrics (# TYPE workers_jobs counter, sampleworkers_jobs_total), the sample in the older format (# TYPE workers_jobs_total counter) โ so the agent renders both and serves the one matching the type it answers with. Sample lines are identical in both. Nothing to do; a scraper that negotiates gets the richer form as it always did. -
Predefined PDH counters can describe themselves.
[/settings/system/windows/counters/<name>]takes two new optional keys,helpandunit, which become the# HELPand# UNITlines of that counter’s metric.helpdefaults to the counter path. Nothing else reads them, and a counter that sets neither behaves exactly as before. -
Python
fetch_metricsaccepts a dict per value.{"value": 42, "help": "โฆ", "unit": "bytes", "type": "counter"}in place of a bare number, and"labels": {"queue": "inbound"}to label a metric. Plain numbers and strings keep meaning exactly what they did, and an out-of-tree C++ module callingnscapi::metrics::add_metric()is unaffected. -
/api/v2/metrics?meta=1serves the same keys and values with their help text, unit, type and labels under ametadataobject, for a dashboard that wants to print a unit rather than a bare number. Withoutmetathe endpoint is byte for byte what it was. -
GraphiteClient can send the labels as carbon tags.
metric tags = trueon a target appends;core=0to the metric path, which is otherwise unchanged. Off by default: a carbon older than 1.1 has no tag support and storespath;core=0as the metric name. -
CollectdClient mappings can read the labels and the types. A variable set to
label:coreexpands to every distinct value of that label, instead of a regular expression over the flat keys that has to know whether the platform spells a corecore 0orcore_0; and a metric expression spelledauto:sends whatever the producing module declared a counter as a collectd DERIVE and everything else as a GAUGE. The built-in default mappings are unchanged. Note that a label includes the aggregates the exposition labels โlabel:coreyieldstotalalongside0,1, โฆ โ so a template built from it can name a metric that does not exist, which is skipped rather than sent. -
A collectd value list naming a metric the snapshot does not carry is no longer sent as a zero. The value expression resolved a missing key to an empty string and forwarded the
0that parsed out of it, so a mapping naming a metric this platform or configuration never produces reported a measurement nobody took โ which is what the platform-specific default mappings exist to avoid. Such a value list is now skipped, in whole: a collectd value list is positional, so dropping one value of several would have the receiver read the next metric’s number under this one’s type. Aderive:expression also splits on,likegauge:always did, instead of looking the wholea,bstring up as one key and sending a single zero. Only hand-written mappings are affected, and only where they were reporting zeroes.
See the REST metrics reference and the Prometheus scenario for the full rules.
-
๐ฅ New module:
GearmanClient, a Mod-Gearman worker and result channel. Nothing to do on an existing install โ the module is optional and disabled by default. Enabling it lets a Naemon or Nagios Core keep scheduling its checks while this agent pulls them off agearmandjob server and answers them as native queries, with no inbound port on the monitored host. It speaks both flavours of the protocol: ConSol’s Mod-Gearman (Naemon) and Nagios Enterprises’ Nagios-Mod-Gearman (Nagios Core 4.5+).Two ways to deploy it, both configured under
/settings/gearman/worker:mode = agent(the default) answers only for the host it runs on; a job for any otherhost_nameis answered UNKNOWN rather than executed.mode = proxyanswers every check on the queues it registered, whichever host the core meant it for โ one domain-joined Windows box running the checks of a whole hostgroup throughcheck_nrpe,check_wmiand the rest. A proxy reaches everything its own credentials reach, so keep the key to those queues to the hosts that should have it.
The same module submits passive results into the core’s result queue (
/settings/gearman/client, channelGEARMAN, commandsubmit_gearman), which is what lets a Mod-Gearman installation drop NSCA. The two halves are independent: configure either one and leave the other empty.Two things a Nagios operator usually has to unlearn when writing the
check_commandon the core, since neither fails in a way that names the cause:- write
host=$HOSTADDRESS$, not-H $HOSTADDRESS$โ a two-character first token puts the agent’s argument parser into key-value mode and the check answers with a help screen; - write
warning=load gt 80, notwarning=load>80โ>is a metacharacter andallow nasty charactersisfalseby default, as in NRPEServer.
On Windows the module is part of the Various client plugins feature of the installer. See Mod-Gearman for the full setup on either core, and Securing NSClient++ for the shared-key model.
The module is marked experimental, and beyond the usual meaning of that mark โ settings, queue handling and output may still change โ it has not been tested at scale. It is verified end to end against a real
gearmandon both cores, but not against hundreds of hosts, a deep job backlog, or a proxy answering for a large hostgroup. Roll it out to a slice of the estate first and keep the transport you have until you are satisfied.
-
๐งช New modules and check commands are now marked experimental. Nothing to do on an upgrade: every check keeps working exactly as before. A module or check command that is new enough that its options, filter keywords and output may still change now says so โ
nscp testappends(experimental)to the name inqueries,aliases,listandplugins(and shows aStatus:line indesc), the web UI shows an Experimental chip on the module and query pages, the REST API reports it as anexperimentalfield on modules, queries and aliases, and the reference documentation renders a marker in the command tables plus a note on the command itself.The recently added
check_*commands ofCheckDisk,CheckDocker,CheckNet,CheckNSCPandSchedulerare marked, as are theCheckMSSQL,CheckMySQL,CheckSecurity,CheckWindowsApps,GearmanClientandNSCANgClientmodules in full.The marker is not a warning that a check is broken โ it is a statement about stability: pin the options and syntax you depend on, and re-read the command’s reference page after an upgrade. Marks are removed as the commands settle.
If you build your own modules, declare it in
module.jsonโ"experimental": trueinside"module"for the whole module, which covers every command it registers, or on a single command entry when only that check is new:"commands": { "check_new_thing": { "description": "โฆ", "experimental": true } }
-
๐ฆ Check your setup Common code moved out of the individual plugins into shared libraries. Code that every plugin used to compile privately now lives in one place, so the install is smaller: about a fifth on Debian and RedHat, and on Windows every TLS-capable module sheds the private copy of OpenSSL that used to make up most of it. New libraries ship next to
nscp.exe:nscp_net.dll(socket and TLS helpers),nscp_client.dll(the client-side command line handling the sender modules share),nscp_json.dll(Boost.JSON), and OpenSSL itself aslibcrypto-3-x64.dllandlibssl-3-x64.dll(libcrypto-3.dllandlibssl-3.dllon 32-bit) โ one copy for the whole service, where before each of NRPE, NSCA, check_mk, the web server, the HTTP clients and the checksum checks carried its own.plugin_api.dllalso grew, having taken over the settings helpers and the program-options helpers that each plugin used to carry its own copy of. The legacy Windows XP build is not affected: it still links everything statically.Nothing to do when installing from the MSI or from the Debian/RedHat packages โ they install the new libraries for you, and the modules themselves are unchanged from the outside. Only a hand-rolled deployment needs to check: if you copy
modules\*.dllinto place yourself rather than installing a package, copy the new libraries (the OpenSSL DLLs included) from the installation root as well, or the modules that need them will fail to load.
-
๐ฆ Windows on ARM is packaged again, without the Python module. Releases carry
NSCP-<version>-ARM64.msiandNSCP-<version>-ARM64.ziponce more; ARM64 was built for one release in April 2026 and then withdrawn because the build failed. Nothing to do on x64 or 32-bit Windows, and an ARM64 machine running the x64 package under emulation keeps working โ the native package is simply faster.One difference from the x64 package: the ARM64 build does not include
PythonScript, nor the embedded Python runtime that comes with it. The package is cross-compiled on an x64 runner and there is no ARM64 CPython to embed, so the module and itspython*.dll/python*.zipare absent from the MSI and the Python Scripting feature does not appear in the installer. An ARM64 host that needs internal Python scripts should run the x64 package under emulation.CheckExternalScriptsis unaffected and can still run a system-installedpython.exe.One more difference, and this one needs action on a fresh machine: the ARM64 MSI does not install the Visual C++ runtime. The x64 and 32-bit installers bundle it as a merge module, but Microsoft ships no ARM64 merge module for the toolset this is built with, so there is nothing to bundle. Install the Visual C++ Redistributable for ARM64 before the agent, or the service will fail to start with a missing
vcruntime140.dll. Most machines already have it โ anything that has run a recent ARM64 desktop application will โ and upgrades over an existing install are unaffected.
-
๐ฆ A Raspberry Pi OS package. Releases now carry
NSCP-<version>-debian-trixie-arm64.deb, built on Debian 13 (Trixie) โ the base of current Raspberry Pi OS 64-bit โ for Raspberry Pi 3 and newer, and for Debian 13 arm64 in general. Nothing to do on an existing install; the Ubuntu packages are unchanged and keep working where they already do.There is no 32-bit package. Raspberry Pi OS has defaulted to 64-bit since Bookworm, and a 32-bit build would have to be either a slow emulated one or a Debian
armhfpackage that still leaves the ARMv6 boards (Pi 1, Pi Zero W) out. On a 32-bit Raspberry Pi OS install, either reimage to 64-bit or build from source.The Debian package is built without the .NET SDK, which Debian does not package, so it ships without the managed (C#) plugin API. The Ubuntu packages still include it.
- ๐ง The web bundle is built with
npm ciand gated onnpm audit.npm installtolerates a lockfile that has drifted frompackage.jsonand rewrites it, so the bytes in the web zip and in the MSI’s bundledweb/distwere not guaranteed to matchpackage-lock.jsonโ the.sha256manifest the installer verifies proved integrity in transit, not provenance of the dependency tree.npm cifails on drift instead, and a new step runsnpm audit --omit=dev --audit-level=highso a known-vulnerable runtime dependency stops the release build. Nothing to do; contributors whosepackage-lock.jsonis out of date will see the build fail rather than see it quietly rewritten.
-
โฑ๏ธ ๐ง Check your setup Reloads now wait for the checks that are running, and a module can no longer unload or restart itself from inside a request it is serving. Nothing to do on a default install; the changes are visible only in a few specific setups.
- A settings reload holds new checks off a module while its
loadModuleExapplies the new configuration, and waits up to five seconds for the checks already inside it to return. A reload is therefore no longer instantaneous on a busy agent, and a check arriving during one waits rather than running against half-applied settings. If a check holds a module for longer than five seconds the reload proceeds anyway and logs which module it was. - Unloading the module that is serving the request is refused and answers with
an error. In practice this is
POST /api/v2/modules/WEBServer/commands/unload(or the same throughput_moduleor the console), which previously took the agent down with it. UnloadWEBServerfrom the command line or restart the service instead. - Reloading a listener module from inside a check that the same listener is
serving - a script calling
core.reload("NRPEServer")from a check invoked over NRPE - is refused with an error instead of leaving that listener dead. Reload it from another transport, or reload the service as a whole. A reload that does not touch the module serving the request is unaffected and still applies before the call returns. - Configuration downloaded over HTTP (
[/settings] 1 = http://...) now gives up on a read or write that stalls for 30 seconds instead of waiting indefinitely. A settings server that used to take longer than that to answer will now fail the refresh and keep the cached copy. - Submissions over NRPE, NSCA, NSCP and check_mk apply the configured timeout to connecting and to the TLS handshake, not just to the exchange. A target that accepts a connection and then goes quiet now fails its timeout instead of holding a scheduler thread.
- A settings reload holds new checks off a module while its
-
๐ฅ Check your setup
check_ntFILEAGEnow checks one file instead of a directory of them.FILEAGEanswers with a single age, taken from the first performance value the underlying check produced. The legacy request mapped ontocheck_files, which walks a whole tree, so a directory argument reported whichever file the walk happened to emit first โ not the oldest, not the newest, just arbitrary. The number looked authoritative and meant nothing.The request now maps onto
check_single_file, which stats exactly one path, so there is only ever one candidate and the age is defined. AFILEAGEnaming a single file, which is what the check was always for, is unaffected and reports the same age as before. AFILEAGEnaming a directory now fails with an error instead of returning an arbitrary file’s age, and one naming a missing file reports that it was not found. If a check depends on passing a directory, point it at the file it actually means.check_single_filelives inCheckDisk, ascheck_filesdid, so no extra module needs enabling.
-
๐ง Check your setup The Windows installer no longer rewrites NRPE transport security it did not configure. The MSI offers two NRPE presets, insecure and secure, and used to apply one whenever it could not recognise the mode an existing or imported configuration was in. It recognised only
insecure = trueandverify mode = peer-cert, so two perfectly ordinary setups fell through:- A listener running TLS without client certificates (
verify mode = none).msiexec /i NSCP-โฆ.msi IMPORT_CONFIG=โฆcame back with four keys under[/settings/NRPE/server]that the imported file never asked for:verify modeforced topeer-cert,tls versionlowered totlsv1.2+, plusinsecure = falseand an emptyssl options(#1558). - A listener configured without either key โ
port = 5666and nothing more, running onNRPEServer’s own defaults. An upgrade read that as “nothing configured here” and wrote the secure preset, so a listener that had never required client certificates started demanding them.
A configuration whose mode was recognised kept its mode, but still had
tls versionandssl optionsrewritten.The installer now reads what the configuration says and applies a preset only where it would overwrite nothing: a fresh install with no NRPE listener configured, or an operator who moves the radio button or passes
NRPEMODE=LEGACY/NRPEMODE=SECUREon the command line. It also no longer writestls versionorssl optionsat all โ both were being set to the valueNRPEServeralready defaults to, so they added nothing to a new install while overwriting an explicit choice on an existing one.Check
[/settings/NRPE/server]after upgrading if you install or upgrade on Windows with an NRPE configuration of your own, in particular one imported withIMPORT_CONFIG:verify modeis now left as your configuration has it, and is no longer added where you never set it. If an earlier install silently moved you topeer-certand you have since issued client certificates, keep it by leaving the setting in your configuration โ it will not be re-added for you.tls versionis left alone, so a pinnedtls1.3survives the install instead of being replaced withtlsv1.2+.- A fresh install is unchanged: it still gets
verify mode = peer-cert, orinsecure = truewithNRPEMODE=LEGACY.
- A listener running TLS without client certificates (
-
๐ง Check your setup The PDH counter browser now narrows on every filter, and ignores case. Nothing to do unless you have a script or a habit built on the counter browser (
nscp sys -- --list โฆ, orexec CheckSystem --list โฆat thenscp testprompt). Two things changed about which counters it lists:--list,--filterand--counterused to share one variable, so whichever came last won and the others were silently discarded:--list SQL --filter Databaseslisted everything matchingDatabases, including counters with noSQLin them at all. Every filter now has to match, so the same command lists what matches both โ a narrower result than before.--filteris also repeatable now, so a listing can be cut down one word at a time.- Matching is now case insensitive, where it used to be case sensitive.
--list diskused to find nothing at all; it now finds the same counters as--list Diskโ a wider result than before. PDH capitalises its own names inconsistently and a localised Windows spells them in another language, so having to guess the casing before getting any output was a trap rather than a feature. (The fold is ASCII: the non-ASCII part of a localised name still has to be typed as it is spelled.)
nscp sys -- --list disk --all --filter queue --filter avg. \PhysicalDisk(_Total)\Avg. Disk Queue Length \PhysicalDisk(_Total)\Avg. Disk Read Queue Length \PhysicalDisk(_Total)\Avg. Disk Write Queue LengthUnchanged otherwise: a substring test against the whole
\object(instance)\counterpath, and a--filterwith no value is still accepted and still filters nothing.
0.21.0¶
-
๐ ๐ง The checks whose argument decides what data is read can now be told which files, queries and counters a caller may ask for. Nothing to do on upgrade: they all default to
any, which is exactly what earlier releases did. Those checks take an argument that decides what data is read, and the agent reads it with its own privileges โ so where callers choose the argument (NRPE withallow arguments = true, or a REST user not on the no-argumentsrestrictedrole) an unrestrictedfile=is a general file-read primitive. Each module gained a mode setting and an allow list:Check Section Mode setting Allow list check_logfile[/settings/logfile]file accessallowed filescheck_wmi[/settings/wmi]query accessallowed classes,allowed namespacescheck_pdh[/settings/system/windows]counter accessallowed counterscheck_files,check_single_file,check_disk_write[/settings/disk]file accessallowed filescheck_registry_key,check_registry_value[/settings/system/windows]registry accessallowed registry keyscheck_eventlog[/settings/eventlog]log accessallowed logsThe modes are
any,allowed(only what matches the list; experimental, since it parses what the caller sent) andpredefined(the secure option: only names you configured โ[/settings/logfile/files],[/settings/wmi/queries],[/settings/disk/files],[/settings/system/windows/registry],[/settings/eventlog/logs], and forcheck_pdhthe counters already in[/settings/system/windows/counters]). Registry and event-log entries are hierarchical: an entry covers that key or channel and everything below it, matched on whole name segments. Configured names resolve in every mode, so you can name your checks first and tighten the mode afterwards. Some arguments only tighten once a mode is set: acheck_wminamespace=may then no longer be moved offroot\cimv2unlessallowed namespacessays so,check_wmitarget=must name a target defined in[/settings/wmi/targets],check_registry_*refusescomputer=so only the local registry is read, andcheck_eventlog’s defaultApplication/Systemchannels go through the gate like any other. See Restricting what a check may read, the securing guide and the security notice.
-
๐ ๐ง Check your setup A new built-in WEB role,
metrics, and amonitoringrole that can finally scrape. The metrics endpoints are gated bymetrics.list(/api/v2/metrics) andopenmetrics.list(/api/v2/openmetrics), but the bundledmonitoringrole grantedmetrics.getโ a privilege nothing checks, so amonitoringuser was answered403on both endpoints.monitoringnow grants the two real ones instead:[/settings/WEB/server/roles] monitoring = public,queries.execute,aliases.list,login.get,metrics.list,openmetrics.list metrics = public,metrics.list,openmetrics.list,login.getThe new
metricsrole is for a Prometheus scraper: it reads the two endpoints and holds noqueries.execute, so it cannot run checks.Roles already written to
nsclient.iniare never rewritten, so an existing install keeps the role strings it has โ including amonitoringline still carrying the inertmetrics.get. Update that line by hand (or assign the newmetricsrole) if you want those users to scrape. Nothing widens by itself on upgrade; see the security notice.
- ๐ Check your setup Fleet: encrypted bundles are now opened by the agent. Nothing to do unless
you use the fleet server’s Encryption key feature. A bundle the operator seals
in the browser (
format: enc-v1) used to be refused by the agent as an unreadable archive; the agent now opens it with a key you hand it out of band, and the server never sees that key. Give the key at enrollment withnscp enroll --bundle-key <key>or theFLEET_BUNDLE_KEYinstaller property, or to an already enrolled host withnscp enroll --update-bundle-keys --bundle-key <key>(several--bundle-keywhile rotating; on Windows, re-running the installer with onlyFLEET_BUNDLE_KEYdoes the same). The key is stored in the enrollment manifest (agent-state.json) beside the host’s private key, never innsclient.ini.nscp enroll --require-encrypted-bundles(orFLEET_REQUIRE_ENCRYPTED_BUNDLES=1) refuses every bundle that is not sealed, for a fleet server you do not trust with plaintext configuration; it is stored in the manifest too, so the server cannot switch it off. See Central management with NSClient Fleet and the security notice.
- ๐ Check your setup
check_filesno longer follows file symbolic links on Windows. The scan already skipped directory reparse points (junctions, mount points, directory links); it now skips file symlinks too, as the Linux scanner always has, so a link planted inside a scanned tree can no longer make the checksum,line_countorversionkeywords read a file outside it. Deduplicated and cloud-backed files are still counted. If you relied oncheck_filescounting file symlinks, point it at the link targets instead. Path allow-list wildcards also changed:*and?in anallowed filesentry no longer cross a directory separator, soC:/logs/*.logcovers the files inC:/logsonly; writeC:/logs/**.logfor the subtree. See Restricting what a check may read.
-
๐ง A new built-in WEB role,
restricted, runs checks without arguments. Nothing to do on an existing install: the role is added to[/settings/WEB/server/roles]but no user is assigned to it, andclient,monitoringand any custom role holdingqueries.executekeep passing arguments exactly as before. The new role holdsqueries.execute.noargsinstead ofqueries.execute, which is the REST equivalent of the NRPE server’sallow arguments = false: the caller may run the checks the agent defines, but a request carrying any query-string parameter is refused with403 Arguments are not allowed for this user. Neither grant implies the other, so the role can never widen into the full privilege;full(*) still confers both.[/settings/WEB/server/users/monitor] role = restricted [/settings/check helpers/alias] check_root_disk = check_drivesize drive=/ warning=free<10% critical=free<5%Give a restricted caller the checks that need arguments as aliases, as above, so the arguments live in your configuration. Note that every query parameter counts as an argument, including a session token passed the legacy way as
?TOKEN=, so such a client must authenticate with a header.
- ๐ง Fleet:
nscp enroll --unenrollleaves the fleet. Nothing to do. Until now leaving meant deleting the enrollment manifest, the fleet directory and the[/includes] fleetentry by hand;nscp enroll --unenrolldoes all three and reports each, then a service restart stops the sync. It is a local act: the host stays listed on the fleet server until you remove it there. See Central management with NSClient Fleet.
- ๐ง Check your setup
check_logfile files=now does what it says. The comma-separated form offile=was parsed before the check read its arguments, so it has been ignored for as long as anyone is likely to have tried it: a check passing onlyfiles=failed with Need to specify at least one file, and one passing bothfile=andfiles=silently read only thefile=entries. Both now work, and the two are one list rather than one overriding the other. If a check of yours passes both, it will read the union from this release on - and every name goes throughfile accessin[/settings/logfile]exactly like afile=one. Nothing to do unless you were relying on the entries being dropped.
- ๐
Check your setup
check_installed_softwaretakes dpkg install dates fromdpkg-query. Nothing to do on a current Debian or Ubuntu host: the date is the same as before, now read fromdpkg-query’sdb-fsys:Last-Modifiedfield instead of from dpkg’s internal database. That field needs dpkg 1.19.3 or later; with an older dpkginstall_dateis now left unset, so an expression on it such aswarning=install_date > -7dno longer matches anything there. The check also returns UNKNOWN when dpkg cannot read a package’s file list for a reason other than the file being missing, where it used to leave only that package undated.
0.20.0¶
- ๐ Action required Generated TLS private keys are now created readable only by the agent, and a
generated CA no longer ships its private key to clients. Certificates
NSClient++ generates itself โ including the one a default NRPE start creates
when
certificatepoints at a missing file โ used to land at0644with an unencrypted key inside; they are now0600(restricted DACL on Windows), and a generated CA keeps its key inca-key.pembeside the distributableca.pem. Existing files are not touched: runchmod 600 /etc/nscp/security/certificate.pem, and if you distributed a generatedca.pem, regenerate that CA and re-issue client certificates โ anyone holding the old file can mint certificates that passverify mode = peer-cert. See the security notice.
- ๐ Check your setup The client host-override guard now also covers a credential kept inside the
target’s address. Two ways past the 0.19.0 guard are closed (see the
security notice):
a secret carried in the URL โ
address = https://h/submit.php?token=SECRETorhttps://user:password@h/โ now counts as the configured credential it is, and a target that supplies a credential but no address no longer adopts the caller’shost=as its own. Nothing to do unless you relied on one of those to point a credentialed target at several hosts; the remedies are unchanged โ pass the credential with the request, configure each destination as its own target and select it withtarget=, or setallow host override = true.
-
๐ Check your setup Undefined-behaviour audit: some inputs that used to misbehave are now rejected. From the security notice; nothing to do unless you relied on one of these:
check_cpu time=0is an error โ the window must be at least one second.- A threshold or unit suffix that overflows 64 bits (
used > 1.0e30T,time=5000000w) is reported as an error instead of silently wrapping. - A module whose load fails is dropped from the plugin list instead of
lingering half-loaded, and no longer answers
exec. NSClientServer(check_nt) andCheckMKServerrestart their listener on a settings reload, so a changed port or password takes effect as it already did forNRPEServer. Connections open at that moment are dropped.- A Python script can no longer unload the
PythonScriptmodule it runs in.
- ๐ โฑ๏ธ Inbound TLS handshakes are now bounded by the listener’s
timeout. The connection deadline was armed only after the handshake had completed, so the handshake phase itself was unbounded: a host permitted byallowed hostscould open sockets, send nothing, and pin one connection object, file descriptor and buffer each, indefinitely. The plain-TCP path was bounded bytimeout(30 s by default) all along; the SSL path โ the NRPE default โ was not. A client on a slow or lossy link that cannot complete a handshake withintimeoutis now dropped instead of lingering; raisetimeouton the listener if that is too tight for your network. See the security notice.
- ๐ Check your setup The NRPE client now says so when it is not authenticating the server, and
generated certificates carry a usable SAN.
verify modedefaults tonone, sossl = truealone gives encryption with no peer authentication and undetectable on-path impersonation. The default is unchanged, but the client now logs one error line per target at its first check; setverify mode = peer-certwithcapointing at the issuer to silence it and actually authenticate the server. Generated certificates now name the machine itself rather than onlylocalhost, so verification is possible at all โ regenerate an existing one to use it. See the security notice.
- ๐ Check your setup The 512-bit Diffie-Hellman parameter file is no longer shipped.
security/nrpe_dh_512.pemwas installed alongsidenrpe_dh_2048.pemand was the default of an unused settings helper. Nothing in the agent used it โ the NRPE server has defaulted tonrpe_dh_2048.pemโ but 512-bit DH is Logjam-broken, and an operator copying the shipped default intodhinherited it. The file and the dead default are gone. If you setdhtonrpe_dh_512.pemexplicitly, change it to${nrpe-dh}/nrpe_dh_2048.pembefore upgrading; otherwise the listener will fail to start with a missing DH file. An existing copy on disk is left where it is by the upgrade. See the security notice.
- ๐ .NET plugins are back, on Windows and Linux. The
DotnetPluginsmodule is built again, now hosting an installed .NET runtime (8.0 or newer) throughhostfxrinstead of the old Windows-only C++/CLI build, so it also works on Linux. Nothing to do on a default install: the module is not loaded unless you addDotnetPlugins = enabledto[/modules], and no runtime is bundled. The Windows installer regained the “.NET plugin support” feature (selected by default, installsmodules/DotnetPlugins.dlland the managed API inmodules/dotnet/); the Linux packages ship the same files when they were built with the dotnet SDK. Plugins are configured under[/settings/dotnet/plugins](<alias> = <assembly>), see the DotnetPlugins reference and Extending with .NET. Plugins written against the pre-0.6 C++/CLI API (NSCP.Core.dllfor the .NET Framework) must be rebuilt against the newnet8.0NSCP.Core.dll; the interfaces are the same.
-
๐ค The WEB server can now cache passive results and serve them over REST. Nothing to do: the feature is off by default and an existing install is unchanged. Set
enabled = trueunder[/settings/WEB/server/results]and restart the service โenabledandchannelare read when the web server starts, because a submission channel cannot be registered by a settings reload โ andWEBServerregisters a submission channel (WEBby default), keeping whatever is submitted to it in memory so a monitoring system that cannot reach the agent can poll the results back out of it. While it is off no channel is registered and the endpoints answer503.Only one result is kept per key (
${host}/${alias-or-command}by default);modepicks which of two results for a key survives โlast(the default, the newest wins) orworst(the most severe wins, so a problem that recovered before the next poll is still reported).GET /api/v2/resultsdrains what it returns unlessclear on poll = false, which is what makesworstmean “worst since the last poll”.Four endpoints expose the cache โ
GET /api/v2/results,GET /api/v2/results/{key},DELETE /api/v2/resultsandDELETE /api/v2/results/{key}โ behind the newresults.list,results.getandresults.deleteprivileges. Those privileges are not part of any bundled role exceptfull, so grant them explicitly to whoever polls:nscp web add-role --role poller --grant results.list,results.get,login.getThe cache is bounded (
max entries, default 1000 keys;max age, default no expiry). See the REST API results page for the full contract.The
check_nsclientbinary bundled with the packages (Windows MSI and the Linux DEB/RPM alike) moves from 1.0.1 to 1.1.0, which is the version that gained theresultscommands โresults list,results show,results delete,results clearandresults feed, the last of which polls an agent’s cache and submits everything in it to Nagios as passive check results. Nothing else about the plugin changes and no existing invocation is affected.
- ๐ง
tls versionnow accepts every spelling it documents.tlsv1.0+,tls1.0+,1.0+,tlsv1.3+,tls1.3+,1.3+,sslv3+,ssl3+andanywere rejected with “Invalid tls version” โ for an NRPE listener that surfaced as “listener failed to start”. Four separate copies of the table of accepted spellings had drifted apart: the maximum-version lookup never stripped the trailing+the way the minimum-version lookup does and listed only some of the+forms literally, and the floor lookup the listener validates through was missingsslv3entirely. They now share one table. Nothing to do; the defaulttlsv1.2+was never affected.
0.19.0¶
- ๐ A
check_servicefilter that matched no service no longer kills the agent. On Windows,check_service "filter=name = 'nosuchservice'"terminated the wholenscpprocess instead of returning a result;check_logfile’scolumn()keyword could be driven into the same fault. Both now return the documented empty-result contract (UNKNOWN: No services found). Nothing to configure โ if you worked around it withservice=<exact name>and nofilter=, service name patterns are usable again. See the security notice.
- ๐ Check your setup A client target no longer lets a request send its configured credentials to
a destination the request names.
host=,port=andaddress=used to move the destination while the target’spasswordortokencame along, so anyone able to run a client module’ssubmit_*/check_*command โ a REST user in the seededmonitoringrole, or an NRPE peer withallow arguments = trueโ could have the agent send that credential to a host of their choosing. That combination is now refused. A target with no credential, a request supplying its ownpassword=/token=, and a request that does not move the destination are unaffected. If you relied on one credentialed target plushost=to reach several servers: pass the credential with the request, configure each server as its own target and select it withtarget=(which now works for queries too), or setallow host override = trueon the target. See the security notice.
- ๐ REST script uploads are staged in a private, randomly named file.
PUT /api/v2/scripts/โฆno longer writes the upload to${temp}/<name>, where a local user could plant a file of the same name and have it imported as a command. No configuration change; a staging failure is now reported as an HTTP 500 instead of importing whatever was on disk. See the security notice.
- ๐ Check your setup Modern layout (opt-in, experimental): the shared folder must be a real
directory. A
%ProgramData%\NSClient++that is a junction or symbolic link is refused by the installer, bynscp settings --migrate-layout modern, and at service start (which fails rather than loading a configuration from behind a link). Relocate the folder with a[paths]override inboot.iniinstead. Legacy (default) installs are unaffected. See the security notice.
- ๐ ๐ฅ Check your setup A multicast collectd target now sends through one interface, not all of
them. A
CollectdClienttarget with no address goes to collectd’s default multicast group (239.192.74.66:25826), and every such target used to send a copy of the metrics through every local interface of the matching address family โ putting them on segments the operator never meant to reach. The new per-targetmulticast interfacesetting decides this:auto(the default) sends one copy through the interface the routing table picks,allrestores the previous fan-out, and a comma-separated list of local IP addresses sends through exactly those. Unicast targets are unaffected. If you depend on a multicast target reaching several segments, setmulticast interface = allon it. See the security notice.
- ๐ Repeated rapid-fire failed WEB logins from one IP are now blocked for longer
each time. The block that follows
auth rate limit max failuresconsecutive failures used to be a fixedauth rate limit block secondswindow (default 60 s) that let an attacker resume guessing at a steady rate forever. A further block now doubles the wait, up to an hour, and resets after a successful authentication or an hour of quiet. Only failures arriving at machine speed escalate: a client retrying a stale password on a schedule keeps the base delay, so it cannot lock out everyone sharing its address behind NAT or a proxy. Nothing to do on a default install.auth rate limit max failures = 0still disables the limiter outright. See the security notice.
- ๐ An NRDP submission over an unverified
httpslink now says so in the log. When a target’sverify moderesolves to no peer verification, the module logs a message naming the endpoint โ the token then goes to whichever server answers โ matching what the Icinga client already does. The connection itself is unchanged. Theverify modehelp text was corrected at the same time: it listed three values the client-side parser rejects (client-once,workarounds,single) and recommendednonefor self-signed certificates; usepeer-certwithcapointing at the certificate instead. See the security notice.
-
๐ง Check your setup
nscp testnow has a real prompt, and writes a command history file. On a terminal the prompt gained line editing, persistent history, tab completion against the command registry, syntax highlighting that shows an unknown query or module name in red before you press enter, and log messages that redraw around what you are typing instead of landing in the middle of it. See Test mode. Two things to know:- History is written to a per-user file โ
%APPDATA%\NSClient++\console-history.txton Windows,$XDG_STATE_HOME/nscp/console-historyor~/.nscp_historyelsewhere (created mode0600on POSIX). Commands typed at the prompt can carry credentials, so if you would rather keep nothing on disk sethistory size = 0under[/settings/cli]. That section also holdshistory fileandcolor. - With stdin not a terminal nothing changes โ no prompt, no history, no colour, and the core keeps writing the log itself. Piping commands in does now work on Windows, where the readiness check used a console-only API and silently ignored a file or pipe; an exhausted stdin no longer spins at 100% CPU on POSIX; and end of input is no longer treated as a reason to exit, which is how the agent is normally started under a supervisor.
Completion after
load/enableoffers the modules that are not already loaded or enabled, andunload/disablethe ones that are. The first such completion in a session pauses while the agent reads the module directory, once per process. - History is written to a per-user file โ
-
๐ง The console log is no longer held back in a buffer. Nothing to do. The console log backend installs a 64 KB buffer on standard output and nothing emptied it, so on Windows log output sat there until something else happened to flush the stream โ in
nscp testthe only thing that did was reading the next line of input, which is why the log appeared to catch up only when you pressed a key. Every message is flushed as it is written now.The same fix means a redirected console log streams instead of accumulating:
nscp test > log.txt, a container, or a supervisor capturing standard output now see each line as it is logged rather than nothing until 64 KB has built up or the process exits.
- ๐ง
--no-stderrand the oneline log format now take effect. Nothing to do unless you were working around the old behaviour. Both were passed to the log level parser, which does not know them, so they were rejected withInvalid log level: no-std-errin the log and never applied. They reach the log driver now.
- ๐ง
target=now selects a configured target on the query path as well. The shared client machinery only ever appliedtarget=/-twhen a command was run as an exec; as a query โ which is what a REST or NRPE caller gets forcheck_*andsubmit_*โ the argument was accepted and then ignored, so the call silently went to thedefaulttarget. It is applied on both paths now, with any explicit option (host=,timeout=, โฆ) still winning over what the selected target says. A query that passedtarget=and relied on reachingdefaultanyway will now reach the target it named.
- โฑ๏ธ Check your setup collectd submissions now honour
timeoutandretries, and report failed sends. Both target settings were read into the connection but never used on the UDP path, and the send discarded its result entirely โ an unreachable target, a full socket buffer or an oversized datagram looked exactly like a successful send, so metrics that never arrived left nothing in the log. A failed send is now retried up toretriestimes (default 3, 20 ms apart) and logged once per distinct failure; the whole send โ name resolution included โ is bounded bytimeout(default 30 seconds), after which the remaining datagrams are abandoned with a message. Retries apply to sends that failed locally, so no datagram the receiver already has is ever sent twice. Nothing to do unless you set a largeretrieson a collectd target โ it now costs real time, bounded bytimeout.
- ๐ง Check your setup An Icinga target address with a path prefix is now honoured. The path of a
target
address(https://proxy.example.com/icinga/) was parsed and then dropped: every call went to/v1/...on the host, so an Icinga 2 master published under a reverse-proxy subpath could not be reached and requests landed on a path the operator did not intend. The prefix is now prepended to every API path (/icinga/v1/actions/process-check-result). Addresses without a path โ the normalhttps://icinga.example.com:5665form, with or without a trailing/โ are unaffected. If a target address carries a path that is not a subpath of the API (a leftover from another client’s configuration, say), remove it: it is now sent.
0.18.1¶
- ๐ Check your setup Icinga API submissions now honour the configured
timeout, and credentials no longer reach the trace log. TheIcingaClientmodule’s HTTP calls previously waited forever โ the target’stimeoutsetting (default 30 s) was read but never applied โ so a stalled Icinga endpoint could silently wedge passive-result submission; settimeout = 0on the target if you depend on the old unbounded wait. The same pass maskedpassword/tokenvalues in the trace-level target dump (this also covers the other client modules sharing that machinery) and added a log message when anhttpssubmission runs with certificate verification disabled (verify modeempty ornone). See Security notices.
- ๐ Filter expressions are now bounded in length and nesting depth. A
filter/warning/criticalexpression โ and a%(...)expression placeholder inside a syntax template โ longer than 1024 characters or nested more than 64 parentheses deep is now rejected at parse time instead of being evaluated, failing with a clear “exceeds the maximum length/depth” error. The where-parser and the expression evaluator both recurse with the shape of the input, so an unbounded or deeply nested expression could exhaust the stack and crash the agent. Real filters are a small fraction of these limits, so the default install and every normal configuration are unaffected โ only a pathologically large or deeply nested expression is refused. See the security notice.
- ๐ Check your setup CheckExternalScripts hardening. A hardening pass over the external
scripts module tightened several rough edges: the
ext-scr installargument lockdown now writes to the setting the module actually reads (previously it was a no-op, so a lockdown could silently not apply), the command timeout is enforced on every execution path with output capped, theshow/deletesandbox resolves symlinks, and%/^are blocked on the shell-fallback path. The default install is unaffected (arguments are off by default). If you rely onext-scr installto disable arguments, re-run it after upgrading so the effective setting is written. See the security notice.
- ๐ Check your setup NSCA-NG cert mode now actually verifies the server (and presents the
client certificate). In 0.18.0 the
NSCANgClientcert mode (use psk = false) applied its TLS configuration to the OpenSSL context after the connection object had been created from it, and OpenSSL copies the verify mode, client certificate, cipher list and TLS-version bounds out of the context at creation time โ soverify mode = peer-certwas silently ignored, any certificate the server presented was accepted, and the configured client certificate was never sent. The configuration is applied before the connection is created now. Two operator-visible consequences: a cert-mode target that “worked” against a server whose certificate does not chain to the configuredca(or does not match the host name) will now fail to connect โ that is the verification working; fix the server certificate, or opt out explicitly withinsecure = trueif you accept the MITM risk. And servers that require a client certificate will start seeing it. The default PSK mode (use psk = true) is unaffected. See Security notices.
- ๐ Check your setup NSCA: an unrecognized
encryptionvalue is now a hard error instead of silently running without encryption. A typo’d algorithm name (aes-256), or one not compiled into the build, used to fall back to no encryption on the end carrying it. Since the ciphers must match, a one-sided typo showed up as the peer rejecting every submission with a CRC error rather than as accepted plaintext โ but that failure gave no hint of its cause, and a value broken the same way on both ends did run plaintext while looking encrypted. Now theNSCAServermodule refuses to load and anNSCAClientsubmission fails, each naming the problem and listing the available algorithms. Default installs (aes256) are unaffected. Breaking only for setups relying on the fallback: fix the algorithm name, or setencryption = noneexplicitly if plaintext was intended โ this includes builds compiled without crypto++, where any cipher name previously degraded to plaintext and the server now refuses to start. See the security notice.
- ๐ Check your setup NSCA hardening: empty-password warning,
performance data = falsehonoured, wire-field validation. Enabling NSCA encryption with an emptypasswordnow logs an error on both ends (the password is the key, so an empty one is a well-known key) โ set the same password on both ends to clear it.NSCAServer’sperformance data = falsenow actually strips perfdata from forwarded submissions (it was silently ignored). Inbound host/service names are stripped of control characters and out-of-range status codes are clamped to UNKNOWN. No action needed on a default install. See the security notice.
- ๐ Check your setup NRDP submissions now honour
timeoutandretry. Both settings were parsed but never applied: a submission to a server that accepted the connection and then stalled hung the submission thread forever, and failed submissions were never retried. Every step of the exchange (connect, TLS handshake, proxy tunnel, request, response) now runs under the configuredtimeout(default 30 seconds for configured targets, 10 for one-shotnscp clientsubmissions), and transport failures are retried up toretrytimes. Nothing to do unless your NRDP endpoint legitimately takes longer than the timeout to answer โ raisetimeouton that target. The response body is also capped at 5 MB, far above any real NRDP reply. See the security notice.
- ๐ A
tls versionwith a trailing+now means “that version or later”.1.2+(the common default) previously negotiated TLS 1.2 only; it now also permits TLS 1.3, andanyis accepted as the documentation always claimed. This applies everywhere the setting exists: NRDP and the other HTTP-based clients, the NRPE/NSCA clients and servers, andcheck_tcp. No action needed; pin an exact version (tls version = 1.2) if a peer misbehaves when TLS 1.3 is offered. See the security notice.
- ๐ Check your setup The Elastic module now verifies HTTPS server certificates and can
authenticate.
ElasticClientpreviously hardcoded TLS verification off; anhttps://address now defaults toverify mode = peeragainst the platform CA bundle, with newtls version,verify modeandcasettings to tune it. Newuser/passwordandapi keysettings authenticate against secured clusters (Elasticsearch 8+ defaults), and a newtimeout(default 30s) bounds each submission. If you rely on a self-signed certificate, pointcaat it or setverify mode = noneexplicitly. See Security notices.
- ๐ค Check your setup The Elastic module no longer sends the legacy
_typeparameter by default. Mapping types were removed in Elasticsearch 8, which rejects bulk requests carrying them, so theevent type,metrics typeandnsclient log typedefaults are now empty. Only Elasticsearch 6.x or older needs them: set the old values (eventlog,metrics,nsclient log) explicitly to keep the previous behaviour. Batched documents also now get distinct ids โ previously all documents in one bulk request shared an id and overwrote each other, so multi-entry events show up completely now.
- ๐ Check your setup check_mk client targets: configured TLS settings are honoured again.
check_mk_target_object::read()added the SSL keys (use ssl,certificate,verify mode,ca, โฆ) to its settings registry but never calledregister_all()/notify(), so values set on a[/settings/check_mk/client/targets/โฆ]section were silently ignored and the client connected in plaintext regardless of configuration. The keys are read (and documented) again. If you configureduse ssl = trueon a check_mk target, the connection becomes TLS on upgrade โ make sure the server side actually speaks TLS, or the check starts failing. Details in the security notice.
- ๐จ Check your setup Syslog client targets: configured severities and templates are honoured
again. The same defect existed in the syslog client’s target object: the
severity,facility,tag_syntax,message_syntaxand per-status severity keys on a[/settings/syslog/client/targets/โฆ]section were never read, so the built-in defaults (error/kernel/โฆ) always won. Values you configured โ perhaps years ago, without effect โ now apply; if your syslog routing depends on the previously effective defaults, review the target sections for stale keys.
-
๐จ ๐ Check your setup Syslog messages now carry the RFC 3164 HOSTNAME field, and several syslog options work for the first time (see the security notice). What changes on the wire and in behaviour:
- Datagrams now read
<PRI>TIMESTAMP HOSTNAME TAG MESSAGE. Thehostnamesetting under[/settings/syslog/client]โ until now read but never used โ fills the HOSTNAME field (defaultauto, the machine name). Receivers that promoted the tag (defaultNSCA) to origin host will now file records under the real host name; adjust any log-parsing rule that keyed on the old, hostname-less format. - The
tag_syntaxandmessage_syntaxtarget settings now reach the wire. They were stored under keys the sender never read, so a settings-defined target sent an empty tag and dropped the message text. - The per-state severity options (
ok-severity,warning-severity,critical-severity,unknown-severity) passed as command arguments now take effect; they too were stored under keys that were never read. - An unknown
severityorfacilityname now degrades to priority<13>(user.notice) instead of<0>โ which is kernel.emergency, a priority many receivers page or broadcast on. A missing per-state severity now falls back to the baseseverityinstead of tripping that fallback. - All C0 control bytes and DEL in the outgoing line are replaced with spaces (previously only CR, LF and NUL), so check output cannot smuggle ANSI escape sequences into the receiver’s log.
- Datagrams now read
- โฑ๏ธ Check your setup The Graphite
timeoutis now enforced โ as a budget for the whole submission. It was doubly dead before: the value the operator configured never reached the connection (the lookup read a map the well-knowntimeoutkey is not stored in, so the default 30 always won), and the connection did not use even that โ resolution, connect, the TLS handshake and every write ran with no deadline, so a stalled carbon endpoint could hold the submitting thread for the OS-level TCP timeout, or indefinitely on a stuck write. The configuredtimeout(default 30s) is read now and bounds the whole submission as one budget, like the SMTP client’s. A target whose submissions previously completed by quietly taking longer will now fail at the configured value; raisetimeouton targets talking to a slow relay. See Security notices.
- ๐งน The Graphite
retrysetting is not honoured and is no longer read. The module always made exactly one attempt per submission; reading the value made it look otherwise, and a retry loop would multiply the worst-case time a stalled endpoint can hold the submitting thread by the retry count.retry/retriesare registered for every client module centrally, so they still appear in the GraphiteClient reference, butGraphiteClientdoes not act on them. Mirrors the SMTPretrychange in 0.18.0.
0.18.0¶
-
๐ฅ Check your setup
check_nscpis now a filter check, and it can see crash reports again. Crash reporting itself has always worked โ the agent has archived crash dumps since 0.4.x โ butcheck_nscp’s count of them has not, for two independent reasons that accumulated over the years:- The count matched files whose extension equalled
txt, while the helper it used returns the extension with its leading dot (.txt). That comparison has been false since the check was written in 0.4.2, so the crash count read 0 whatever was in the folder. - 0.6.10 dropped Google Breakpad, whose vendored submodule and build
machinery had become a dependency burden, along with the separate
crash-report sender tool that shipped with it. The handler that replaced
it writes one plain-text
<timestamp>.crashfile per crash โ the exception, the faulting address and the module it landed in โ where breakpad left a<guid>.dmpminidump plus a<guid>.dmp.txtdescription. From 0.6.10 on, even a corrected.txtmatch would have found nothing.
Separately, 0.4.3 stopped the module reading the archive folder from
[/settings/crash]archive folderand hardcoded the compile-time default instead, so on any installation that had moved the archive folder the check was looking in the wrong place as well.All of that is fixed. Crash reports are recognised by
.crash, and still by.dmpand.txtso pre-0.6.10 archives keep counting; the configuredarchive folderis read again; andcheck_nscpnow exposescrashes,last_crash,crash_age,errors,last_error,uptime,versionanddateas filter keywords, accepts the usualfilter/warning/criticalandtop-syntax/detail-syntaxoptions, and emits perfdata for whichever keywords the thresholds name.The default verdict is unchanged โ any crash report or any logged error is CRITICAL โ but two things change for existing users. The message is now
N crash(es), M error(s), uptime <duration>without the appendedlast crash:/last error:fragments. To put them back, pass adetail-syntax:check_nscp "detail-syntax=${crashes} crash(es) (${last_crash}), ${errors} error(s) (${last_error}), uptime ${uptime}"And because the crash count now works, an agent with an old report still sitting in the crash archive folder will start reporting CRITICAL where it previously read 0 โ threshold on
crash_age(for example"crit=crash_age < 7d") if you only care about recent crashes, or clean the folder out. Crash reports remain a Windows-only concept; on Linux there is no crash handler, socrashesis always 0. - The count matched files whose extension equalled
-
๐ค Check your setup
check_and_forwardnow actually submits the result. The command ran the wrapped check, answeredMessage submittedand delivered nothing: the submission was built in a form the channels could not read, so NSCA, NRDP, Graphite and every other client module received a message with no results in it. It now submits like a scheduled check does. If you had given up on the command, it works โ and if you built a workaround around its silence (for instance a schedule that exists only to be triggered), that workaround is no longer needed. The command also gainedchannel(targetstill works as a synonym),alias,destinationandsourceoptions, and names the result after the command when no alias is given.Breaking: the command no longer accepts positional arguments for the wrapped command.
check_and_forward command=check_cpu warn=load>80used to (try to) hand the bare trailing tokens to the wrapped command; it now fails to parse. Pass each wrapped-command argument througharguments=instead, one per argument:check_and_forward command=check_cpu "arguments=warn=load>80" "arguments=crit=load>90"The positional form had to go because its parser also swallowed the CLI’s own
--argument key=valuetokens, which is what fed the wrapped command garbage and made it fail. If a submission is rejected by the channel the command now reports that failure instead ofMessage submittedโ including when only one channel of a comma list (channel=NSCA,GRAPHITE) fails, which previously was silently reported as success.
- โ๏ธ A reload now loads modules enabled in an included file since the last one.
An
[/includes]file is read once when the configuration is loaded and served from memory after that, so a module switched on in one โ most importantlyfleet.ini, which the fleet sync rewrites whenever a bundle changes โ was not actually loaded until the service next restarted. Nothing said so: the file on disk was right and a fleet host reported itself in sync, while the module quietly did nothing. The visible case was a fleet bundle turning onNRDPClientandSchedulerto start passive submissions, which then never arrived. A reload now re-reads the included files first, so settings changed in them take effect and newly enabled modules are loaded; modules already running are left alone. Two things are unchanged: a module disabled since the last load keeps running until the service restarts, and[/modules]in the mainnsclient.iniis still only read at startup.
- ๐
New: run scheduled checks on demand with
run_schedules. After editingnsclient.iniyou no longer have to wait out the interval to see the new result on the monitoring server โnscp client --boot --query run_schedules(optionally--argument schedule=<alias>) runs the configured schedules now and submits their results on their normal channel. It is a regular check command, so it also works over NRPE, REST and innscp test. Nothing changes for existing configurations; the timers are untouched. See Passive monitoring โ Step 6.
- ๐ซ A denied check is no longer submitted as a passive result. The permission
layer answers a denied query as a successful query carrying an UNKNOWN
“Permission denied” payload, and both
run_schedulesandcheck_and_forwardforwarded that to the monitoring server โ overwriting the last real result for that service while telling the caller it had succeeded. Both now refuse withPermission denied: not allowed to run <command>, nothing was submittedand touch no channel. If you restrict what a REST or NRPE identity may run, expect the denial as an error where you previously saw a stale UNKNOWN appear on the server. See Permissions.
- ๐ง Check your setup
settings --update --add-defaults --use-samplesnow writes the sample objects. The flag was parsed and never read, so it behaved exactly like the plain invocation. It now writes the registered/samplesections, and--remove-defaultsstrips them again โ the two are now exact inverses. An edited sample is still never overwritten or removed, and without the flag the output is byte-for-byte what it was. If you have scripted--add-defaults --use-samplesexpecting today’s (sample-free) output, drop the flag.
- ๐ฌ Check your setup
nscp client --query <cmd>no longer appends “No module was specifiedโฆ”. The line was appended to every result when no--modulewas named. Scripts that stripped or matched it can stop; naming a module is unchanged.
- ๐ง Settings writes work again when
[/includes]names a directory. Saving handed the directory path to the INI writer, and the resulting “Is a directory” error aborted the whole save โ sonscp settings --setand the web UI failed and the main file was never written. Directory includes are read-only by nature and are now skipped when saving. If you worked around this by removing a directory include, you can put it back.
- ๐ Check your setup NRDP HTTPS submissions now verify the server certificate by default on
the
nscp client/REST path. Previously anhttps://submission made that way with noverify modeset trusted any certificate silently (configured targets already defaulted topeer). If you submit to a self-signed NRDP endpoint that way and want to keep skipping verification, pass--verify none(or point--caat the certificate) explicitly. Plainhttp://is unaffected. Two further NRDP hardening fixes ship in the same release โ a malformed server response can no longer crash the agent, and the NRDP token and any proxy-URL credentials are redacted from the trace log. See Security notices.
- ๐ก Check your setup
--source-host/--sender-hostnow name the sending host, on every client module. They were registered against the destination container, where the well-knownhostkey is routed into the typed address field โ so naming a source host silently redirected the connection to it, and the sender the handler reads was never set.SMTPClientandNRDPClienthad each worked around this by registering their own copies, which made the option name ambiguous and so unusable on those two modules (option '--source-host' is ambiguous). The options are registered once now, against the sender. If you had scripted around the old behaviour by passing--source-hostto redirect a connection, use--hostor--addressfor that instead.
- ๐ Check your setup SMTP submissions now verify the server certificate against the agent’s
CA bundle.
SMTPClientrelied on OpenSSL’s built-in default verify paths, which on Windows do not include the Windows certificate store โ so the defaultsecurity=starttlsfailed verification against Gmail, Microsoft 365 and every other public provider there, andinsecure-skip-verifywas the only way through. A newcatarget setting (and--caargument) defaults to${ca-path}, the same trusted bundle the other TLS clients use: the distribution’s CA store on unix, the exported Windows ROOT store on Windows. If a Windows SMTP target was only working because you setinsecure-skip-verify = true, remove it and retry โ verification should now succeed. For an internal relay with a private CA, pointcaat that bundle instead of waiving verification. Setca = noneto restore the old behaviour. A bundle that cannot be loaded now fails the submission with a message naming the file, rather than failing the handshake later with an unrelated-looking issuer error. A target that names nocaat all โ a one-shot command line, or a default target โ falls back to the same bundle, resolved once at module load, so no submission path is left on OpenSSL’s built-in verify paths by accident.
- ๐ SMTP client security hardening. The same pass closed a set of
trust gaps in
SMTPClient: data pipelined into the STARTTLS greeting is refused rather than trusted as post-handshake input, the EHLO name is validated for command injection before it reaches the wire, and ESMTP capabilities are matched per reply line instead of by substring search. No configuration change is needed. See Security notices.
- โฑ๏ธ Check your setup The SMTP
timeoutis now a budget for the whole submission rather than a fresh deadline per operation (and per resolved address). A target that previously completed by using several times its configuredtimeoutacross a slow session will now give up at the configured value; raisetimeouton targets talking to a slow relay.
- โ๏ธ SMTP messages now carry a
Message-IDheader. Nothing to do โ it improves deliverability and gives mail administrators a handle to trace a notification by.
- ๐งน The SMTP
retrysetting is not honoured and is no longer read. The module always made exactly one attempt per submission; reading the value made it look otherwise.retry/retriesare registered for every client module centrally, so they still appear in the SMTPClient reference, butSMTPClientdoes not act on them.
- ๐ NRPE hardening: a new
expose versionsetting, and the metachar guard now also checks decoded input. Nothing to do on a default install. Two things are worth knowing.NRPEServergainedexpose version(defaulttrue, which keeps the legacy bannercheck_nrpeexpects); set it tofalseto answer the unauthenticated_NRPE_CHECKping with a generic message instead of the exact build. Andallow nasty characters = falsenow re-checks the decoded command and arguments, not just the raw wire bytes โ with a non-UTF-8encodingset, a multi-byte sequence could previously decode into a metacharacter that was never literally on the wire, so a request the guard was always meant to block may now be rejected. Separately,nscp nrpe installreads the storedverify modeagain instead of silently resetting it on a re-run. See the security notice.
- ๐ Check your setup WEB server security hardening. A review of the
WEBServermodule produced several defense-in-depth fixes (session tokens now come from the OpenSSL CSPRNG, cookie-name matching requires a name boundary, the installer refuses an HTTPSโHTTP redirect, and thelegacygrant’s startup warning now names/settings/query.pb). The default install needs no action. Two changes touch observable behaviour: a script or module name that begins with-is now rejected (rename it; interior dashes are fine), and the legacyPOST /auth/logoutroute now enforcesallowed hostslike the rest of the API (a caller outside the perimeter gets 403). A third only bites on a broken system: if the OpenSSL CSPRNG fails, the server now refuses to issue a session token (HTTP 500, logged asSECURITY:) rather than falling back to a weaker generator. See the security notice.
- ๐ The bundled OpenSSL is updated from 3.5.4 to 3.5.8 in the Windows
builds, picking up the fixes from four upstream security releases on the
3.5 LTS line. The most relevant fix for NSClient++ is
CVE-2025-11187, a stack
buffer overflow parsing a crafted PKCS#12 file โ reachable through
check_certificatewhen it scans certificate files that less-trusted principals can write to โ alongside further PKCS#12/ASN.1 and TLS-stack fixes. No configuration change is needed. The Linux packages link the distribution’s OpenSSL and are unaffected. See Security notices.
0.17.0¶
-
๐ท๏ธ Check your setup Check-specific filter keywords that clashed with the generic summary keywords are renamed. A handful of checks registered their own keyword named
status,countortotalโ the same names as the built-in summary keywords (%(status),%(count), โฆ). The check-specific value won infilter/warning/criticalanddetail-syntax, whiletop-syntaxand the reference documentation showed the generic one, which was confusing. Each such keyword now has a distinct, documented name:Check Old New check_cpu,check_cpu_utilizationtotalusagecheck_batterystatusbattery_statuscheck_networkstatus,totallink_status,throughputcheck_os_updatescountupdatescheck_patch_agecountpatchescheck_pending_rebootcountsignalscheck_printjobsstatusjob_statuscheck_printqueuestatusprinter_statuscheck_installed_software(Linux)statuspackage_statuscheck_activationstatusactivation_statuscheck_dockerstatuscontainer_statuscheck_connectionscount,totalconnections,total_connectionscheck_dnscountrecordscheck_httpstatusstatus_messagecheck_shadowcopycountcopiescheck_disk_healthtotalsizeExisting configurations keep working: the old names remain as undocumented deprecated aliases with unchanged behaviour, so filters like
check_cpu "warn=total > 80"still work. Migrate to the new names at your convenience; only they appear in the reference documentation. A few behaviour changes ride along:check_os_updates’ default output now reports the actual number of updates (rendered from the detail line via${list}) instead of the matched-row count the old default showed.- Three default perfdata keys change because the default
perf-configlists the renamed keyword:check_cpu_utilization(Linux)cpu_totalโcpu_usage,check_patch_agepatch_countโpatch_patches,check_pending_rebootreboot_countโreboot_signals. Pass your ownperf-config=extra(...)with the old (alias) keyword to keep the old key. All other perfdata keys are unchanged (the renames keep their original perf suffixes, andcheck_connections’ default perf-config intentionally still uses the alias for series continuity).
- ๐ Check your setup
${host}and friends now resolve in attachment paths and in[/includes]. Host name placeholders were only expanded in settings urls and in the url an attachment is fetched from, not in the path it is written to nor in an included file name. An unknown${...}token in a path is not an error - it resolves to the installation directory - so a configuration such as[/attachments] ${shared-path}/${host}.ini = ...did not fail, it quietly wrote one file with the installation directory in its name. Those paths now name the host, which changes where such a file lands: check any${host},${hostname}or${domain}you already have under[/attachments]or[/includes], and remove the workaround if you scripted around this. Configurations without a host name placeholder are unaffected. When the substitution lands in a local path, the value is sanitized to the characters a legal host name can contain (see the security notice). Likenscp settings --switch,nscp settings --migrate-to(and the REST migrate) now keeps a placeholder you pass it as-is inboot.iniwhile migrating into the expanded per-host file, so the template survives on a fleet-managed machine.
- ๐ข Check messages can now be told how to render their numbers. Every filter
check gained four options -
decimals,byte-unit,decimal-separatorandthousands-separator- socheck_drivesizecan report141.06GB/1006.85GB(or141,06GB/1.006,85GB) instead of140.293GB/0.983TB. The defaults are unchanged, so an installation that does not set them renders exactly what it rendered before. The options only touch the message: performance data keeps its full precision and its.radix, and so do the numbers you write in a filter or a threshold. Real-time filters take the same settings asdecimals,byte unit,decimal separatorandthousands separatorkeys, inheritable from the default template.decimalsis capped at 15 (adoublecarries no more than that): the query option and theformat_bytes()/format_number()argument reject a larger value, and the settings key clamps it, so a typo likedecimals=1000000can no longer make a check try to render a multi-megabyte number. Note that setting any of the four options also moves plain float keywords in the message onto the number format: withdecimalsunset they then render with up to three decimals (trailing zeros stripped) instead of the legacy 6-significant-digit form โ2.71094becomes2.711, and large values stop rendering scientific (1.23457e+07). A pipeline that matches float text in the message may need its pattern relaxed when you first set one of these options; leave all four unset and the message is byte-for-byte unchanged.
- ๐ข Check your setup An unknown unit in
format_bytes()is now reported instead of rendering nonsense.format_bytes(used, 'gb')used to render1.27055e-10because the unit comparison was case sensitive, and any misspelled unit renderedvalue/1024^7. Lowercase units now work, and a unit that names nothing (say'ZB') makes the check reportFilter processing failed: format_bytes failed: Unknown byte unit: ZB. A syntax string with such a typo returns UNKNOWN rather than a quietly wrong number - fix the unit, or the check will stay UNKNOWN.
- ๐ Check your setup A
unit:inperf-configthat names no unit no longer divides the metric by 1024โท. An unrecognised unit now leaves the value alone. If a graph of yours has been flat at a near-zero value, check theunit:spelling in itsperf-config: the metric will jump to its real magnitude on upgrade.
- ๐ Check your setup
perf-config’sunit:now converts plain byte series instead of relabelling them. On series that are byte counts but do not auto-scale (most byte keywords outsidecheck_drivesize),unit:KBused to change the label only, shipping=1536KBfor a value of 1536 bytes - a metric off by the unit ratio to any consumer that trusts the label. The value and the warn/crit bounds now convert into the requested unit, matching what the auto-scaling series always did. A dashboard that compensated for the mislabelling will see the metric drop by that ratio on upgrade. Series not measured in bytes (ms,%,s, …) and explicitminimum:/maximum:overrides are unaffected, and aunit:that names no byte unit still only changes the label.
- ๐งฉ Check your setup Errors raised while a template renders are now reported. A function that
failed inside
detail-syntaxortop-syntaxused to leave the placeholder empty and say nothing; the check now returns UNKNOWN withFilter processing failed: โฆ. This surfaces template mistakes that have been silently producing incomplete messages.
- ๐ช Check your setup
check_pending_reboot’s default message now names the pending-since time. When the reboot was queued by Component Based Servicing or Windows Update, the message gains a suffix:Reboot required: Windows UpdatebecameReboot required: Windows Update (pending since 2026-08-16 09:41:12). Notification pipelines that match the exact message text (an anchored regex, a string equality) need their pattern relaxed; thresholds, states and existing keywords are unchanged.
- ๐ข Check your setup Filter comparisons between a text keyword and a bare number are now
numeric. A string-typed keyword compared against an unquoted number used
to order lexically โ
filter=value > 90onfilter_perfmatchedvalue=100as false (“100” sorts before “90”) โ or, with the operands reversed (90 > value), failed to evaluate at all. Both now compare as numbers, whichever side the keyword is on: the row’s text is parsed per record, and a value that is not a number simply never matches (the check logs one warning naming the value; the result stays a certain non-match, not UNKNOWN). This applies to keywords such asvalue/warn/crit/min/max(filter_perf,render_perf),speed(check_network),string_value(check_registry_value) and thecolumn()function (check_logfile). Quoted literals keep the lexical comparison โversion < '8'still orders as text โ as dolike,regexpandin, keyword-specific converters (state = 'running',age > 30m), and the= 'unknown'/= 'never'sentinels for optional values. Review any filter that deliberately relied on text ordering against a bare number: quote the number to keep the old behaviour.
- ๐ข Check your setup Fractional numbers in thresholds are no longer truncated or rounded.
count > 2.5used to evaluate ascount > 3(the literal was rounded into the counter’s integer domain); unit literals lost their fraction entirely, soworking_set > 1.5gmeant 1g anduptime < 2.5hmeant 2h. Fractions now mean what they say. Whole-number thresholds are unchanged; only expressions that already used a decimal point can behave differently.
- ๐ Check your setup
filter_perf/render_perf/xform_perf: themaxandminfilter keywords were swapped.maxread the perf-data minimum bound andminthe maximum. They now read the bounds they name โ a filter that compensated for the swap needs the two names exchanged back.
- ๐จ Check your setup Syslog submission works again, so a configured syslog server will start
receiving traffic.
SyslogClientread its connection settings from the wrong place, so the target’s address, port, facility, severity and templates were all ignored: the agent loggedUndefined facility:and sent nothing. Broken since 0.4.3 (2015). If you have a syslog target configured, check it still points where you want before upgrading - it has not been delivering, and it will now.CheckMKClienthad the same defect on its query path.
- โ๏ธ Check your setup SMTP notifications now announce this host in EHLO instead of
localhost. The sender’s host name was read from the wrong place, so it was always empty and the EHLO fell back tolocalhost. If your mail server applies HELO/EHLO policy (SPF checks, or a rule that rejectslocalhost), the agent will now identify itself properly - setehlo-hostnameon the target if you need a specific name.
- ๐ง Check your setup
nscp settings --shownow says so when--keyis missing.--show --path /some/pathwithout a--keyused to print nothing and exit 0; it now reportsInvalid command line please use --path and --key with showand exits non-zero. A bare--showstill describes the active settings store, and--show --path โฆ --key โฆis unchanged. Scripts that relied on the silent success need the missing--keyadded.
- ๐ฌ Client commands shorter than eight characters work again. A command
such as
cpuorrunansweredException processing command line: basic_string::substr โฆinstead of running, in every module built on the shared client machinery (NRPE, NSCA, NRDP, Graphite, โฆ). Remove any workaround that renamed such commands to a longer alias; no configuration change is needed.
- ๐ง The settings diff no longer reports changes that were already saved.
get_changes()โ behind the REST settingsdiffendpoint and any operator-facing “what am I about to save?” view โ kept listing an edit for the lifetime of the process after it had been written, reporting it as amodifiedentry whose old value equalled its new one. Tooling that treated a non-empty diff as “unsaved work pending” no longer needs to special-case that; no configuration change is needed.
0.16.4¶
- ๐ The bundled Mongoose web server is upgraded to 7.23, fixing two
critical (CVSS 9.1) HTTP request-smuggling vulnerabilities in its HTTP
parser (CVE-2026-73256,
CVE-2026-73257). This
affects the Windows builds of the
WEBServermodule (REST API / web UI) and is exploitable when NSClient++ sits behind a reverse proxy or WAF โ upgrade promptly in that topology. The Linux packages use the Boost.Beast backend and are unaffected. No configuration change is needed. See Security notices.
0.16.3¶
- ๐
check_nt(NSClientServer) answers the real nagios-plugins client again. Requests without a trailing newline used to hang until the client timed out (No data was received from host!) โ broken since 0.12.2. Remove any client-side timeout/retry workarounds; no configuration change is needed. If you expose this legacy endpoint, see the new guidance on securing it (password,allowed hostsand theallowcommand list).
0.16.2¶
- ๐ Check your setup Sensitive settings values are redacted on read. The REST settings
read endpoints (
GET /api/v2/settings/...,/descriptions) andnscp settings --list/--shownow return***for keys registered sensitive, matching thediffendpoint. No action required; tooling that read such a value back now receives***. See Security notices.
- ๐ The built-in
legacyWEB role is no longer seeded on fresh installs, and any role granting thelegacypermission now triggers aSECURITYwarning at startup (and fromnscp web add-role/add-user). Existing installs are unaffected โ the role stays in their config. Thelegacygrant unlocks the deprecated/query.pband/query/{name}query-dispatch endpoints, so a token with it can run any registered check/command; only grant it to trusted legacy systems. See Security notices.
0.16.1¶
- ๐ง RHEL/SUSE: workaround
ca=arguments can be dropped โ${ca-path}now resolves on its own (the explicit form still works). Packagers cross-building for another distribution should set-DCONFIG_CA_PATH=.
- ๐
check_logfileis unchanged unless you opt in tobookmark/max-lines. Adoptingbookmarkis a trade-off: a line is consumed when the check runs (not when its result is submitted) and positions are saved on clean shutdown, so a crash re-reports the backlog. Prefer an explicit bookmark name for a check whose filter changes often.
- ๐ง Check your setup Settings URLs with a query string now send it. A server that relied on receiving the bare path will now see the parameters. The offline-boot cache file is migrated to the query-aware name once on first start.
- ๐ท๏ธ Check your setup
${hostname}in an existing config changes meaning โ it is now expanded everywhereexpand_hostnameis used (including submit clients’hostname), where it used to be left as literal text.
- โฑ๏ธ
run on startupis off by default so the default install is unaffected; if you enable it for thedefaultschedule usestartup windowto avoid a thundering herd.
- ๐ Check your setup Building the HTML docs on non-Windows now needs
-DNSCP_BUILD_DOCS_HTML=ON.
0.14.1¶
- โ๏ธ Licence change: now distributed as Apache-2.0 OR GPL-2.0-only โ a clarification/relicensing with no code or runtime behaviour change. Review it if your organisation tracks bundled-software licences.
- ๐ Check your setup CheckNet perfdata is on by default. If you added perfdata manually, make sure you are not now emitting it twice.
- โ๏ธ Boolean check arguments (
option=true/option=false) now work from the CLI as well as REST; bare-flag usage is unchanged.
- ๐ Action required
CheckSecurityis not loaded by default. Enable it before using its checks (nscp settings --active-module CheckSecurity). Windows-only checks return UNKNOWN elsewhere.
0.12.6¶
- ๐ New permission policy layer, disabled by default. Existing installs
behave exactly as before until an operator sets
/settings/permissions/enabled = true. If you opt in: per-command rules apply to queries only (exec is gated by the separateallow execboolean, which defaultstrue); roll out withlog allows = truefirst to inventory real traffic. See Permissions.
- ๐ NRPEServer
client identity sourcedefaults tonone(previous behaviour). Set tocnonly after configuringverify_mode = peer-certand aca pathpinned to your private monitoring CA โ the system trust store would accept any public cert’s CN.
- ๐ Check your setup
[/paths]overrides from an older install moved to[paths]inboot.ini(same section name, different file). No automatic migration โ copy each entry across and delete the old section.
- ๐ WEB
disable admin user = trueis a new opt-in for status-only WEB exposure; existing installs keep their admin unchanged.
- ๐ Check your setup NRPEServer now survives a failed listener (logs an ERROR, leaves the module loaded) instead of failing the whole module. Add “NRPE listener failed” as a signal if you alerted on module-load failure.
- ๐ Check your setup
insecure = trueon NRPEServer now logs at ERROR (louder, behaviour unchanged) โ whitelist the message on agents intentionally run insecure.
0.12.5¶
- ๐ Check your setup
[/paths]users: copy entries into[paths]inboot.ini; the settings-side section is no longer consulted. Default installs are unaffected.
- ๐งฉ Check your setup Custom-plugin authors: implement the new optional
prepare_shutdowncallback if your module manages sockets or background threads โunloadis now a last-resort teardown.
- ๐ Monitoring-only WEB deployments:
disable admin user = trueunder[/settings/WEB/server]suppresses the built-in admin even on first boot; define your own read-only users (or a tightly scopedanonymousrole).
0.12.4¶
- ๐ Icinga
check_nscp_apiworks again after upgrade with no config change. For a non-stock probe, set[/settings/WEB/server] legacy query auth user agentsto a substring of its User-Agent. For the strict 0.12.3 behaviour (no query-string credentials at all), set that key to empty.
0.12.3¶
- ๐ Action required Audit
allowed hostson every node โ empty values now reject everything.
- ๐ Action required
check_nt(NSClientServer) now defaults tossl = true; setssl = falseexplicitly if your clients don’t speak TLS.
- ๐ Check your setup Replace clients that call
/auth/tokenor/auth/logoutwith the/api/v2/loginflow, and any that pass?TOKEN=/?__TOKEN=in the query string with a header-based token.
- โฑ๏ธ Action required Scheduler cron expressions on non-UTC hosts shift to local time โ update
them or set
[/settings/scheduler] timezone = utc.
- ๐ง Check your setup Review
check_service/check_process/check_filesfilters that relied on the old (now corrected) behaviours.
- ๐ Action required Restart and review the log for new “refused alias” / “rejected connection” warnings โ configurations that were previously silently accepted.
0.11.33¶
- โ
Check your setup No configuration migration required (new
proxykeys are opt-in). Thecheck_filesfixes change a few corner cases:max-depth=0now scans the top directory (#730); missing paths return UNKNOWN (#613); junction loops are not double-counted (#605); empty results return OK instead of UNKNOWN (#717). Review alerting that relied on the old corner-case behaviour.