Skip to content

connectors.yaml

Connector instances live in one file, read by the same strict YAML loader as the sensor registry and the site configuration. An unknown field, a duplicate instance name or a plaintext secret is a start error, not a warning.

Terminal window
# in the service environment file, mode 0600, root-owned
NDR_CONNECTORS_ENABLED=true
NDR_CONNECTORS_PATH=/etc/ndr/connectors.yaml # this is the default

With the flag unset, the file is not read at all. That is the default, and an existing installation keeps its behaviour until an operator switches it on.

connectors:
- id: unifi-network
instance: unifi-home # several instances of one connector are allowed
tenant: example
enabled: true
config:
url: https://controller.example.invalid:11443
site: default
tls_fingerprint_sha256: "<read from the controller itself>"
poll_interval: 60s
secrets:
username: file:/etc/ndr/unifi-username
password: file:/etc/ndr/unifi-password
Field Meaning
id Which connector. Must be one the core knows.
instance Your name for this instance. Must be unique — a duplicate is a start error.
tenant Which tenant the data belongs to. The connector cannot influence this.
enabled false makes the instance inert. It keeps its cursor and deletes nothing.
config Validated against the connector’s declared schema, field by field, with bounds.
secrets References only. Never values.

Two forms, and nothing else:

secrets:
password: file:/etc/ndr/some-secret # a regular file under /etc/ndr
token: env:NDR_SOME_TOKEN # from the mode-0600 service environment

A file: reference must be a regular file under /etc/ndr, non-empty, and unreadable by group and others. A symlink, a directory, a wrong mode or a world-readable file fails the start.

A literal secret in connectors.yaml is refused outright. The interface never displays a secret back, not even masked — it can only set one.

Two firewalls, two DNS filters, two controllers: allowed, and each runs independently with its own cursor, its own backoff and its own health line.

