0.23.0 Paths that land where you meant them, more enforced TLS verification, and logins that survive a restart¶
0.23.0 is a short release with two themes, and both are about a setting that looked right and did something else.
The first is paths. Expanding a path substituted ${tokens} and never made
anything absolute, so a bare relative name was measured against the process’s
working directory — C:\Windows\System32 for a Windows service, / under a
bare init script, the shell’s directory for nscp test. An unrecognised token
was not an error either: ${scripst}/check.bat quietly became a real path
under the installation folder, and whatever depended on it went somewhere
nobody was looking. A relative value in a setting that writes files now lands
in the folder that setting owns, an unknown token is reported and the setting
skipped, file name = none really switches the log off, and on Windows a
relative external-script command is found from where it is rather than from
where nscp was started.
The second is TLS verification, in the three places where it was configured
but not happening. A listener verify mode parser accepted five spellings and
silently dropped everything else — including fail-if-no-peer-cert, the
spelling the documentation tells you to write — so a carefully configured
mutual-TLS listener was really a listener with an IP filter. The NSCP and
check_mk clients, and check_nsclient_web_online, encrypted their connections
and never authenticated the peer. And a fleet mtls_url that was not
https:// ran the whole management channel — desired state, signed bundles,
certificate renewal — on a plain socket, with the client certificate and the
pinned CA doing nothing. All three now fail closed, which means three of this
release’s notes need reading before you upgrade.
Then the smaller things: you stay logged in to the web UI across a restart, session tokens are only ever held as hashes, a multi-adapter Windows host stops publishing every NIC’s metrics twice, and non-ASCII text in the UI and the reference docs is no longer mojibake.
✨ Highlights¶
- 🔒 A listener
verify modeit does not recognise now refuses to start.verify mode = peer,fail-if-no-peer-certresolved to barepeer: the listener asked for a client certificate and completed the handshake when none arrived. Both spellings are accepted now, along withcertificateandclient-certificate, and any other token makes the listener log it and stop instead of starting with whatever survived the typo. Check every listener you have configured for mutual TLS before upgrading. (#1555) - 🔑 The NSCP and check_mk clients, and
check_nsclient_web_online, verify the server certificate by default. All three encrypted the connection and authenticated nothing, while sending the remote agent’s password over it.verify mode/verifynow defaults topeerandcato the agent’s own trust bundle; a target pointed at an agent still using its self-signed certificate needsca = <that certificate>andverify mode = peer-cert. (#1568) - 🛰️ A fleet management url that is not
https://is refused, and enrollment no longer follows a symlink planted by the service account. Themtls_urlcarries configuration and signed bundles, which is remote code execution by design; on a plain socket neither the agent’s client certificate nor the pinned server certificate did anything.nscp enroll --insecurerecords the decision where plaintext is genuinely wanted. (#1555) - 🗂️ A relative path in a setting that writes files is rooted at the folder
that setting owns. The log file lands in
${log-path}, an attachment in${shared-path}, a crash archive in${crash-folder}, a fleet managed path in${fleet-folder}, a filewriter log in${log-path}— instead of in whatever the service’s working directory happened to be. (#1560) - 🚫 An unknown
${...}path token is an error, and a[paths]override has to name a location of its own. A mistyped token used to resolve to the installation directory; now the setting that carried it is skipped and the log says which token and which setting.file name = noneswitches the log off on Windows too, where it used to create a file calledC:\Program Files\NSClient++none. (#1560) - 🪟 A relative external-script command is found from where it is, not from
where
nscpstarted.check_foo = scripts\check_foo.batworked or failed depending on how it was spelled: an argv-safe spelling resolved against the service’s working directory and failed with “the system cannot find the path specified”. It is rooted at the installation directory now, both ways. (#1560) - 🔐 You stay logged in to the web UI across a restart, and a session token is
only ever held as a hash. Restarting or upgrading the agent no longer logs
every web user out; only the SHA-256 of each key is kept, in memory and on
disk, and a logout removes it immediately.
persist sessions = falsebrings back the old behaviour. (#1549, #1573) - 📥
ext-scr,pyandluaadd --importwork on a fresh install and record a path that resolves. The destination folder is created, the recorded value resolves on both platforms, and an import with no--scriptis refused instead of overwriting the script folder with a file. The Web UI’s script upload goes through the same path. (#1560, #1557) - 🔁 Remote
[/includes]and[/attachments]refresh on their own. An agent configured from anhttp(s)://settings url re-read only the top-level file on each maintenance pass, so an included file stayed pinned to whatever it held at start. Both are fetched every pass now, and a change in either triggers the same reload. (#1560) - 🛡️ The op5 installer profile defaults to secure NRPE, and the Windows
release build verifies what it downloads. The op5 page used to force legacy
mode, hide the radio group so the choice could not be seen, and enable
allow nasty characters; the dependency checksum file added last release had every line readingunrecorded. (#1555)
🔍 Detailed changes¶
🗂️ Paths — resolved against something that does not move¶
Expanding a path substituted ${tokens}; it never made anything absolute. A
value with neither a token nor a leading / was therefore resolved against
whatever the service’s working directory happened to be — four different
answers from the same configuration file, depending on how the agent was
started.
A relative value lands in the folder its consumer owns.
| Setting | Relative values now land in |
|---|---|
[/attachments] target |
${shared-path} |
[/settings/log] file name |
${log-path} |
[/settings/crash] archive folder |
${crash-folder} |
[/settings/fleet] managed path |
${fleet-folder} |
[/settings/filewriter] file |
${log-path} |
On unix this usually changes nothing, because the working directory and the
package directory tended to agree. On Windows it moves a file that was landing
in C:\Windows\System32 into the folder it should always have been in.
An unknown token is reported and the setting skipped. The tokens stay open
ended — anything defined in boot.ini’s [paths] section counts as known —
but ${scripst}/check.bat is now an error in the log rather than a real path
under the install folder that nobody is watching. ${appdata} and
${common-appdata} are Windows-only and are rejected on unix, where they
previously resolved to the installation directory.
A [paths] or --path-override entry that does not resolve to a location of
its own is ignored, with an error naming it, and the built-in default
applies. An override may still be written in terms of other tokens
(scripts = ${shared-path}/mine); it is the resolved value that is judged.
“A location of its own” is slightly wider than “absolute”: a drive-relative
C:mine and a root-relative \mine are accepted on Windows, because the
operator plainly named a drive or a root.
none is a sentinel the path expander knows. [/settings/log] file name =
none is documented as “no log file”, but the name was joined to the
installation directory before the sentinel was tested — so on Windows it
produced a real file called C:\Program Files\NSClient++none and file logging
stayed on. It is recognised by the expander itself now, so it is equally safe
in every setting that accepts it, including every ca option where it means
“use the TLS library’s own trust store”. (#1560)
🪟 Windows: external scripts, found from where they are¶
[/settings/external scripts/scripts] entries that carry a folder — the
conventional check_foo = scripts\check_foo.bat, and what
nscp ext-scr add --import writes — are rooted at ${base-path} before they
are launched.
The old behaviour was easy to miss because it depended on how the command happened to be spelled. A single backslash is not tokenisable as an argument vector, so it fell back to the legacy single-string launcher, where the lookup already honoured the installation directory and the command worked. A command that was argv-safe — a doubled backslash, a forward slash, a quoted path — ran with argv isolation, where the executable is named separately and was resolved against the working directory, and it failed for the service with “the system cannot find the path specified”.
Absolute paths, UNC paths, drive-relative C:check.exe and root-relative
\tools\check.exe are all still used exactly as written. (#1560)
📥 Script folders and imports¶
Both ext-scr CLIs derived their folder by appending a literal scripts
segment to a root rather than naming ${scripts}, the token that already means
that folder:
| Looked in / imported into (before) | Now | |
|---|---|---|
nscp lua list/show/add/delete |
${base-path}/scripts/lua — right on Windows, /usr/sbin/scripts/lua on Linux |
${scripts}/lua |
nscp py list/show/add/delete |
${scripts}/scripts/python — a folder nothing creates |
${scripts}/python |
add --import also records a different value: python/<name> and lua/<name>
relative to ${scripts}, instead of the scripts\python\<name> spelling that
only resolved on Windows.
Three things were wrong with importing a script, each silent in its own way.
copy_file never created the destination folder, so a fresh install died with
a bare No such file or directory; ext-scr recorded a Windows spelling of a
path it never resolves, so the imported command exited 127 on Linux; and
add --import with no --script collapsed the destination onto the script
folder itself and overwrote it with a file. The folder is created, the recorded
path is absolute on Linux, and the missing option is named. The Web UI’s script
upload goes through the same add --import and failed the same way.
The Windows installer creates scripts\custom\ again, and always. It is
not part of the Scripts (SampleScripts) feature, so REMOVE=SampleScripts
no longer leaves an installation with no scripts\ directory at all — which
previously meant an [/attachments] entry, or anything else writing a script,
had nowhere to land. (#1560, #1557)
🔁 Remote includes and attachments¶
An agent configured from an http(s):// settings url re-downloads its whole
configuration every settings maintenance interval (default 5m). Until now
that pass stopped at the top-level file: a file pulled in by [/includes], and
every [/attachments] target, was only re-fetched when the top-level file
itself happened to change. On a server where nsclient.ini is the stable part
and the included file is the one that moves, the include stayed pinned to
whatever it held when the agent started.
Both are fetched on every pass now, and a change in either triggers the same reload a change in the top-level file does. Two things to be aware of: the settings server sees one request per included file and per attachment on every interval (responses are hash-compared, so an unchanged file costs only the request), and a change made only in an included file now takes effect within one interval instead of requiring a restart. (#1560)
🔒 TLS verification that was configured but not happening¶
Listeners. The verify mode parser accepted five spellings and silently
ignored everything else. fail-if-no-peer-cert was not one of the five — but
it is what the permissions guide, the client identity source help text,
OpenSSL itself, the NRPE scenario walkthrough and the reference docs all tell
you to write. So verify mode = peer,fail-if-no-peer-cert resolved to bare
verify_peer: the listener asked the client for a certificate and completed
the handshake when none arrived. An operator whose configuration looked exactly
like the documentation was running an NRPE listener authenticated by the
allowed hosts IP list alone, and any typo degraded the same way, always in
the direction of accepting more.
The parser now takes the same vocabulary as the outbound client parser —
peer or certificate, fail-if-no-cert or fail-if-no-peer-cert or
client-certificate, plus peer-cert, client-once, none, and the context
options workarounds and single — and ignores whitespace around a token.
Anything else is rejected: the listener logs the offending token and does not
start.
Outbound clients. NSCPClient defaulted verify mode to none,
CheckMKClient left it unset (an empty verify mode parses the same as none),
and check_nsclient_web_online defaulted verify to none. All three sent
the remote agent’s password over a connection whose peer was never
authenticated. verify mode / verify defaults to peer now and ca to
${ca-path} — the auto-generated ROOT store export on Windows, the
distribution bundle elsewhere.
Because an agent generates a self-signed certificate on first start, a relay or REST check pointed at a default agent will now fail the handshake where it used to connect:
| Situation | Configuration |
|---|---|
| The remote agent uses a certificate from your own CA | ca = <the CA> (the default verify mode = peer then works) |
| The remote agent still uses its self-signed certificate | ca = <that certificate> and verify mode = peer-cert |
| You accept an unauthenticated link | verify mode = none (encrypted, but an on-path attacker can impersonate the remote) |
Fleet. The enrollment response names mtls_url, the base for the
desired-state poll, the bundle download and the certificate renewal. The
fail-closed guard that refuses mTLS without server authentication only existed
on the TLS path, so a response carrying http://… — or a url with no scheme,
which the http client also opens on a plain socket — built a plain TCP client,
ignored the certificate and the pin, and ran the whole management channel
unauthenticated with nothing in the log to say so. Enrollment refuses a
non-https management url, the sync loop refuses to start on a stored one, and
the http client refuses to attach a client certificate or a pinned CA to a
non-TLS transport at all. nscp enroll --insecure and [tls] allow plaintext =
true record the decision; the sync then logs INSECURE on every start.
Enrollment’s two pre-adopt_owner writes also followed symlinks. The manifest
temporary and the fleet.ini placeholder were created with a plain open and
a plain ofstream in a directory the unprivileged service account owns, so
that account could pre-create either name as a link and have the next
sudo nscp enroll truncate the target as root. Both now open the containing
directory O_NOFOLLOW|O_DIRECTORY and create through that descriptor with
O_CREAT|O_EXCL|O_NOFOLLOW.
The op5 installer profile. Picking op5 on the MSI’s monitoring-tool
page used to force NRPE into legacy mode — anonymous Diffie-Hellman, no peer
verification — hide the mode radio group so the choice could not be seen or
changed, and enable allow nasty characters for every NRPE-reachable check.
It now defaults to the same secure mode the generic profile uses, shows the
radio group so legacy is a visible decision, and does not set allow nasty
characters. allow arguments stays on, because op5’s check commands need it.
(#1555, #1568)
🔐 Web sessions¶
Restarting the service, or upgrading it, no longer logs every web user out: a
browser tab, and a script holding a key from /api/v2/login, keeps working.
Sessions are kept in ${data-path}/nsclient.db and restored at the next start,
still expiring eight hours after login.
Only the SHA-256 of each session key is stored, in memory and on disk, so a copy of the database — or a dump of the process — does not yield a usable token. A session is only restored while the user’s password and role are unchanged, and logging out removes it from the file straight away; it stops working immediately and does not come back, not even if the agent is killed rather than stopped cleanly.
Changing a user’s password or role also ends that user’s sessions, and removing the user ends theirs — but only from the next start of the agent, because the running service keeps the users it read when it started.
persist sessions = false under [/settings/WEB/server] keeps sessions in
memory only, as before. It is also the way to invalidate every session at once
after a suspected leak: restart once with it off. (#1549, #1573)
🐛 Bug fixes¶
- A Windows host stops publishing every network adapter’s metrics twice.
check_network’s two source maps both hold every adapterWin32_NetworkAdapterknows about, andfetchMetricshanded both copies to the bundle under the same key. The JSON views kept whichever came first and dropped the other silently; the OpenMetrics renderer refused the duplicate series and logged one error per field per snapshot — on a five-adapter host, 70 error lines on every metrics tick. (#1575) - Non-ASCII text is no longer mangled into mojibake. UTF-8 read as cp1252
and re-encoded as UTF-8 stays valid UTF-8, so nothing downstream complained:
the module-glue generators read
module.jsonwith a bareopen(), so an em dash in a command description was doubled by a Windows builder and carried into the registry, the REST API,nscp test descand the extracted reference docs. Eight user-visible strings are repaired. (#1576) - WinGet publishing works again. The manifests were bumped to schema 1.12.0
for 0.21.0, which the in-box winget on the runner does not ship, so
validation failed on a schema header mismatch and both releases went
unsubmitted. The header and
ManifestVersionare back to 1.9.0, and the publish step reads the exit code instead of dying on the warning-only result. (#1572) - The Windows release build verifies what it downloads. The checksum gate
added last release was in place but every line in
.github/dependency-checksums.txtreadunrecorded, so each download warned and continued. Digests are recorded for OpenSSL, protobuf, Crypto++, miniz and the prebuiltcheck_nsclient.exe; TinyXML2, Mongoose and MariaDB Connector/C are cloned and verified against a recorded commit rather than fetched as GitHub-generated tag archives. Bumping a dependency now means adding its line first, because a missing line fails the build. (#1555)
⚠️ Upgrade notes¶
- Check the
verify modeof every listener configured for mutual TLS before upgrading. A token this release does not recognise makes the listener log it and refuse to start — which is the point, but it is a restart away. If you believed a listener was requiring client certificates, verify it now: until this release,fail-if-no-peer-certwas dropped and the handshake completed without one. - An
NSCPorCheckMKclient target, or acheck_nsclient_web_onlinecheck, pointed at an agent with its self-signed certificate will now fail the handshake. Setca = <that certificate>withverify mode = peer-cert, pointcaat your own CA, or accept an unauthenticated link explicitly withverify mode = none. - A fleet host enrolled against a plaintext
mtls_urlstops syncing. Re-enroll againsthttps://, or record the decision withnscp enroll --insecure/[tls] allow plaintext = trueinboot.ini. - An op5 host monitored by a
check_nrpewith no client certificate needs one, or Insecure mode picked explicitly on the installer’s monitoring-tool page. The profile no longer forces legacy NRPE, and no longer setsallow nasty characters. - A bare relative path in a setting that writes files moves — on unix
usually not at all, on Windows usually into the folder it should always have
been in. Nothing to do if your paths are absolute or written with
${...}tokens. - An unrecognised
${...}token, and a[paths]override that does not resolve to a location of its own, are now errors that skip the setting instead of quietly resolving to the installation directory.${appdata}and${common-appdata}are rejected on unix. - If
[/settings/log] file name = nonewas set on Windows, a stray file calledC:\Program Files\NSClient++noneis left where it is — delete it once you have checked you do not need its contents. - A relative external-script command that relied on the working directory no
longer resolves. Name the script with an absolute path, or with
${scripts}\<name>. An absolute, UNC, drive-relative or root-relative command is unaffected. nscp luaandnscp pylook in a different folder, and record a different value on import. Existing configuration entries keep working and loading scripts fromnsclient.iniis unchanged.- If you relied on a restart to log every web user out, set
persist sessions = falseunder[/settings/WEB/server]. - A settings server now sees one request per included file and per attachment on every maintenance interval. Responses are hash-compared, so an unchanged file costs only the request — but size the interval accordingly if you serve a large attachment to a large fleet.
Security notices for this release are on the security notices page, and the full list of behaviour changes is on the upgrading page.
Download¶
You can download the new version from GitHub
// Michael Medin