Skip to content

UniFi Network

The UniFi Network connector is a read-only source against a UniFi OS controller. It exists because WiFi is where a hypervisor mirror is blind: the controller knows which client is on which access point, on which SSID, in which VLAN, on which switch port, and — most valuable of all — the name a human gave that client.

It is a Pro connector. See Free vs Pro.

Call Every Gives
Session login Once, renewed on a 401 A session
Active clients 60 s by default, floor 30 s Current clients with address, host name, access point, SSID, VLAN, switch port, signal
Known clients 900 s by default, floor 300 s Operator-assigned names, which change rarely

The two polls have different intervals on purpose: login plus timeouts cost real seconds on a normal controller, and the name list is nearly static. The session is held and renewed exactly once on a 401; after that, backoff takes over.

Client blocking and isolation are a separate response connector with their own approval path. This source cannot change anything on the controller.

1. Create a read-only account on the controller

Section titled “1. Create a read-only account on the controller”

Settings → Admins → Add Admin, Local Access Only, role View Only, restricted to the site the appliance should read. Do not use a UI Cloud account and do not reuse an existing admin: this credential ends up in a file on the appliance, so it has to be worth exactly the read access it grants.

Nyxtrace never needs to write, and a connector that cannot write cannot be turned into a way to write.

2. Read the controller’s certificate fingerprint

Section titled “2. Read the controller’s certificate fingerprint”

The connector pins the controller’s certificate by SHA-256 fingerprint. There is deliberately no option to disable verification — not a flag, not an “insecure” mode.

Read the fingerprint once, from the controller itself:

Terminal window
openssl s_client -connect controller.example.invalid:11443 </dev/null 2>/dev/null \
| openssl x509 -noout -fingerprint -sha256

Copy the hex digest — with or without colons — into tls_fingerprint_sha256.

The installer creates /etc/ndr/secrets as 0750 root:nyxtrace, empty. A credential file has exactly two accepted shapes, because the API does not run as root:

Terminal window
umask 077
printf '%s' '<controller username>' > /etc/ndr/secrets/unifi-username
printf '%s' '<controller password>' > /etc/ndr/secrets/unifi-password
chgrp nyxtrace /etc/ndr/secrets/unifi-username /etc/ndr/secrets/unifi-password
chmod 0640 /etc/ndr/secrets/unifi-username /etc/ndr/secrets/unifi-password

That is the group model: mode 0640, owner root, group the service group — and the group of /etc/ndr/secrets itself is the authority, so the directory is what a credential’s group is checked against. The alternative is the owner-only model, mode 0600 owned by the service user.

Everything else is refused, each with the file name and the fix in the message: readable by another account, group-writable, executable, a world-traversable directory, a group the directory does not name, a group-readable file whose group is root, and a root-owned 0600 file while the directory names a non-root service group — that last one is the shape that looks correctly locked down and is unreadable at the next restart. A trailing newline is trimmed, so a printf without \n and an echo both work.

Copy infra/appliance/examples/connectors.example.yaml to /etc/ndr/connectors.yaml (0640 root:root) and keep one unifi-network entry. Leave enabled: true — the framework flag in step 6 is the real switch.

connectors:
- id: unifi-network
instance: unifi-home
tenant: example
enabled: true
config:
url: https://controller.example.invalid:11443
site: default
tls_fingerprint_sha256: "0000000000000000000000000000000000000000000000000000000000000000"
poll_interval: 60s
user_poll_interval: 900s
secrets:
username: file:/etc/ndr/secrets/unifi-username
password: file:/etc/ndr/secrets/unifi-password

poll_interval has an enforced floor of 30 s; site is default unless the controller says otherwise.

Validate as the service user: a root-run validation proves the file modes but not that nyxtrace can read them.

Terminal window
sudo -u nyxtrace env NDR_CONNECTORS_ENABLED=true ndr connectors validate
sudo -u nyxtrace env NDR_CONNECTORS_ENABLED=true ndr connectors test unifi --live
sudo -u nyxtrace env NDR_CONNECTORS_ENABLED=true ndr connectors collect unifi --once --live

test --live proves reachability, authentication, authorisation and the response shape, and writes nothing. collect --once --live pulls exactly one batch and prints how many anchors of which class arrived; it stores nothing, so running it never makes the service skip data. Anchor values are withheld unless --verbose is given, because they are MAC addresses and client names.

A failure names its class: auth (wrong account), tls (wrong or missing pin), unreachable (wrong host or port), forbidden (the account lacks the read right), rate_limited, timeout.

Terminal window
printf 'NDR_CONNECTORS_ENABLED=true\n' >> /etc/ndr/ndr.env
systemctl restart ndr-api

The restart runs ndr migrate as ExecStartPre, and that is required: the anchor insert writes provenance columns a newer migration added, so a binary that runs before the migration fails the insert — and that insert is shared with the identity job. If the ndr identity timer could fire between deploying the binary and restarting ndr-api, run ndr migrate by hand first.

Expect /api/v1/health to read degraded for up to one poll interval after the restart: a declared instance is unavailable until its first successful collection, the same rule the sensors follow.

The system view shows one component row per declared instance, connector:<id>, with its state and lag. The same lines are visible without the interface:

Terminal window
sudo -u nyxtrace env NDR_CONNECTORS_ENABLED=true ndr connectors list
journalctl -u ndr-api -g 'operation=connector'

The service then polls on the configured interval, persists the cursor and the health line in state.sqlite3 (connector_cursors, connector_health), and writes identity anchors with source = connector:unifi-network. The hourly ndr identity --tenant <id> job reads them like every other source.

To take the connector out again, set enabled: false on the instance and restart: a switched-off instance keeps its cursor, deletes no data, and stays visible with its reason.

Not everything the controller says is an identity claim. The connector maps its fields deliberately:

Controller field Becomes Weight
MAC address A mac anchor, or mac_random if the locally administered bit is set Establishes
MAC vendor prefix A mac_oui anchor Confirms only
Client host name A dhcp_hostname anchor Establishes
Operator-assigned name An authoritative_map anchor Establishes
RADIUS identity A dot1x anchor Establishes
Access point, SSID, switch, port, VLAN, vendor string, signal, wired flag Facts, not anchors

The operator-assigned name is the one that matters most: it is what binds a rotating MAC address to the device you already know. See MAC rotation.

A randomised address is recognised from the locally administered bit and marked as such, rather than being treated as a stable identity.

The instance reports unreachable and its facts go stale, never deleted. No identity merge and no identity split is decided on stale controller data alone.

That is the general rule for connector data: a connector is a witness, not an authority. Its last statement stays on the record with its age attached.