Model packs
A model pack is a versioned set of detection models —
YAML files exactly like the shipped model file — published on the key server,
signed with the release key and pulled by a Pro installation whose licence
carries model_packs. The analyzer loads the packs over the built-in
model set. Without a licence, or without the feature, the built-in set runs
alone and nothing on this page applies; what a free installation gets is on
Pro content.
Packs are additive by construction. A pack can add models, replace a built-in
model of the same id and carry roles for its own
models. It can never weaken a shipped policy, never set the aggregation
section, and never carry code: every file in a pack goes through the
analyzer’s own validator — the one that reads models/network-core.yaml —
before anything is installed, and a detector or a parameter the running
binary does not know refuses the whole pack.
The commands
Section titled “The commands”ndr models pull [--channel stable|beta] [--id network-core] # as nyxtracendr models status # the set the analyzer runsndr models validate DIR # the publisher's check; no configuration, no connectionndr models pull reads the cached entitlement out of the state store, asks the
key server for the newest manifest of the pack in the channel
(NDR_CONTENT_CHANNEL, or the update channel when that is empty), verifies
the manifest against /etc/ndr/release-pubkeys.json (NDR_UPDATE_KEYRING,
the same file the updater
reads — a pack is signed with the release key), refuses a pack whose
min_appliance_version is newer than the running version, downloads the
tar.gz through the key server’s redirect, checks the signed size and SHA-256,
extracts plain *.yaml files only (bounded in count and size, no paths), runs
every model through the validator, loads the whole staging directory once
more to catch duplicate ids or role names across files, compares the model
count with the signed entries, and only then renames the version into place
and writes current.json — last, atomically. Its three outcomes:
installed: network-core 1.2.0 (9 models, signed by nyx-release-2026a)the analyzer loads it at its next pass; ndr models status shows the setunchanged: network-core 1.2.0 is installednone: the channel holds no network-core pack yetExit code 3 is not entitled — the free core, licensing switched off, a
licence without model_packs — and is printed with the reason (the licence
does not include model_packs). No connection is opened in that case. Any
other failure keeps the installed version; the analyzer keeps running it.
The daily unit does the same thing on a timer:
systemctl enable --now ndr-model-packs.timersystemctl status ndr-model-packs.service ndr-model-packs.timerndr-model-packs.service runs as the service user — a pack is data the
analyzer validates, so no root step is needed — at 03:41 with a random delay
of up to four hours, persistent across a missed slot, and treats exit code 3
as success: on a free installation the timer runs and does nothing, which is
correct. The installer ships the two units but enables neither.
The packs directory and current.json
Section titled “The packs directory and current.json”NDR_MODEL_PACK_DIR (/var/lib/ndr/model-packs, owned by the service user)
holds one directory per pack id and, under it, one per installed version:
/var/lib/ndr/model-packs/└── network-core/ ├── current.json # the version in use ├── 1.1.0/ # kept for a rollback by hand │ └── *.yaml └── 1.2.0/ └── *.yamlcurrent.json is the pointer: version is the directory the analyzer loads,
and the rest is what the pull recorded — id, channel, key_id, sha256,
models, notes, pulled_at and, after a pull that changed nothing for a
reason, error with that reason. The previous version stays on disk for a
rollback by hand; older ones are pruned. A pull that ends in unchanged —
same version and checksum as the pointer — writes nothing.
ndr models status prints the set exactly as the analyzer composes it:
built-in: /opt/nyxtrace/models/network-core.yamlpack directory: /var/lib/ndr/model-packspack: network-core 1.2.0 (9 models, signed by nyx-release-2026a, pulled 2026-09-19T03:52:10Z)models in use: 9 (rare_destination_v1, periodic_destination_v1, …)or packs: none (the built-in model runs alone). A notice: line names a
pack, a model id and the reason whenever the composition skipped something —
the same line the analyzer writes to its log as
operation=models.load pack=… model=… note="…".
Precedence
Section titled “Precedence”The rule lives in one place in the code and is short enough to state whole:
- The built-in model file is loaded first.
- A pack model with the same
idas a built-in model replaces it in place; the position in the file, and therefore the order of evaluation, is the built-in one. - Packs apply in id order. On a collision between two packs the first pack’s model stays and the later one is skipped with a notice.
- A pack whose files no longer load — the binary was downgraded, a file was edited by hand — contributes nothing and a notice. A pack never stops the built-in model.
Since roles and the daily cap exist, the composition has three more lines:
- A pack may carry a
rolessection for the models it defines; it is validated against the pack’s own models, and a role name may not repeat across the pack’s files. The built-in roles come first in file order and a pack’s roles are appended in pack order. Because the first matching role wins, a device that matches a shipped role (infrastructure) keeps the shipped policies — content can add roles for its own models but never weaken a shipped policy. A pack role that reuses a shipped name is skipped with a notice. aggregationis the built-in file’s alone. A pack that sets one gets a notice and no effect.- After a pack is applied the merged file is validated again. A pack that
would leave a shipped policy invalid — say it replaces
periodic_destination_v1with a model of another detector while the shippedinfrastructurerole saysallow_known_destinationsfor it — contributes nothing, with the reason in the notice and on the health row. It stays listed as installed. A policy that should govern shipped models is a change to the shipped file, not to a pack.
When a pull lands matters too. A long-running catch-up loop
(ndr analyze --loop, see analysis runs)
compares every current.json before each pass and reloads the set — models,
roles and the daily cap together — without a restart, keeping the previous set
when a reload fails. The timer-driven analyzer reads the packs at its next
run. ndr analysis replay evaluates “current” as the
configured file with the installed packs applied — what actually runs —
and a --model candidate alone; ndr analysis forecast --model FILE joins
the candidate alone too.
The models health row
Section titled “The models health row”GET /api/v1/health and the system view carry a
models component (kind: analysis) since 0.39:
{ "id": "models", "kind": "analysis", "status": "ok", "last_event_at": "2026-09-19T03:52:10.000Z", "lag_seconds": null, "message": null, "built_in": 8, "packs": [ { "id": "network-core", "version": "1.2.0", "channel": "stable", "signing_key_id": "nyx-release-2026a", "models": 9, "pulled_at": "2026-09-19T03:52:10.000Z", "loaded": true } ] }| Status | Meaning |
|---|---|
unavailable |
no pack is installed — the built-in model runs alone, the free core. Not a fault |
ok |
every installed pack loads |
stale |
the last pull failed while the installed version runs |
degraded |
an installed pack does not load, or the built-in catalogue is broken; the admin-only message says which |
built_in is the size of the built-in catalogue and packs is always a
list. The row never changes the overall status. In /metrics it is one more
health_component row with kind="analysis"; there is no separate family.
Rolling back
Section titled “Rolling back”ndr models has no --rollback. The previous version is still on disk, so
the rollback is a pointer edit as the service user:
systemctl stop ndr-model-packs.timer # or the next pull re-installs the newer onecd /var/lib/ndr/model-packs/network-coresudo -u nyxtrace sed -i 's/"version":"1.2.0"/"version":"1.1.0"/' current.jsonndr models status # pack: network-core 1.1.0 …Only version matters for the loader; the other fields keep describing the
pull that wrote them, and the next successful pull rewrites the whole file.
Removing the pack directory altogether — rm -r /var/lib/ndr/model-packs/network-core — makes the analyzer run the built-in
model at its next pass; the models row goes back to unavailable. Neither
step restarts anything: the loop picks the change up before its next pass,
the timer at its next run. To stop pulling for good, disable the timer; to
force the built-in model on a Pro installation, remove model_packs from the
licence in the portal — the next pull ends not entitled and the installed
packs stay until you remove them.
The minimum appliance version
Section titled “The minimum appliance version”A pack manifest carries min_appliance_version: the oldest appliance that
understands its parameters. A pull that meets a newer requirement than the
running version refuses the pack and keeps the installed version; the
models row turns stale with the reason for admins. Update the appliance,
and the next pull takes the pack. The rule is the same as the one the
0.38 model file states for a downgrade: a model file
must not be handed to a binary that does not know its parameters, and the
validator is what makes sure it is not.
Known limits
Section titled “Known limits”- Two versions are kept per pack; a rollback further back needs the publisher.
- One pack id is published today (
network-core); the--idflag exists for the day there is a second. - The pull is not exercised against the live key server by the project’s own tests — they run against an in-test key server with real signatures, the wrong ring, the wrong artefact type, a truncated body, an expired manifest and the validator. The first live pull on the reference installation is part of the 0.39 rollout.