Metrics a scraper can actually read, Mod-Gearman, and a much smaller install
0.22.0 rebuilds the metrics side of the agent. /api/v2/openmetrics served the
JSON keys verbatim — names with dots, spaces and colons in them, no types, no
help text, six significant digits — which a strict parser rejected outright.
It now serves a conformant, self-describing OpenMetrics document: every family
named to the grammar, typed, described, carrying its unit, and per-instance
readings collapsed into one family with a core, nic or drive label
instead of one family per core. Every metric name changes, so read the upgrade
note before you upgrade a scraped host.
The second theme is a new transport. GearmanClient makes the agent a
Mod-Gearman worker: a Naemon or Nagios Core keeps scheduling its checks, the
agent pulls them off a gearmand job server and answers them as native
queries, and the monitored host needs no inbound port at all. The same module
submits passive results back into the core's result queue. It ships marked
experimental and has not been exercised at scale: it is verified end to end
against a real job server on both cores, but no one has yet run it across a
large estate, so put it on a slice of your hosts before you move a whole
Mod-Gearman installation onto it.
Then a long tail of security work — the low-severity tier of the audit, in
three parts, plus a second undefined-behaviour sweep and the ten worst
threading bugs — and a packaging release: common code moved out of every
plugin into shared libraries, Windows ARM64 is back, and there is a Raspberry
Pi package.
✨ Highlights
- 📊 A conformant OpenMetrics exposition, and every metric name changes.
Names follow the grammar (system.cpu.core 0.idle→
system_cpu_idle_percent{core="0"}), every family carries# TYPE,# HELP
and a# UNITwhere the value is measured in something, counters are typed
as counters, string readings come back as_infofamilies, values keep their
full precision and the body ends with# EOF. Per-core, per-NIC, per-drive
and per-process readings become one family with a label, sosum by (core)
has something to group on. The JSON endpoints, the dashboard, Graphite,
collectd and Pythonsubmit_metricsreport the same keys and values as
before.openmetrics format = legacyreproduces the old body as a migration
window. (#1533, #1535, #1538, #1539) - 📥 New module:
GearmanClient, a Mod-Gearman worker and result channel.
Optional, off by default, and it speaks both flavours — ConSol's Mod-Gearman
for Naemon and Nagios Enterprises' Nagios-Mod-Gearman for Nagios Core 4.5+.
mode = agentanswers for the host it runs on;mode = proxyruns a whole
hostgroup's checks from one box. Windows and Linux. Experimental and not
yet tested at scale — try it on a few hosts first. (#1542) - 🔒 A settings source, include or attachment that is not
https://is
refused. The remote store is the agent's whole configuration, re-read at
boot and on every housekeeping pass, and over plainhttp://nothing
authenticates the server. Opt back in per host withallow plaintext = true
inboot.ini; every plaintext fetch is then logged asINSECURE. (#1529) - 🛡️ The audit's low-severity tier lands, in three parts. A request may no
longer weaken a credentialed target's transport security or re-address
submit_smtpmail; script arguments,check_docker's endpoint and the
remote-connection checks are confined; and the web server sends browser
hardening headers, negotiates TLS 1.3 on Linux and stops minting a session
token per request. (#1550, #1551, #1552) - 🔑
check_tcpandcheck_sshverify the server certificate by default.
verifydefaulted tonone, so a TLS check handshook against any
certificate at all and reportedok. It now defaults topeerwithca=
falling back to the agent's own trust bundle, and the checks gained
certificate identity, SAN and STARTTLS-service keywords. (#1547) - 🔁 Agent-to-agent checking works, over the REST API, and the raw protobuf
web API is gone.POST /query.pblet a caller write the header the core
reads its identity from; its only consumer wasNSCPClient, which had never
actually worked.check_remote_nscpand friends now go through
/api/v2/queries, authenticated. (#1552) - 🧪 New modules and check commands are marked experimental. A statement
about stability, not about breakage:nscp test, the web UI, the REST API
and the reference docs all say which checks may still change their options,
keywords or output. (#1543) - 🎨 The prompt and the web UI paint filter expressions. Both now read the
check's own vocabulary and colour afilter=,warning=ordetail-syntax=
as you type it — a keyword the check does not offer is red before the check
is ever run. A newGET /api/v2/queries/{query}/helpis what the browser
reads. (#1540, #1541) - ⏱️ Reloads wait for the checks that are running. A settings reload holds
new checks off a module while it applies the new configuration, a module can
no longer unload or restart itself from inside a request it is serving, and
connect and TLS handshake now count against a client's configured timeout.
(#1531) - 📦 A fifth off the Linux install, and much more off Windows. Code every
plugin compiled privately moved into shared libraries, and OpenSSL ships as
DLLs instead of being linked into each of NRPE, NSCA, check_mk, the web
server and the HTTP clients. (#1544, #1556) - 🪟 Windows ARM64 is packaged again, and there is a Raspberry Pi package.
NSCP-<version>-ARM64.msi/.zipare back (withoutPythonScript, and the
ARM64 MSI does not bundle the VC runtime), and
NSCP-<version>-debian-trixie-arm64.debcovers Raspberry Pi OS 64-bit and
Debian 13 arm64. (#1546)
🔍 Detailed changes
📊 OpenMetrics — named, typed, described and labelled
The endpoint used to paste the JSON keys into the exposition verbatim. Names
carried ., %, spaces and colons (system_mem_commited.avail,
system_cpu_core 0.idle, disk_free_C:.total); there was no # TYPE and no
# EOF; values were truncated to six significant digits, so 16 GB of memory
scraped as 1.6554e+10; monotonic counts were typed as gauges, so rate()
was unsafe on them; and string readings — uptime, boot time, MAC address,
power source — were dropped entirely. A strict parser rejected the body, and
the scenario page told you to repair the names with metric_relabel_configs.
Three things changed, and each of them renames families.
Names follow the grammar. % becomes the word percent, everything else
outside [a-zA-Z0-9_] becomes _, runs collapse, and a name that would not
start with a letter borrows a metric_ prefix.
| JSON key | Metric name |
|---|---|
system.mem.physical.%
| system_mem_physical_percent
|
system.cpu.core 0.idle
| system_cpu_core_0_idle
|
disk.free.C:.total
| disk_free_C_total
|
Every metric carries a description, a type and a unit. # HELP on every
built-in family, # UNIT wherever the value is measured in something — which
renames the family again, since a family that declares a unit has to end in it
(system_mem_physical_total → system_mem_physical_total_bytes). Counters
are typed as counters and carry the reserved _total suffix on their sample;
strings come back as an _info family in the node_uname_info shape.
Per-instance metrics are one family with a label.
# TYPE system_cpu_idle_percent gauge
system_cpu_idle_percent{core="0"} 93
system_cpu_idle_percent{core="1"} 91
system_cpu_idle_percent{core="total"} 95
The label is core, cpu, nic, zone, battery, exe, disk, drive
or pdh_instance depending on the bundle — pdh_instance rather than
instance, because Prometheus attaches its own instance label to every
sample. Windows and Linux spell a CPU core differently in the JSON key
(core 0 and core_0); neither spelling reaches the label, which is the bare
0 on both, so one query works across a mixed fleet.
Around that:
/api/v2/metrics?meta=1serves the same keys and values with their help
text, unit, type and labels under ametadataobject. Withoutmetathe
endpoint is byte for byte what it was.- GraphiteClient can send the labels as carbon tags (
metric tags = true,
off by default — a carbon older than 1.1 storespath;core=0as the name). - CollectdClient mappings can read the labels and the types. A variable set
tolabel:coreexpands to every value of that label instead of a regular
expression over flat keys, andauto:sends whatever the producing module
declared a counter as a DERIVE and everything else as a GAUGE. - Predefined PDH counters can describe themselves with
helpandunit
keys, and Pythonfetch_metricsaccepts a dict per value
({"value": 42, "help": "…", "unit": "bytes", "type": "counter"}, plus
"labels"). - Windows only:
system.mem.page.%andsystem.mem.physical.%report
different numbers, because they were reporting the wrong thing — both
divided the commit charge by the commit limit. Alert thresholds tuned
against the old reading need re-checking.
📥 GearmanClient — Mod-Gearman, either flavour
A Naemon or Nagios Core installation running Mod-Gearman keeps its scheduler,
its check definitions and its escalations; the agent registers for the queues
you name, pulls jobs off gearmand and answers them as native NSClient++
queries. Nothing listens on the monitored host.
| Setting | Effect |
|---|---|
mode = agent
| Answers only for the host it runs on; a job for another host_name is answered UNKNOWN
|
mode = proxy
| Answers every check on the queues it registered — one Windows box running a whole hostgroup through check_nrpe, check_wmi and the rest
|
The same module submits passive results into the core's result queue
(/settings/gearman/client, channel GEARMAN, command submit_gearman),
which is what lets a Mod-Gearman installation drop NSCA. The two halves are
independent.
Two things to unlearn when writing the check_command on the core, since
neither fails in a way that names the cause: write host=$HOSTADDRESS$, not
-H $HOSTADDRESS$ (a two-character first token puts the argument parser into
key-value mode and the check answers with a help screen), and
warning=load gt 80, not warning=load>80 (> is a metacharacter, and
allow nasty characters is false by default). See
Mod-Gearman for the full
setup on either core.
The module is marked experimental, and that mark is doing real work here:
its settings, queue handling and output may still change, and while it has been
run end to end against a real gearmand on both cores, it has not been tested
at scale — not against hundreds of hosts, a deep job backlog, or a proxy
answering for a large hostgroup. Roll it out to a slice of the estate first and
keep the existing transport until you are satisfied. Reports of how it behaves
on a real workload are exactly what the experimental mark is asking for.
(#1542)
🛡️ Security
The low-severity tier of the audit landed in three parts, alongside a second
undefined-behaviour sweep and the settings/web/build hardening from #1529.
Outbound clients. The request-override guard now covers the keys that
decide how a connection is protected — verify, insecure, no-psk,
ssl, tls-version, ca, certificate, allowed-ciphers, dh — and, for
SMTP, recipient and sender (behind their own allow recipient override).
proxy= and no-proxy= count as moving the request, since a caller-chosen
proxy receives it whole with the configured NRDP token in it.
payload-length is clamped to what each protocol accepts, 65536 for NSCA and
1 MiB for NRPE. (#1526, #1550)
Scripts and check targets. A NUL in a script argument is refused —
CreateProcessW reads the command line as a C string, so it truncated there
and silently dropped every operator-fixed argument after the substitution
point. ext-scr add --import reads only from the script folders. A script now
receives only its own stdin and its own stdout/stderr pipe ends instead of
every inheritable handle of the service. host= on a docker check must match
the configured endpoint. And on Linux, a script section with user,
domain or password set is refused rather than run as the service account:
those keys are implemented by the Windows launcher only, so a script sandboxed
with user = nobody was not sandboxed at all. (#1523, #1551)
The web server. Every response carries X-Frame-Options: DENY,
X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer and a content
security policy, plus Strict-Transport-Security over TLS — so a page that
embeds the agent's UI in a frame will stop working, which is the point.
[/settings/WEB/server] gains tls version (default 1.2+) and
allowed ciphers. Logging out calls DELETE /api/v2/login, so the bearer
token stops working immediately; the in-memory log buffer is capped at 1000
entries, where an unauthenticated peer could previously add one per rejected
request for the life of the process; and nscp web install-ui no longer stages
its download in ${temp}. (#1552)
Elsewhere. The shared /settings/default/password is registered sensitive
by the core rather than by whichever server module happens to be loaded, so
every agent redacts it. Elastic and Op5 submissions log when credentials go
over an unverified https link, as the Icinga, NRDP and NRPE clients already
did — and the Icinga warning, which repeated on every submission, is logged
once per target now. allowed hosts finally understands the * ranges it has
always advertised (192.168.1.*, 10.*, a bare *); such an entry used to
throw out of the address parser and take the whole listener with it. The
Windows release build pins every third-party action to a commit and verifies
every download against .github/dependency-checksums.txt. (#1529, #1532,
#1534, #1537)
Fleet bundle signatures changed shape, and this one needs action. An
Ed25519 signature used to cover the bare SHA-256 digest of a bundle's bytes,
which bound nothing about which bundle those bytes were: an old signed blob
could be re-advertised under a new id, name or version and still verify. It now
covers a canonical descriptor of the bundle's identity, which means the fleet
server must be upgraded before — or at the same time as — the agents. A
mismatch is safe but inert: the agent logs signature verification failed,
keeps the configuration it last applied, and stops picking up changes. (#1536)
🔁 Agent-to-agent checks over REST
POST /query.pb and POST /settings/query.pb are removed. The first handed
the core a message whose header the caller wrote, and the core reads the
calling module and user out of that header. The second had been unreachable for
several releases. Nothing that talks to an agent over HTTP used them: Icinga's
check_nscp_api asks for GET /query/{name}, untouched, and the web UI uses
/api/v2.
Their only consumer was NSClient++ itself, through NSCPClient — and that
never worked: it passed the serialized message as the HTTP request target, so
every request was malformed, and the configured password was never sent.
check_remote_nscp, remote_nscp_query and remote_nscpforward now run the
command through GET /api/v2/queries/{command}/commands/execute. If you have
an NSCP target configured:
| Setting | Was | Now |
|---|---|---|
path
| /query.pb
| /api/v2/queries
|
| TLS | off unless ssl = true
| on unless no ssl = true
|
password
| read, never sent | sent; the remote's admin user needs queries.execute
|
exec_remote_nscp and submit_remote_nscp are removed rather than ported:
both posted an NRPE-style string to the protobuf route, which the remote parsed
as an empty message and answered with nothing — and the submit path reported
success regardless. Use NSCA, NRDP or another submit client for passive
results. (#1552)
🧪 Experimental modules and commands
A module or check command that is new enough that its options, filter keywords
and output may still change now says so: nscp test appends (experimental)
in queries, aliases, list and plugins and shows a Status: line in
desc, the web UI shows a chip, the REST API reports an experimental field,
and the reference documentation renders a marker in the command tables and a
note on the command. The recently added check_* commands of CheckDisk,
CheckDocker, CheckNet, CheckNSCP and Scheduler are marked, as are the
CheckMSSQL, CheckMySQL, CheckSecurity, CheckWindowsApps,
GearmanClient and NSCANgClient modules in full. Out-of-tree modules declare
it in module.json. (#1543)
🎨 Filter expressions, painted
The prompt already knew a command name from a typo. It did not know anything
about what came after the =, which is where the mistakes actually are:
filter=fre < 10% is a check that runs, matches nothing and reports OK. The
value of every option that takes a filter expression or a syntax template is
now read as what it is, following the where grammar and the placeholder rules
the engine itself uses — a keyword the check offers is painted, one it does not
is red, before enter is pressed. Filter functions, operators and number+unit
literals each get their own colour.
The web UI's arguments field does the same, from the same registry data:
GET /api/v2/queries/{query}/help returns one check's whole vocabulary — every
option with its default, flags and description, and every filter keyword it
offers. An alias declares no keywords of its own, so the endpoint follows it to
the command it stands for and says which one in keyword_source. (#1540,
#1541)
⏱️ Threading, reloads and timeouts
The ten highest-severity findings of the concurrency audit are fixed, and
several of them are visible from outside:
- A settings reload holds new checks off a module while its
loadModuleEx
applies the new configuration, and waits up to five seconds for checks
already inside it to return. A reload is no longer instantaneous on a busy
agent; if a check holds a module longer than that, the reload proceeds anyway
and logs which module it was. - Unloading the module that is serving the request is refused instead of taking
the agent down with it — in practice
POST /api/v2/modules/WEBServer/commands/unload. Reloading a listener from
inside a check that the same listener is serving is refused too, instead of
leaving that listener dead. - Configuration downloaded over HTTP gives up on a read or write that stalls
for 30 seconds, instead of waiting indefinitely, and no longer holds the
settings instance lock while it downloads. - Submissions over NRPE, NSCA, NSCP and check_mk apply the configured timeout
to connecting and to the TLS handshake, not just to the exchange.
Underneath: the Lua script manager and the collector pointers are published
atomically so a reload cannot free them under a running check, the scheduler
watchdog no longer re-arms the pool during shutdown, a socket server refuses to
be stopped from one of its own threads, the Python function registry is read
under the GIL, and a CheckEventLog filter object no longer closes a handle it
does not own. (#1531, #1534)
🐛 Bug fixes
check_ntFILEAGEchecks one file instead of a directory of them. It
mapped ontocheck_files, which walks a whole tree, so a directory argument
reported whichever file the walk happened to emit first — not the oldest, not
the newest, just arbitrary. It maps ontocheck_single_filenow; a directory
fails with an error.- The Windows installer no longer rewrites NRPE transport security it did not
configure. The MSI recognised onlyinsecure = trueand
verify mode = peer-cert, so a listener running TLS without client
certificates, or one with a hand-written cipher string, fell through and got
a preset applied over it. (#1562) nscp nrpe installwritesuse ssl, notssl = true— a key the server
never reads. On a host whereuse ssl = falsehad been set previously, the
command claimed encryption and client-certificate authentication while the
listener stayed in plaintext. The legacy cipher default also drops its
!ADH, which never excluded the anonymous elliptic-curve suites.- The PDH counter browser narrows on every filter and ignores case.
--list,--filterand--countershared one variable, so whichever came
last won and the others were silently discarded;--filteris repeatable
now. Matching is case insensitive, so--list diskfinds what--list Disk
finds. - A round-robin counter with a zero or unparseable buffer size is refused
and named in the log, instead of being loaded with a buffer that holds
nothing. - A collectd value list naming a metric the snapshot does not carry is no
longer sent as a zero — it reported a measurement nobody took. Such a value
list is skipped in whole, since a collectd value list is positional. check_pingkeeps listening for its own echo reply instead of giving up
on the first reply that arrives, an HTTP status line with a reason phrase
parses, every line of a remote check result is kept, and a string Python
cannot encode no longer crashes the agent.check_dns'shost=description said the wrong thing. (#1545)
📦 Packaging
- Shared runtime libraries.
nscp_net.dll(sockets and TLS),
nscp_client.dll(the sender modules' shared command line),nscp_json.dll
and OpenSSL aslibcrypto-3-x64.dll/libssl-3-x64.dllnow ship next to
nscp.exe, one copy for the whole service, where before NRPE, NSCA,
check_mk, the web server, the HTTP clients and the checksum checks each
carried their own.plugin_api.dllabsorbed the settings and program-options
helpers. About a fifth off the install on Debian and RedHat, and more than
that on Windows. The legacy XP build still links everything statically.
(#1544, #1556) - Windows ARM64 is back as
NSCP-<version>-ARM64.msiand.zip. Two
differences from x64: noPythonScript(the package is cross-compiled and
there is no ARM64 CPython to embed), and the MSI does not bundle the Visual
C++ runtime, because Microsoft ships no ARM64 merge module for this toolset —
install
vc_redist.arm64.exe
first on a fresh machine. (#1546) - A Raspberry Pi OS package,
NSCP-<version>-debian-trixie-arm64.deb,
built on Debian 13 for Raspberry Pi 3 and newer and for Debian 13 arm64 in
general. 64-bit only, and without the managed (C#) plugin API, since Debian
does not package the .NET SDK. (#1546) - The web bundle is built with
npm ciand gated onnpm audit, so the
bytes in the web zip and in the MSI'sweb/distmatchpackage-lock.json.
Pull requests build only the newest RedHat, and a failed dependency download
fails at the download instead of somewhere later.
⚠️ Upgrade notes
- Every OpenMetrics family name changes. Drop any
metric_relabel_configs
block that rewrote dots to underscores — the agent does that itself now.
Update dashboards, recording rules and alerts:system_cpu_core 0.idleis
system_cpu_idle_percent{core="0"},disk_free_C:.totalis
disk_free_total_bytes{drive="C:"},workers_jobsisworkers_jobs_total.
Excludecore="total"from anything that aggregates over cores. If
dashboards cannot be updated first, setopenmetrics format = legacyunder
[/settings/WEB/server]as a migration window — it is deprecated and will be
removed. - Upgrade the fleet server before or with the agents. Bundle signatures now
cover the bundle's identity, not only its bytes. A version mismatch is inert
rather than damaging: the agent keeps the configuration it last applied and
stops picking up changes. - A settings source, include or attachment over plain
http://is refused.
Move it tohttps://, or setallow plaintext = trueunder[tls]in
boot.iniper host. check_tcp/check_sshagainst an internal or self-signed service now
fail withtls_handshake_failed. Pointca=at the issuing CA or add
verify=none.sni=on a non-TLS connection is now rejected rather than
ignored.POST /query.pbis gone. If you have anNSCPclient target, remove an
explicitpathor point it at/api/v2/queries, expect TLS unless you set
no ssl = true, and give the remote'sadminuser thequeries.execute
grant.exec_remote_nscpandsubmit_remote_nscpare removed.- Requests may no longer weaken a credentialed target's transport security
(verify,insecure,ssl,ca,tls-version,proxy, …) or re-address
submit_smtpmail. Supply the credentials with the request, configure the
variant as its own target, or setallow host override = true/
allow recipient override = true. - External scripts with
user,domainorpasswordset are refused on
Linux — those keys were never implemented there. Put the identity change in
the command itself withsudo. - The web UI cannot be framed any more, and
check_nt FILEAGEnaming a
directory now fails instead of reporting an arbitrary file's age. - A hand-rolled deployment must copy the new shared libraries from the
installation root alongsidemodules\*.dll, the OpenSSL DLLs included.
Installing from the MSI or the Debian/RedHat packages needs nothing. - On Windows ARM64, install the VC++ ARM64 redistributable before the agent
on a fresh machine, and use the x64 package under emulation if you need
PythonScript.
Security notices for this release are on the
security notices page, and the
full list of behaviour changes is on the
upgrading page.
Full Changelog: 0.21.0...0.22.0