Skip to content

Proxmox VE

The hypervisor is the one system that knows, for every guest it runs, which virtual NIC it gave it and — with the guest agent — which addresses that NIC carries right now. The proxmox connector reads a Proxmox VE cluster (or a single node) through its REST API: the guest name becomes the device name (name_source: hypervisor, via hypervisor, directly after NetBox), the MAC↔IP binding of every virtual NIC reaches the identity model as anchors, and the device detail shows VMID, node, type, status, tags, pool and operating system with a link into the Proxmox interface.

It is a Free connector. Nothing is written to the cluster, ever: every call is a documented read, the token is granted the read-only PVEAuditor role and nothing else, and the test connection proves it can read — it never needs to write. It answers the question a homelab asks most — which VM is this address? — from the system that actually runs the VM, which a controller or a DHCP server can only guess at.

All calls are reads of /api2/json/... with an API token (Authorization: PVEAPIToken=<id>=<secret>):

Call Purpose
/version Reachability and authentication probe, for the test connection
/cluster/resources?type=vm Every guest: vmid, name, node, type (qemu or lxc), status, tags, pool, template
/nodes/{node}/{qemu|lxc}/{vmid}/config Per guest: only the netN lines, the agent switch and ostype are kept
/nodes/{node}/qemu/{vmid}/agent/network-get-interfaces and agent/get-osinfo A running VM whose configuration enables the guest agent: the live addresses and the OS name
/nodes/{node}/lxc/{vmid}/interfaces A running container: the live addresses of its network namespace

Disks, CPU, a cloud-init password hash and everything else in a guest configuration are never decoded or stored. A NIC line is parsed in both spellings — virtio=<mac>,bridge=vmbr0,tag=100 for a VM and name=eth0,hwaddr=<mac>,ip=<cidr>,ip6=<cidr>,bridge=,tag= for a container, where dhcp, auto and manual are not addresses.

An address counts only when its interface’s MAC is one of the guest’s configured NICs. A docker bridge or a nested container inside the guest has a MAC of its own that the hypervisor never handed out, and is ignored. Loopback, link-local, multicast and unspecified addresses never count. Templates are skipped; a stopped guest is inventoried without addresses.

The optional per-guest calls are tolerant by design. An agent that is not running or not installed (500), an endpoint an older node does not serve (404/501), a token without VM.GuestAgent.Audit on the agent path (403) and a guest that answers too slowly all mean no live addresses, not failure. A node pveproxy cannot reach (595/596 on the configuration) fails that guest only. A rejected token (401) or a missing VM.Audit on a configuration (403) fails the collection, because it is the same token for every guest.

Like NetBox, the cluster is a snapshot, not an event stream. An unchanged inventory produces an empty poll, which is still a successful sync; once a day the inventory is re-emitted in full so the identity anchors stay inside the identity job’s window.

1. Create a read-only token with privilege separation

Section titled “1. Create a read-only token with privilege separation”

On any cluster node, as root, create a dedicated user, a token that does not inherit the user’s rights, and one ACL that grants the built-in PVEAuditor role on / to the token itself:

Terminal window
pveum user add nyxtrace@pve --comment "Nyxtrace inventory (read-only)"
pveum user token add nyxtrace@pve inventory --privsep 1 --comment "Nyxtrace inventory"
pveum acl modify / --users nyxtrace@pve --roles PVEAuditor
pveum acl modify / --tokens 'nyxtrace@pve!inventory' --roles PVEAuditor

Both ACL lines are required. With --privsep 1 Proxmox VE computes a token’s effective permissions as the intersection of the user’s and the token’s permissions, so a token-only ACL yields an empty inventory (/cluster/resources returns no guests) even though authentication succeeds. Verified on PVE 9.2.

The second command prints the token secret exactly once; copy it into the credential file of step 2. With --privsep 1 the token can never exceed the user’s rights, which is why the user carries the same read-only role.

PVEAuditor on / is the minimal ACL: VM.Audit for the guests and their configurations, Sys.Audit for /version, VM.GuestAgent.Audit for the agent’s address list. On a release older than 8.2 that last privilege does not exist; the connector then tolerates the agent’s 403 and inventories the guest without live addresses. Do not grant PVEVMAdmin or Administrator.

2. Read the node certificate’s fingerprint

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

A stock Proxmox node serves a self-signed certificate, which the connector refuses without a pin. Read the fingerprint once, on the node named in url:

Terminal window
openssl x509 -in /etc/pve/local/pve-ssl.pem -noout -fingerprint -sha256

In a cluster every node has its own certificate — pin the one of the node you point url at. A node with an ACME certificate from a public CA needs no pin; the host trust store accepts it. Plain http is refused: the API listens on 8006 with TLS only.

The token secret goes into a file the service can read, in one of the two accepted shapes of UniFi, step 3 (0640 root:nyxtrace or 0600 nyxtrace):

Terminal window
install -o root -g nyxtrace -m 0640 /dev/null /etc/ndr/secrets/proxmox-token-secret
printf '%s\n' '<the secret printed by pveum>' >/etc/ndr/secrets/proxmox-token-secret
connectors:
- id: proxmox
instance: pve
tenant: example
enabled: true
config:
url: https://pve.example.invalid:8006 # any cluster node
token_id: nyxtrace@pve!inventory # user@realm!tokenname
tls_fingerprint_sha256: "<sha256 from step 2, colons optional>"
poll_interval: 5m # floor 1m
# nodes: pve-01, pve-02 # optional: only guests on these nodes
secrets:
token_secret: file:/etc/ndr/secrets/proxmox-token-secret

