Skip to content

0.22.0 Metrics a scraper can actually read, Mod-Gearman without an open port, 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 # UNIT where the value is measured in something, counters are typed as counters, string readings come back as _info families, 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, so sum by (core) has something to group on. The JSON endpoints, the dashboard, Graphite, collectd and Python submit_metrics report the same keys and values as before. openmetrics format = legacy reproduces 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 = agent answers for the host it runs on; mode = proxy runs 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 plain http:// nothing authenticates the server. Opt back in per host with allow plaintext = true in boot.ini; every plaintext fetch is then logged as INSECURE. (#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_smtp mail; 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_tcp and check_ssh verify the server certificate by default. verify defaulted to none, so a TLS check handshook against any certificate at all and reported ok. It now defaults to peer with ca= 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.pb let a caller write the header the core reads its identity from; its only consumer was NSCPClient, which had never actually worked. check_remote_nscp and 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 a filter=, warning= or detail-syntax= as you type it โ€” a keyword the check does not offer is red before the check is ever run. A new GET /api/v2/queries/{query}/help is 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/.zip are back (without PythonScript, and the ARM64 MSI does not bundle the VC runtime), and NSCP-<version>-debian-trixie-arm64.deb covers 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=1 serves the same keys and values with their help text, unit, type and labels under a metadata object. Without meta the 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 stores path;core=0 as the name).
  • CollectdClient mappings can read the labels and the types. A variable set to label:core expands to every value of that label instead of a regular expression over flat keys, and auto: sends whatever the producing module declared a counter as a DERIVE and everything else as a GAUGE.
  • Predefined PDH counters can describe themselves with help and unit keys, and Python fetch_metrics accepts a dict per value ({"value": 42, "help": "โ€ฆ", "unit": "bytes", "type": "counter"}, plus "labels").
  • Windows only: system.mem.page.% and system.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_nt FILEAGE checks one file instead of a directory of them. It mapped onto check_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 onto check_single_file now; a directory fails with an error.
  • The Windows installer no longer rewrites NRPE transport security it did not configure. The MSI recognised only insecure = true and 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 install writes use ssl, not ssl = true โ€” a key the server never reads. On a host where use ssl = false had 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, --filter and --counter shared one variable, so whichever came last won and the others were silently discarded; --filter is repeatable now. Matching is case insensitive, so --list disk finds 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_ping keeps 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’s host= 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 as libcrypto-3-x64.dll / libssl-3-x64.dll now 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.dll absorbed 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.msi and .zip. Two differences from x64: no PythonScript (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 ci and gated on npm audit, so the bytes in the web zip and in the MSI’s web/dist match package-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.idle is system_cpu_idle_percent{core="0"}, disk_free_C:.total is disk_free_total_bytes{drive="C:"}, workers_jobs is workers_jobs_total. Exclude core="total" from anything that aggregates over cores. If dashboards cannot be updated first, set openmetrics format = legacy under [/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 to https://, or set allow plaintext = true under [tls] in boot.ini per host.
  • check_tcp / check_ssh against an internal or self-signed service now fail with tls_handshake_failed. Point ca= at the issuing CA or add verify=none. sni= on a non-TLS connection is now rejected rather than ignored.
  • POST /query.pb is gone. If you have an NSCP client target, remove an explicit path or point it at /api/v2/queries, expect TLS unless you set no ssl = true, and give the remote’s admin user the queries.execute grant. exec_remote_nscp and submit_remote_nscp are removed.
  • Requests may no longer weaken a credentialed target’s transport security (verify, insecure, ssl, ca, tls-version, proxy, โ€ฆ) or re-address submit_smtp mail. Supply the credentials with the request, configure the variant as its own target, or set allow host override = true / allow recipient override = true.
  • External scripts with user, domain or password set are refused on Linux โ€” those keys were never implemented there. Put the identity change in the command itself with sudo.
  • The web UI cannot be framed any more, and check_nt FILEAGE naming 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 alongside modules\*.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.

Download

You can download the new version from GitHub

// Michael Medin