Skip to content

Security and privacy model

Metadata about connections: who talked to whom, when, for how long, over which service, with which certificate fingerprint, under which DNS name, with which byte and packet counts.

Not payloads. Not credentials. Not whole log files. The browser API never returns raw payload JSON, and the analyst — when enabled — receives only metadata.

On your appliance. The database and the state store are on that machine, the Go core binds to loopback, and the dashboard layer binds to loopback until you deliberately configure otherwise.

Four things can leave. Three of them are off or explicit by default:

Leaves when What goes Default
The analyst is enabled Incident metadata to the configured provider Off
A sink connector is configured Incident and health summaries to that endpoint None configured
An alert contact point is configured Alert text to your notification endpoint Paused, no contact point
Always, unless switched off One daily heartbeat to the Nyxtrace server: version, OS, plan, bucketed counts On, NDR_TELEMETRY=off disables it

The fourth one is the exception, and it is stated plainly rather than buried: Nyxtrace does phone home. Every installation registers itself once and then sends one minimal heartbeat a day. No network data is in it — no addresses, no names, no flows, no incident content — and the exact field list, the enforcement, the retention and the switch are documented under instance registration and telemetry. The System page shows the exact payload that was last sent.

Pro adds a second connection: the online licence check. Entitlements are looked up on the Nyxtrace licence server and renewed as short-lived signed tokens, so a paid module needs a working internet connection — see licensing. The free core never does, and no licence check, failed heartbeat or unreachable server can stop ingest, detection or the interface.

None of this reads anything about your hardware: there is no hardware fingerprinting, and the installation identity is a random value the appliance writes for itself.

Every record belongs to exactly one tenant, and nothing crosses that line: reads, cursors, writes, stream deltas, search, conflict responses, not-found responses, peer-group comparisons and analyst context.

Two mechanisms, at different layers:

The query layer has no unscoped path. Typed methods require a non-empty tenant identity as their first argument. SQL uses parameters and always includes the tenant predicate. Every mutable table’s compound key begins with the tenant.

Cross-tenant tests seed identical resource identifiers into two tenants and assert that reads, cursors, writes, stream deltas, conflicts and not-found responses do not reveal the other one. Identical identifiers are the case that catches a missing predicate, which is why the tests are written that way.

An administrator of the installation does not automatically hold access to a tenant’s data. Memberships and roles are granted explicitly and checked on every operation.

This is the most important operational rule on this page.

A tenant’s configured networks describe what the interface shows. They do not filter what arrives. Ingest isolation happens at the source: which interfaces an exporter covers, which client networks a resolver log allows, which guest ports a mirror captures.

The appliance also enforces canonical, non-overlapping exclusion prefixes at startup for the flow path, and requires an explicit statement about whether IPv6 prefixes exist for those excluded networks — a missing or unknown state fails startup rather than defaulting to “probably fine”. The health check prints the asserted state so its absence stays operationally visible.

Secrets exist only in root-owned, mode-0600 files under /etc/ndr. Nowhere else: not in a YAML file, not in a database row, not in a log line, not in a process argument list.

  • Five separate source tokens, each shared only between the transport and the registry.
  • The service secret, shared only between the Go core and the dashboard layer.
  • OIDC, notification and connector credentials, each in their own file.

The installer fails closed if a partial set exists, if one is a symlink, if ownership is not root, or if the mode is anything other than 0600. Provisioning never passes a password in an argument list.

The connector framework strips every configured secret value from error texts, health lines and the entire unwrap chain, and drops a wrapped cause that carried one — because a foreign system’s error body can contain the credential that was just sent to it.

Listens on Reachable from
Go core API loopback only
Ingest listener loopback only
Database loopback only
Grafana loopback only, administered through an SSH tunnel
Dashboard port 3001 loopback by default; the configured proxy networks once you set them
Syslog and flow ports the specific firewall address each is registered for

The appliance’s own additive firewall table admits the dashboard port from the configured networks and nothing else. It is additive: it does not replace or interfere with a host firewall you already run.

Host names, user agents, DNS names and connector responses are attacker- influenceable. They are handled as data everywhere:

  • into the analyst request as data, never as instructions;
  • never into a SQL parser — search compiles a validated structured filter, and model text never reaches a query;
  • never as an identity assertion — a connector’s output is untrusted, and the core assigns tenant, sensor identity and trust class from configuration.

Analyst tools are read-only and allowlisted. Every analyst output stores its model, prompt version and evidence, so an implausible answer is traceable.

The free core has no write path to your firewall, your endpoints or any remote system. Response actions are records. Execution is a separate module with its own approvals, its own rollback and its own gates — and even there, a model never blocks on its own.