Instance registration and telemetry
Every Nyxtrace installation — free and pro — registers itself with the
Nyxtrace server (https://license.nyxtrace.com) and sends one small heartbeat
per day.
Open source does not mean that nothing is ever sent home. The trade we make instead is radical transparency: the payload is written out field by field on this page, the installation shows you the exact bytes it last sent, and on the free plan you can switch the whole thing off with one variable.
Why it exists
Section titled “Why it exists”- Deployment tracking. How many installations run, in which version, on which operating system, in which country, free or pro. Without that number every product decision is a guess.
- Update notices. The instance learns that a newer version exists and says so in the interface. The heartbeat itself downloads nothing and replaces nothing; the signed automatic updater is a separate path that stays off until you enable it.
- Support. When you ask for help, the version, the source kinds and the size class are already known, so you do not have to collect logs first.
- Product decisions. Which source kinds and connectors are really used decides the order of the connector catalogue.
- The licence check on pro. Registration is the seat binding described under licensing — on the pro plan it is not just statistics.
Explicitly not a purpose: user profiles, per-person usage measurement, advertising, passing data to third parties, or research on foreign networks.
What is sent
Section titled “What is sent”The complete list. There is nothing else in the payload:
| Field | Type | Example |
|---|---|---|
schema_version |
int | 1 |
install_id |
string | ins_9f2b7c1d4e5a6b70c1d2e3f405162738 |
product_version |
string | 0.3.1 |
bundle_version |
string | 2026.09.11 |
os_id |
string | debian |
os_version_major |
string | 13 |
kernel_major |
string | 6.1 |
arch |
string | amd64 |
plan |
free | pro |
free |
license_key_id_hash |
string | null | 4f1c2a9b7e0d3856 — the first 16 hex of a SHA-256 over the key identifier, pro only |
tenant_count |
int | 1 |
device_bucket |
enum | 11-50 |
source_kinds |
string[] | ["netflow","suricata","unbound","zeek"] |
connector_counts |
map | {"unifi":1} — the type and how many instances, never a name |
uptime_bucket |
enum | 1-7d |
learning_phase |
enum | learning | complete | unknown |
ingest_error_bucket |
enum | 0 |
sent_at |
string | 2026-09-11T14:45:00Z |
update |
object | absent | What the auto-updater knows about itself — six fields, each from a closed vocabulary, listed under the update report |
analyst |
object | absent | The installation’s analyst spend in the current quota period — three bounded numbers, Pro with a licence quota only, listed under the analyst usage report |
The counts are buckets, not figures:
| Bucket | Values |
|---|---|
device_bucket |
0-10, 11-50, 51-200, 201-1000, >1000 |
uptime_bucket |
<1h, 1-24h, 1-7d, 7-30d, 30-90d, >90d |
ingest_error_bucket |
0, 1-10, 11-100, 101-1000, >1000 |
That is deliberate. An exact device count, repeated daily, is a behavioural profile of a household; a bucket boundary is not. Every product decision this serves is answered at bucket resolution.
source_kinds are the source categories of the sensor registry —
netflow, suricata, zeek, unbound — not sensor identifiers. zeek.conn
and zeek.dns both become one zeek; a sensor name such as
sensor_ndr01_vsensor is never sent.
The server adds three things of its own: the country the request came from,
its own receipt time, and whether a plan: pro matches an active licence.
The payload, in full
Section titled “The payload, in full”{ "schema_version": 1, "install_id": "ins_9f2b7c1d4e5a6b70c1d2e3f405162738", "product_version": "0.3.1", "bundle_version": "2026.09.11", "os_id": "debian", "os_version_major": "13", "kernel_major": "6.1", "arch": "amd64", "plan": "free", "license_key_id_hash": null, "tenant_count": 1, "device_bucket": "11-50", "source_kinds": ["netflow", "suricata", "unbound", "zeek"], "connector_counts": {"unifi": 1}, "uptime_bucket": "1-7d", "learning_phase": "learning", "ingest_error_bucket": "0", "sent_at": "2026-09-11T14:45:00Z", "update": { "policy": "notify", "channel": "stable", "running_version": "0.3.1", "last_check_at": "2026-09-11T02:14:07Z", "last_result": "up_to_date", "keyring_key_ids": ["nyx-release-2026a"] }, "analyst": { "period": "2026-09", "spent_microusd": 1250000, "runs": 17 }}The analyst object appears only on a Pro installation whose licence carries
an analyst quota; the free installation in the rest of this example would not
send it.
The update report
Section titled “The update report”Since 0.26 the heartbeat additionally carries an optional update object: what
the automatic updater knows about
itself. Every field is optional, and none of them is free text — each value
comes from a closed vocabulary that the server validates against the same
list, so the object cannot become a side channel for anything the table above
excludes.
| Field | Values |
|---|---|
policy |
off, notify, auto-patch, auto-minor, auto — the NDR_UPDATE_POLICY in force |
channel |
stable, beta |
running_version |
The version the updater believes is live; normally equal to product_version |
last_check_at |
When the release channel was last asked — RFC 3339 in UTC at second precision, always ending in Z |
last_result |
One word for the most recent run: up_to_date, available or check_failed from a check; applied, failed, rolled_back or skipped from an attempt |
keyring_key_ids |
The key ids in /etc/ndr/release-pubkeys.json, sorted, at most 8; empty while no release keyring is installed. The published keyring holds nyx-release-2026a — see automatic updates |
The updater’s error text and its history detail stay on the appliance — in
ndr update --history and on the System page. An appliance without updater
state sends no update key at all, and an object that would say nothing is
left out as well. The same whitelist and pattern guard that check the rest of
the payload check the nested object; a value outside its vocabulary is a
refusal that names the field, never a trimmed send.
An older server. The Nyxtrace server has accepted the object since its
0.3.0 release. An older one rejects any heartbeat that carries a key it does
not know with a plain 400 that does not name the field, so the appliance
treats a 400 on a heartbeat that carried the object as exactly that case: it
sends the same heartbeat once more without the object, at once and with no
backoff wait, records the refusal as a refused row (one log line,
status=update_report_withheld) and places a 24-hour hold on the report.
While the hold is in force the heartbeat goes without the object from the
start; when the hold expires the object is tried again, so a server upgrade is
picked up on its own at the cost of one refusal a day until then. A 400 on a
heartbeat that did not carry the object is the ordinary refusal it always
was: it sets no hold and is not retried.
ndr telemetry show and ndr telemetry send --dry-run read the same hold and
print which of the three states applies:
update report: includedupdate report: none (no updater state)update report: withheld until <time>; <reason>The analyst usage report
Section titled “The analyst usage report”Since 0.39 a Pro installation whose licence carries an
analyst quota adds an optional
analyst object: its own account of the analyst spend in the current quota
period, so the portal can show usage against quota and flag an installation
that is over. A free installation, a licence without analyst, and a licence
whose token carries no quota send no analyst key at all.
| Field | Values |
|---|---|
period |
The UTC calendar month, YYYY-MM |
spent_microusd |
0 … 1,000,000,000 — settled and reserved micro-dollars together, across every tenant, in that month |
runs |
0 … 1,000,000 — reservations in that month |
Three numbers, bounded, none of them free text; the same whitelist and pattern
guard check the nested object. It is the same figure the analyst health row
shows as spent_microusd and runs. The number is a running total for the
period: the server replaces the period’s row rather than summing, so a
heartbeat without the object leaves the period alone — “said nothing today” is
not “spent nothing this month”. Nothing about it is billed from: the quota is
a product limit the appliance applies against your own OpenAI key and your own
ledger, and the server flags an installation over it and enforces nothing.
What is never in it: the incident, the prompt, the answer, the model name,
your API key.
An older server, one step further down the ladder. The Nyxtrace server
accepts the object since its 0.4.0 release; an older one answers the whole
heartbeat with a plain 400. The rule from the update report
applies once more, as a ladder rather than a switch: the appliance retries at
once without analyst, then — if that is refused too — without analyst
and update, records the refusal (status=analyst_report_withheld) and
places a 24-hour hold on the dropped object; the next heartbeat starts at
the first variant that omits every held object, and an expired hold is tried
again. ndr telemetry show and ndr telemetry send --dry-run print the
state as a fourth line:
analyst report: includedanalyst report: none (no licence quota)analyst report: withheld until <time>; <reason>What the server does with the sender address
Section titled “What the server does with the sender address”Your address reaches the licence server the way it reaches any other web server. It is not part of the payload above, and the full address is never written down — but “no IP” would be too broad a claim, so here is exactly what happens to it.
- A country code. The address is resolved against a GeoIP database held
locally by the server when the request is accepted, and is then discarded.
countryis the only thing derived from it that is kept: a two-letter code, or empty if the operator runs the server without a country database at all. - A truncated network prefix, for registration and for licence
activation. Each of those writes one row into the server’s append-only
audit log, and that row carries the sender address shortened to a
/24prefix for IPv4 or a/48prefix for IPv6 —192.0.2.0/24, never192.0.2.37. Its purpose is abuse detection: many installation identifiers appearing from one single network is the signal that one licence is being shared across many, and the same prefix is the counter behind the temporary lockout that answers a burst of wrong keys. A prefix of the last activation is likewise what the licence record keeps, and the server’s own request log records the truncated prefix in place of a full address. - Nothing at all for a heartbeat. The daily heartbeat writes no audit row, so it leaves behind no prefix: the country code is the only thing that survives the request.
A /24 is a network, not a person: it is the smallest unit that still makes a
shared licence visible, and the largest that is of no use for following one
household around.
What is never sent
Section titled “What is never sent”IP addresses — not even hashed — MAC addresses, host names, domain names, device names, user names, email addresses, tenant identifiers, sensor identifiers, site names, coordinates, VLAN names, CIDRs, flows, incident contents, incident titles, model names from your own YAML models, evidence, analyst text, configuration files, tokens, or a licence key in clear text.
That list is about the payload. The address the request arrives from is a separate question, and it is answered above under what the server does with the sender address.
That is not a statement of intent but an enforced property of the client. Two independent layers run before every send, and before every dry run:
- A field whitelist. The finished payload is serialised and read back generically; every key must be in the table above and every value must match its rule. An unknown key — including one a later change adds by accident — fails the send.
- A pattern guard over the whole JSON string: IPv4, IPv6, MAC in both
notations, email addresses, and forbidden key names such as
ip,host,hostname,domain,user,tenant,sensor,device_name,serial,flow,incident,evidence,token,secret,password.
A violation aborts the send rather than trimming the payload. One layer
would not be enough: the whitelist catches a new field but not a host name that
ended up inside bundle_version, and the guard catches the host name but not a
harmlessly named new field with sensitive content. The code is open, so you can
check this yourself instead of believing it.
The installation identifier
Section titled “The installation identifier”/etc/ndr/install-id, mode 0600, owned by the service user: ins_ plus 32
random hex characters, written once and then left alone.
It is not a device identifier. It belongs to the installation, not to the hardware: no machine ID, no MAC address, no serial number and no CPU ID is read, so there is no hardware fingerprinting anywhere in the product. Creation is atomic and idempotent — two parallel starts produce one identifier, not two — and an existing but malformed file is reported rather than overwritten, because on the pro plan overwriting it costs a seat.
Put /etc/ndr/install-id in your backup. Losing it makes the installation look
like a new one and costs a pro seat until the server side is corrected.
When it sends
Section titled “When it sends”| Event | When |
|---|---|
| Registration | On the first start, once |
| Heartbeat | Daily, at a fixed minute derived from the install_id so not every instance sends at the same time |
| Extra heartbeat | Immediately on a version change (product_version or bundle_version) |
| Update check | Right after a successful heartbeat |
Restarting does not send anything unless it is genuinely due or the version changed: a restart storm must not become a request storm. A failed send is retried with backoff — 30 seconds doubling to a 15-minute ceiling, at most five attempts per cycle — and does not displace the next regular slot.
A failing server is visible and harmless. The telemetry health component turns
degraded, and ingest, analysis, detection, the interface and the API are
untouched. Nothing about the heartbeat delays or blocks a start.
The update check
Section titled “The update check”After a successful heartbeat the appliance asks whether a newer version exists and gets back the latest version, its channel, its release date, whether it is a security release, a short notice and a release URL. The interface renders that and nothing more: it downloads nothing and replaces nothing, and the signed update channel used by an actual upgrade is separate. See upgrades.
A notice from the server is treated as untrusted data: plain text, length limited, no markup, no link rendering, no authority to do anything.
Where to see what was sent
Section titled “Where to see what was sent”Three places show the same record, so you never have to take a claim on trust:
The interface. System has a Telemetry & registration section with the status, the endpoint, the registration state, the last attempt and its result, and the exact last payload as expandable JSON — the stored record, not an example.
The command line, which opens no connection at all:
ndr telemetry show # the payload that would be sent nextndr telemetry send --dry-run # build, sign, check and record it; still no connectionThere is deliberately no ndr telemetry send without --dry-run: a tool that
can send by accident contradicts the promise this page makes. Both commands
print an update report: line — included, none (no updater state) or
withheld until <time>; <reason> — so you can see whether the
update report is part of the next heartbeat; the dry run
signs and records the exact body the service would send.
The health API. GET /api/v1/health carries a telemetry component line
and a telemetry object with the same fields, including the payload. The
payload is visible to every reader — hiding it would defeat the point — while
the failure detail is admin-only, because a failure reason can contain a
host-local address.
Switching it off
Section titled “Switching it off”# /etc/ndr/ndr.env (mode 0600)NDR_TELEMETRY=offsystemctl restart ndrThe switch takes effect at the next start of the unit, not immediately.
On the free plan that is all it does. With telemetry off the appliance opens
no connection, creates no /etc/ndr/install-id and writes no log row; the
System page shows Off and the telemetry health component reports
unavailable with the reason disabled. No banner, no delay, no reduced
function, no nagging.
On the pro plan registration is required, because it is part of the licence
check — the seat is bound to the install_id. The variable still works, but
switching it off also ends entitlement renewal, so the Pro modules go read-only
after the grace period exactly as they do for an expired subscription. There is
no state “Pro active, but not registered”, and there is no punishment beyond
that: no data loss and no ingest stop.
NDR_TELEMETRY_URL points the client at a different origin, for an organisation
that wants to collect this itself. In production it must be https.
Retention
Section titled “Retention”| Data | Kept |
|---|---|
| Raw heartbeats, one record per send | 90 days, then deleted |
| Aggregates (counts per day, version, country, plan, bucket) | Indefinitely |
| Registration record (identifier, secret hash, plan, country, first and last seen) | While the installation is active, plus 90 days |
| Full sender IP address | Never stored — not in the database, not in a log line, not at the server in front |
The /24 (IPv4) or /48 (IPv6) prefix written for registration and activation |
In the append-only audit log, kept |
A deletion request is answered through the install_id: the raw and the
registration records are deleted, and the aggregates remain, because they carry
no link to an installation. The audit rows remain as well — an append-only
record that a registration or an activation happened is what makes the log
worth keeping at all, and it holds a network prefix rather than an address.
The short version
Section titled “The short version”Telemetry and instance registration
Every Nyxtrace installation registers itself with the Nyxtrace server and sends one small heartbeat per day. We do this to know how many installations exist, on which versions, so we can ship update notices, support you, and decide what to build next.
What we send: a random installation ID, the product and bundle version, the operating system and kernel major version, the CPU architecture, the plan (free or pro), the number of tenants, the number of devices as a bucket (0–10, 11–50, 51–200, 201–1000, more), which kinds of sources are active (NetFlow, Suricata, Zeek, Unbound, …), how many connectors of each type are enabled, an uptime bucket, whether the learning phase is still running, an ingest-error bucket, a timestamp, and what the automatic updater knows about itself — its policy, channel, running version, when it last checked and the one-word result, and the ids of its release keys; each of these is a value from a fixed list, never free text. A Pro installation with an analyst quota also sends the month and how much of that quota it has spent, as a number of micro-dollars and a number of runs — never what the analyst read or wrote. Our server adds the country your request came from, derived from the sender IP.
What we never send: IP addresses, MAC addresses, host names, device names, user names, flows, incident contents, tenant data, configuration, keys. This is enforced in the client: the payload is checked against a field whitelist and a pattern guard before every send, and a violation aborts the send. The code is open, so you can verify this yourself.
We do not store your IP address. For the one-time registration and for a licence activation we do record a shortened address range — a
/24for IPv4, a/48for IPv6 — in our audit log, so that one licence being used from many different networks stays visible; the daily heartbeats record no range at all. We keep raw heartbeats for 90 days and anonymous aggregates indefinitely.The System page in your installation shows the exact payload that was last sent. To turn telemetry off, set
NDR_TELEMETRY=offand restart the service. On the free plan that is all it does. On the pro plan registration is part of the licence check, so turning it off also ends entitlement renewal; your data, incidents and ingest keep working.