connectors:
- id: unifi-network
instance: unifi-home
tenant: example
enabled: true
config: { url: https://controller.example.invalid:11443, site: default, poll_interval: 60s }
secrets: { username: file:/etc/ndr/unifi-username, password: file:/etc/ndr/unifi-password }
- id: unifi-network
instance: unifi-lab
tenant: example
enabled: false
config: { url: https://lab.example.invalid:11443, site: lab, poll_interval: 300s }
secrets: { username: env:NDR_UNIFI_LAB_USERNAME, password: env:NDR_UNIFI_LAB_PASSWORD }

The NetBox connector takes a URL, a poll interval and a page size, and one secret, the API token:

connectors:
- id: netbox
instance: netbox
tenant: example
enabled: true
config:
url: https://netbox.example.invalid
poll_interval: 15m # floor 1m
page_size: 200 # 10..1000
secrets:
token: file:/etc/ndr/secrets/netbox-token # or env:NDR_NETBOX_TOKEN

This entry is optional. An appliance that already has NDR_NETBOX_URL and NDR_NETBOX_TOKEN in its service environment gets the same connector declared from those two variables — without this file and without NDR_CONNECTORS_ENABLED, because the flag gates the file and not a connector the environment configured explicitly. Declare it here to choose the interval, the page size or a token file; a declared netbox entry wins over the environment.

The Pi-hole and AdGuard Home connectors are Free. Pi-hole takes one secret, the app password (v6) or the API token (v5); AdGuard Home takes a username and a password for a dedicated user. tls_fingerprint_sha256 is only for an https URL with a self-signed certificate.

connectors:
- id: pihole
instance: pihole
tenant: example
enabled: true
config:
url: http://pi.hole
api_version: auto # auto | v6 | v5
poll_interval: 60s # floor 30s
page_size: 500 # 50..5000, twenty requests per poll at most
secrets:
password: file:/etc/ndr/secrets/pihole-password
- id: adguard
instance: adguard
tenant: example
enabled: true
config:
url: http://adguard.example.invalid:3000
poll_interval: 60s # floor 30s
page_size: 500 # 10..500, twenty requests per poll at most
secrets:
username: file:/etc/ndr/secrets/adguard-username
password: file:/etc/ndr/secrets/adguard-password

The DHCP leases connector is Free and has three inputs chosen by the required mode. The file modes take a path and no secrets — --live means reading the file as the service user — and the Kea Control Agent mode takes a url with optional basic auth and client certificate:

connectors:
- id: dhcp-leases
instance: dhcp
tenant: example
enabled: true
config:
mode: kea-memfile # kea-memfile | isc-leases | kea-api
path: /srv/dhcp/kea/kea-leases4.csv
# path6: /srv/dhcp/kea/kea-leases6.csv
poll_interval: 60s # floor 30s; kea-api defaults to 120s
- id: dhcp-leases
instance: dhcp-api
tenant: example
enabled: false
config:
mode: kea-api
url: https://dhcp.example.invalid:8000
tls_fingerprint_sha256: "<sha256 of the leaf>"
ipv6: false
page_size: 100 # 10..1000
secrets:
username: file:/etc/ndr/secrets/kea-username
password: file:/etc/ndr/secrets/kea-password

The Proxmox VE connector is Free and takes an https URL of any cluster node, the token id in the form user@realm!tokenname, the node certificate’s fingerprint (a stock node is self-signed; a public-CA certificate needs no pin) and one secret, the token secret pveum printed once. nodes optionally restricts the walk to named nodes:

connectors:
- id: proxmox
instance: pve
tenant: example
enabled: true
config:
url: https://pve.example.invalid:8006
token_id: nyxtrace@pve!inventory
tls_fingerprint_sha256: "<sha256 of /etc/pve/local/pve-ssl.pem on that node>"
poll_interval: 5m # floor 1m; every poll reads every NIC configuration and every running agent
# nodes: pve-01, pve-02
secrets:
token_secret: file:/etc/ndr/secrets/proxmox-token-secret

Two clusters are two entries with different instance names; each runs with its own cursor, backoff and health line.

The webhook connector is Free and is a sink: it sends rather than reads. It takes the receiver’s URL, an optional subscription and severity floor, and one secret, the HMAC key the receiver shares. A self-signed receiver is pinned like every other endpoint; plain http has to be asked for:

connectors:
- id: webhook
instance: hook
tenant: example
enabled: true
config:
url: https://hooks.example.invalid/nyxtrace
# tls_fingerprint_sha256: "<sha256 of the receiver's leaf, self-signed only>"
# allow_insecure_http: true
# events: incident.created, incident.escalated, incident.closed, health.transition
min_severity: medium # incident events only; health transitions always pass
timeout: 5s # per request, 1s..30s
max_pending: 1000 # 10..10000; beyond it the oldest are dropped and counted
# public_url: https://nyxtrace.example.invalid # defaults to NDR_PUBLIC_URL
secrets:
secret: file:/etc/ndr/secrets/webhook-secret

Two receivers are two entries with different instance names, each with its own bookmark, backoff and counters. The links in every event need NDR_PUBLIC_URL in the service environment or public_url on the instance.

The four chat sinks — telegram, discord, slack, matrix — are Pro and take the webhook’s subscription fields (events, min_severity, timeout, retry_delay, max_pending, public_url) plus the platform’s address and one credential each, always as a secret file: Telegram a bot token, Discord a webhook URL (it carries the token), Slack a webhook URL or a bot token depending on mode, Matrix an access token. https only; tls_fingerprint_sha256 is for a self-hosted homeserver, a self-hosted Bot API server or a proxy in front of the platform.

connectors:
- id: telegram
instance: telegram
tenant: example
enabled: true
config:
chat_id: "-1001234567890" # numeric, or a public @channelusername
# message_thread_id: 42 # a topic of a forum group
# silent: true # no notification sound
min_severity: medium
secrets:
bot_token: file:/etc/ndr/secrets/telegram-bot-token
- id: discord
instance: discord
tenant: example
enabled: true
config:
username: Nyxtrace # optional display name
min_severity: medium
secrets:
webhook_url: file:/etc/ndr/secrets/discord-webhook-url
- id: slack
instance: slack
tenant: example
enabled: true
config:
mode: webhook # webhook | api
# channel: C0123456789 # api mode only
min_severity: medium
secrets:
webhook_url: file:/etc/ndr/secrets/slack-webhook-url
# bot_token: file:/etc/ndr/secrets/slack-bot-token # api mode instead
- id: matrix
instance: matrix
tenant: example
enabled: true
config:
homeserver_url: https://matrix.example.invalid
room_id: "!abcdefghijklmnop:matrix.example.invalid" # the id, never an alias
# tls_fingerprint_sha256: "<sha256 of the homeserver's leaf, self-signed only>"
msgtype: m.notice # m.notice | m.text
min_severity: medium
secrets:
access_token: file:/etc/ndr/secrets/matrix-access-token

Two channels or two rooms are two entries with different instance names. ndr connectors test <instance> --live posts a real test message into the chat; an instance without the licence feature is unlicensed and posts nothing.

Terminal window
ndr connectors validate

This checks the file exactly the way startup would — same loader, same schema, same secret rules — and opens no network connection. Run it before restarting the service, every time. See the CLI.

Every poll interval has an enforced minimum, declared by the connector. Setting a value below the floor is a configuration error, not a value that gets quietly raised. The floors exist because polling costs the foreign system work, and a misconfigured interval is how a monitoring tool becomes the incident.