NDR_CONNECTORS_ENABLED=true in /etc/ndr/ndr.env is required, as for every file-declared connector — see connectors.yaml. Two clusters are two instances of the same connector, each with its own cursor, backoff and health line.

The poll interval defaults to five minutes and has a floor of one: every poll reads the guest list, every NIC configuration and every running guest’s agent, so a large cluster pays a few requests per guest each time.

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

test --live reports the Proxmox VE version, the guest count (qemu, lxc, running, templates), the node count and the one configuration it read — the version, the guest list and one configuration are exactly the rights the token needs, and the test fails closed on a missing one. collect --once --live prints how many addresses became items and how many anchors of which class (mac, mac_oui, authoritative_map) arrived, plus failed for guests on a node pveproxy could not reach. It stores nothing.

A failure names its class:

Class Meaning
auth Token rejected — check the id spelling user@realm!name and the secret file
forbidden No VM.Audit: the ACL of step 1 is missing or on the wrong path
tls Wrong pin, or a self-signed certificate without one
unreachable, timeout The node did not answer — 8 s per request, 5 min per collection
unexpected_shape Not the API: a reverse proxy in front of it, or the wrong port
too_large An answer over 4 MiB

Guests without addresses are not an error. A stopped guest, a VM without the guest agent (qemu-guest-agent not installed, or agent: 0 in its options) and a container on DHCP have no address the hypervisor can prove. Install and enable the agent in the VMs that matter, and the next poll picks the addresses up.

NDR_CONNECTORS_ENABLED=true in /etc/ndr/ndr.env and systemctl restart ndr-api. The system view then shows connector:pve with its state, the time of the last sync and the counters guests, running, ips, agent_ok, templates and nodes, which move on every poll, empty batches included. The instance turns stale after three missed polls (15 minutes at the default interval). Without the interface:

Terminal window
journalctl -u ndr-api -g 'instance=pve'

The hourly naming job logs hypervisor=N on its next run.

One item per (guest, address). Every address inside the tenant’s own configured VLANs gets an inventory record in the state store and its identity anchors in the anchor table under source = connector:proxmox: mac (or mac_random) plus mac_oui for the virtual NIC, and authoritative_map with the guest name reduced to its host label — the same class the UniFi controller map and NetBox use — each with the poll as its window. Addresses outside the configured networks are dropped and counted in the dropped= field of the poll’s log line.

A guest name that is not a usable label yields no name: one that contains a control character or an undecodable byte, or one longer than 200 bytes, produces neither an authoritative_map anchor nor a naming candidate (0.27). The mac and mac_oui anchors of the guest’s NICs are recorded as before — the hypervisor is trusted for what it proves about a NIC, not for whatever an operator typed into a name field.

The inventory record carries the guest name, its kind (qemu or lxc), status, the link to the guest in the Proxmox interface, the MAC, and the hypervisor fields: vmid, node, pool, tags, os (the agent’s pretty-name, empty without an agent), bridge and vlan. The payload is a reduced guest record, never the raw configuration.

Nothing is deleted when a guest disappears from the cluster. Its record does expire: one the connector has not re-emitted for three full snapshots (72 h) stops being shown and stops naming the device, and the name stays listed as a former name. The next emission that carries the address makes it live again.

Bounds: at most 2,000 guests per poll — more is marked truncated, never silently dropped — 8 s per request, 5 minutes per collection, 4 MiB per answer.

Device name. The naming job ranks the hypervisor third, directly after NetBox and above the UniFi controller map, DHCP, mDNS, the resolver, reverse DNS and the address. The guest name is a host label, not a typed name, so it becomes a DNS label (lower case, no spaces): Web-01 in Proxmox and a DHCP host name web-01 are one name. A name with a control character in it, or one over 200 bytes, is no label and names nothing. The interface labels such a name via hypervisor (name_source: hypervisor). The full order is on device identity and naming.

Device detail. A Proxmox block in the identity panel — virtual machine or container, the guest name, VMID, node, status, pool, tags, operating system and the connector instance it was read through — with a link Open in Proxmox VE when the connector has an https origin to point at.

API. GET /api/v1/devices/{id} returns a proxmox object — name, vmid, node, type, status, tags, pool, os, url, via, source — or null when no hypervisor knows any of the device’s addresses. See endpoints.

  • Only the guest agent (VMs) or the network namespace (containers) can prove a live address; a VM without qemu-guest-agent is inventoried by name, MAC, bridge and VLAN tag, with no address, and never gains one by guessing.
  • agent/get-osinfo costs one request per running VM with an agent on every poll; on a very large cluster, lengthen poll_interval or narrow nodes.
  • The device list (GET /api/v1/devices) does not carry the proxmox object and does not search by VMID or node; both are in the detail only.
  • The connector was shaped after the public Proxmox VE API documentation and its synthetic fixtures — a container’s interface list is read in both the hwaddr and the hardware-address spelling — and has not yet been exercised against a live cluster by the project itself. test --live is the first thing to run on yours.