Device Agent Deployment Guide¶
Overview¶
The device agent is a downloadable binary that customers deploy in their on-premises or air-gapped environments to perform device interrogations. The agent communicates outbound-only with the platform service, receiving jobs and submitting results.
Architecture¶
The device agent operates in a hybrid deployment model:
- Platform Internal Agent: Runs as a background worker in the device-interrogation-service, handling cloud provider APIs and internet-accessible devices
- Downloadable Binary: Customer-deployed agent for on-premises/air-gapped environments
Prerequisites¶
- Network access to the platform API gateway (outbound HTTPS)
- Registration key from the platform
- Appropriate credentials for target devices (configured in platform)
Minimum supported Windows version¶
Windows Server 2019 or later (or Windows 10 1809 and later) — for the host the agent runs on, and for any Windows host it collects from in remote mode. Remote host-inventory collection on Windows uses PowerShell over SSH, which needs the OpenSSH Server feature; that feature is available from these versions. WinRM is not supported (see "Exactly what runs, per OS" below).
The OS/arch list for the binaries themselves is in Downloads in INSTALL.md.
Installation¶
Getting the binary¶
Download the device-agent-<os>-<arch>-<version> asset matching your
platform from the
latest GitHub release
and verify it against that release's signed SHA256SUMS — see
Downloads in INSTALL.md
for the full OS/arch table and the cosign verify-blob command. There is
no in-product download endpoint — the platform does not serve agent
binaries over HTTP, so a curl/Invoke-WebRequest against the platform
itself will not work (see "There is no binary-download API" in the
sensor registration guide, which
applies to the device agent too).
Prefer to build it yourself? make build-device-agent (current platform) or
make device-agent-all-platforms (all six targets) builds the same binary
from this repository.
The sections below assume you've downloaded the asset for your platform and
renamed it to device-agent (device-agent.exe on Windows) for brevity —
substitute the versioned filename if you'd rather keep it as-is.
Linux (recommended): the installer¶
The registration screen (Discovery → Sensors & Agents → Register → Device agent) shows these commands with your platform URL, key and release filled in:
# from the release matching your platform's version
curl -fLo device-agent https://github.com/VistaSecurity/VistaPlatform-Core/releases/download/<version>/device-agent-linux-amd64-<version>
curl -fLO https://raw.githubusercontent.com/VistaSecurity/VistaPlatform-Core/<version>/scripts/install-device-agent.sh
chmod +x device-agent
sudo bash install-device-agent.sh --url https://platform.example.com --key YOUR_REGISTRATION_KEY
The installer is the device agent's counterpart of the sensor installer. It
installs the agent to /opt/crypto-device-agent as the crypto-device-agent
systemd service, running as its own user. It keeps the agent's certificate and
the platform's CA under the install directory, starts the service, and waits
until the agent has enrolled, then prints its agent ID and version. If the
platform rejects the key (expired, invalid or already used), it stops the
service and says so. Re-running the same command on that host keeps the agent's
enrollment and replaces only the binary, which is how to upgrade it. For a
platform whose certificate this host does not already trust, add
--ca-fingerprint <sha256> (see
Platforms with a privately-signed certificate).
A --key on the command line is visible to other users in the process list.
To keep the key out of it, set VISTA_REGISTRATION_KEY in the environment
(sudo VISTA_REGISTRATION_KEY=... bash install-device-agent.sh --url ..., which
passes the variable through sudo directly) or pipe it in with --key-stdin. The
installer never prints the key in full, and writes every value it puts in the
config file as a quoted YAML string.
Exit status 78. A start that only a person can fix, namely a registration
key the platform rejected, a config file that cannot be parsed, or a data
directory the agent cannot write, makes the agent exit with status 78
(EX_CONFIG). The installed systemd unit sets RestartPreventExitStatus=78, so
the service stops and stays stopped instead of retrying every 10 seconds; check
journalctl -u crypto-device-agent, fix the cause (usually: generate a new key
and re-run the installer), then systemctl start crypto-device-agent. Any other
failure, such as the platform being unreachable, still restarts and retries.
Windows services and macOS launchd have no equivalent exit-status exclusion, so
a rejected key under those supervisors still exits 78 but may be restarted by
the supervisor.
bash install-device-agent.sh --check-dependencies checks the binary runs on
this host without installing anything. Use -arm64 in the download for ARM
hosts.
Linux (AMD64): running the binary yourself¶
# after downloading device-agent-linux-amd64-<version> from the release page
mv device-agent-linux-amd64-<version> device-agent
chmod +x device-agent
# Run it — with no arguments the agent walks you through setup and then starts
./device-agent
Data path. The agent's default data path on Linux is
/var/lib/crypto-device-agent, which only root can create. Run it as root, or choose a data path your user can write. The agent checks it can save its enrollment before it sends the registration key, and refuses to start with "cannot store its enrollment" if it can't. The key is then still unused.
Running the binary with no arguments opens the interactive installer: it asks
for the platform URL (and tests reachability), the registration key, the data
path and the poll interval, enrolls the agent, writes
<data_path>/agent-config.yaml, and then starts polling for jobs
immediately. Verbose logging is on by default. On every later start the agent
finds that config file and runs straight away — no flags, no second command.
To configure a host without answering prompts, write the config file yourself and the installer stays out of the way:
cat > device-agent.yaml <<EOF
platform_url: https://platform.example.com
registration_key: YOUR_REGISTRATION_KEY
poll_interval: 30s
verbose: true
EOF
./device-agent --config device-agent.yaml
A config file that exists but cannot be read or parsed (a typo such as a line
missing its :) is fatal: the agent logs the file's path and the parse error and
exits non-zero, rather than starting against built-in defaults with no identity. The
environment-variable fallback applies only when no config file exists.
Linux (ARM64)¶
# after downloading device-agent-linux-arm64-<version> from the release page
mv device-agent-linux-arm64-<version> device-agent
chmod +x device-agent
# Follow same configuration steps as AMD64
Windows¶
# after downloading device-agent-windows-amd64-<version>.exe from the release page
Rename-Item device-agent-windows-amd64-<version>.exe device-agent.exe
# Run it — with no arguments the agent walks you through setup and then starts
.\device-agent.exe
As on Linux, a config file suppresses the installer:
@"
platform_url: https://platform.example.com
registration_key: YOUR_REGISTRATION_KEY
poll_interval: 30s
verbose: true
"@ | Out-File -FilePath device-agent.yaml -Encoding utf8
.\device-agent.exe --config device-agent.yaml
macOS¶
# after downloading device-agent-darwin-amd64-<version> (Intel) or
# device-agent-darwin-arm64-<version> (Apple Silicon) from the release page
mv device-agent-darwin-*-<version> device-agent
chmod +x device-agent
# Follow same configuration steps as Linux
Configuration¶
Environment Variables¶
The agent can be configured via environment variables or a YAML file:
PLATFORM_URL: Platform API gateway URL (default: http://localhost:8080)PLATFORM_URL_OVERRIDE: set to1to forcePLATFORM_URLon an already-enrolled agent (see below)REGISTRATION_KEY: Agent registration key (required)AGENT_ID: Agent ID (set after first registration)POLL_INTERVAL: Job polling interval (default: 30s)VERBOSE: Enable verbose logging. Verbose is on by default; setVERBOSE=false(orverbose: falsein the config file, or-verbose=falseon the command line) to quiet a long-running install.HOST_INVENTORY_ENABLED: set totrueto have the agent inventory its own host on a schedule (default:false). See Host inventory.HOST_INVENTORY_INTERVAL: how often it does so (default:24h, minimum1h— a shorter value is raised to the minimum and logged).HOST_INVENTORY_CONNECTIONS_ENABLED: explicit opt-in for collecting a bounded remote-peer list (default:false). Destinations can reveal application and browsing activity, so host inventory alone does not enable it.
These are a starting position, not the source of truth. Once the agent is enrolled,
POLL_INTERVAL,HEARTBEAT_INTERVAL,VERBOSEand the inventory schedule/interval settings are managed from the console. The connection opt-in remains local to the agent, alongside the credentials and endpoint. The managed settings are shown under Discovery → Sensors & Agents → the agent → Settings, and a later local edit to them is overwritten at the next check-in. See Agent and sensor settings.
Configuration File¶
Create a device-agent.yaml file:
platform_url: https://platform.example.com
registration_key: your-registration-key-here
poll_interval: 30s
verbose: true
# Optional: inventory this host as well as interrogating network devices.
host_inventory_enabled: true
host_inventory_interval: 24h
Registration¶
Registration key type (required)¶
Create a pending registration from the tenant web UI (Operations / Sensors, or the enhanced registration flow) and select Device interrogation agent so the profile is device_interrogation. Keys issued for network sensors are rejected by POST /api/v1/device-interrogation-service/agents/register.
Gateway-first¶
Enrolled agents pin their own endpoint. When agent mTLS is enabled, registration saves the advertised TLS-passthrough URL into the agent's config, and that saved value wins over
PLATFORM_URLon subsequent starts. This is deliberate: the agent's client certificate is bound to that endpoint, and letting a stale bootstrapPLATFORM_URLwin would send post-registration traffic back through the edge proxy, where TLS terminates, the client cert is lost, and the agent gets 401s.The agent logs a line when it ignores
PLATFORM_URLfor this reason. To repoint an enrolled agent deliberately, setPLATFORM_URL_OVERRIDE=1alongside the newPLATFORM_URL— expect to re-enroll, since the existing certificate will not be valid for the new endpoint. The sensor has the equivalent pair (CONTROL_PLANE_URL/CONTROL_PLANE_URL_OVERRIDE).
Set platform_url (or PLATFORM_URL) to the API gateway base URL (for example https://platform.example.com or http://localhost:8080). Do not point the agent at individual backend service ports.
First-time enrollment¶
- Option A — just run the binary (default):
device-agentwith no arguments prompts for the registration key, enrolls, saves the config, and starts the agent in one step. Nothing to prepare beforehand. - Or, to prepare the config yourself, add
registration_keyto your YAML file or environment and pick one of the following. - Option B — explicit register flag:
device-agent -register -config device-agent.yaml
Then run the agent normally:device-agent -config device-agent.yaml. - Option C — auto-enroll on start (sensor-style):
Ifregistration_keyis set andagent_idis empty, the agent registers once on startup, saves certificates under<data_path>/certs, and updates the config file withagent_id. If you did not pass-config, it writes<data_path>/agent-config.yamlby default. - After a successful enrollment, the server marks the key as used. Keep
agent_idand the saved certificate paths for subsequent runs.
What the agent keeps after enrollment¶
The registration key is single-use and is not kept once enrollment has succeeded. At enrollment the platform also issues the agent a random agent secret, once, alongside its client certificate. That secret is what protects the device credentials the platform sends with each job (F5, Cisco, Fortinet, Palo Alto and UniFi administrator passwords), so it replaces the registration key as the thing worth stealing.
| What | Where | Notes |
|---|---|---|
| Client private key, certificate, platform CA | <data_path>/certs/client.key (0600), client.crt, ca.crt |
Written as one unit. |
| Agent secret | <data_path>/certs/agent-secret.json (0600) |
Written atomically with the certificate, so a rotation replaces both or neither. The platform stores only its encrypted form and cannot show it again. |
| Registration key | Nowhere. The config file holds registration_key_sha256 (a fingerprint) in its place. |
The fingerprint lets install-device-agent.sh recognise "this host already enrolled with this key" on a re-run. The key itself is removed from the config only after the agent secret is safely on disk; if the platform issued no secret, or the save failed, the key stays where it was. |
Setting REGISTRATION_KEY in the service environment after enrollment has no
effect on credential decryption; an enrolled agent never derives its key from it.
Certificate rotation also rotates the agent secret¶
Every certificate rotation (an agent rotates when its certificate is within 30
days of expiry) issues the agent a new agent secret, saved with the new
certificate. The secret it replaces stays valid on the agent for 24 hours,
so a job the platform sealed just before the rotation still decrypts; after that
the old secret is discarded from memory and from agent-secret.json. The
platform seals every job with the current secret only.
Agents enrolled by an earlier release¶
An agent enrolled before agent secrets existed keeps working with no re-enrollment. It keeps decrypting with its registration key until its next certificate rotation, when it receives its first agent secret, switches to it, and removes the registration key from its config. The registration key keeps decrypting jobs sealed in the 24 hours around that switch, then is discarded too. An agent binary older than this feature never asks for an agent secret, so the platform never switches it away from its registration key.
Job polling, results, and heartbeat use agent mTLS (client certificate) when certificates are present; they do not use tenant user JWT cookies.
Platforms with a privately-signed certificate¶
If the platform's edge certificate comes from an internal CA this host does not trust, enrollment fails certificate verification before it can start — registration is itself an HTTPS call. The agent resolves this with an explicit one-time trust decision rather than by skipping verification.
- Interactive: the default install path — plain
device-agent, ordevice-agent --interactiveto force the dialogue on a host that already has a config file — shows the CA the platform presents, with its SHA-256 fingerprint, and asks whether to trust it. Accepting pins it to the agent's config; every later connection is verified against it. - Unattended: pass
--ca-fingerprint <sha256>. The agent pins the CA only if it hashes to that value and aborts on a mismatch. With neither a fingerprint nor an operator to ask, it refuses rather than pinning an unapproved CA. - Alternative: install the CA into the host's system trust store, after which no flag is needed.
The expected fingerprint is shown in the web UI when you mint the registration code (Discovery → Sensors & Agents → Register, on the confirmation screen), so you can compare it against what the agent displays. Read it there rather than trusting the connection you are trying to validate. If the platform cannot inspect its own certificate, get it from the host directly:
openssl s_client -showcerts -connect <platform-host>:443 </dev/null 2>/dev/null | openssl x509 -outform PEM | openssl x509 -noout -fingerprint -sha256
Before offering you a CA to approve, the agent checks that the certificate the
platform presents is valid for the hostname you gave it. If it is not, the agent
refuses and names what the server is actually serving — commonly an ingress
controller's placeholder such as a1b2c3d4.traefik.default, which means the
platform's TLS certificate was never installed. Trusting a CA cannot fix this:
verification checks the hostname before it checks the signature, so the
connection would fail either way, and approving one would look like a completed
security step while changing nothing. Fix the platform's certificate and run
setup again. The same refusal applies on the --ca-fingerprint path.
Full explanation, including the trust-on-first-use caveat: Sensor Registration → Trust Bootstrap.
Operation¶
Job Polling¶
The agent polls the platform every 30 seconds (configurable) for pending jobs. When a job is available:
- Agent receives job with encrypted credentials
- Credentials are decrypted in-memory
- Device interrogation is executed
- Results are submitted back to platform
- Credentials are cleared from memory
Timeouts, retries and shutdown. The agent runs one job at a time, and each
job is bounded: 5 minutes unless the job carries its own timeout_seconds
(capped at 1 hour). A device that does not answer in time fails the job with a
"timed out" reason and the agent moves on to the next one. A crash inside one
device's interrogation is reported as a failed job too, not a dead agent. If the
platform cannot be reached when a job finishes, the agent retries the result
five times over about two minutes before giving up; a result still undelivered
after that is lost, and the job shows as failed or stuck on the platform. On
SIGTERM or SIGINT (and on a service stop) the agent takes no new jobs, lets
a job already running finish and submit its result, and exits, waiting at most
30 seconds. Set your service manager's stop timeout above 30 seconds.
Adding devices through the agent. When a device is reachable only from
this agent, Add device can route its identification here (Reach it
from in the form). That is a device_discovery job, assigned to this agent
alone: the agent logs in, reads the device's identity with the same calls the
platform uses, and reports only that identity — or the reason it could not.
On each poll the agent tells the platform which job types it supports; an agent
older than the platform does not, and is never handed a discovery it cannot
run. Add device refuses to queue on an agent that has not polled in the last
10 minutes.
Supported Device Types¶
Currently supported: - Fortinet FortiGate - SSL VPN, IPSec tunnels, certificates - F5 BigIP - Virtual servers, SSL profiles with cipher lists, cert chains (iControl REST API) - Palo Alto Networks - SSL/TLS policies, certificates (PanOS XML API) - Cisco - Crypto maps, IPSec/IKEv2 SAs, SSL/TLS configs, SSH metadata (SSH/CLI) - Ubiquiti UniFi - Controller TLS configs, device enumeration, site settings (REST API) - Generic HTTP - REST API certificate extraction with TLS deep scan - Generic SNMP - SNMPv2c system information collection
Host inventory: local and remote modes¶
The tenant-facing view of this — what it collects, what the result becomes, and where it shows up in the console — is Host Inventory.
Beyond interrogating network devices, the agent can build a general inventory of a host — its operating system and kernel, hardware identity, installed packages, listening sockets, and installed certificate stores. One collector, reached two ways:
| Local | Remote | |
|---|---|---|
| What it describes | The machine the agent is installed on | Another machine, over SSH |
| Credentials | None — it is already there | The target's stored SSH credentials |
| Started by | The agent, on a schedule | A job you queue against a device |
| Identity | Strongest: the agent's own id, plus the host's serial and MACs | Whatever the host reports |
| Sees loopback sockets and the package database | Yes | Yes, subject to the login account's privileges |
Local mode is off by default. Turn it on from the console — Discovery →
Sensors & Agents → the agent → Settings — or, for an agent that has not
enrolled yet, with HOST_INVENTORY_ENABLED=true (or host_inventory_enabled:
true in the config file). The agent then collects on start and every
HOST_INVENTORY_INTERVAL (default 24 hours, minimum 1 hour) and posts the
result to the platform. An agent you installed to interrogate firewalls will not
start enumerating its own host's software just because it was upgraded.
Turning it on from the console takes effect at the agent's next check-in, without a restart and without touching the host. Turning it off stops the collection, rather than collecting and discarding the result.
Remote mode is queued like any other job, against a device that already has SSH credentials configured:
POST /api/v1/device-interrogation-service/devices/{id}/interrogate
{
"job_type": "host_inventory",
"mode": "remote",
"transport": "ssh",
"agent_id": "<the agent that can reach the target>"
}
mode must be remote: local collection is agent-originated, so a local-mode
job would sit in the queue with nothing to claim it. agent_id is required —
the collection runs from an agent that can reach the target host.
Preview what would be sent¶
./device-agent --host-inventory-once
Collects this host once, prints the ordinary report as JSON, and exits. It
contacts nothing and needs no enrollment. The preview excludes outbound
connection peers; those remain behind the separate scheduled or requested
collection opt-in. It exits 2 if some sections did not complete (the report
is still printed).
Exactly what runs, per OS¶
Everything below is read-only. Nothing is written to the host, no configuration is changed, and no command takes an argument from outside the agent.
Linux
| What | Command or file |
|---|---|
| OS name and version | /etc/os-release |
| Kernel | uname -r |
| Host and domain names | uname -n, hostname -f |
| Hardware vendor, model, serial, UUID, BIOS | /sys/class/dmi/id/{sys_vendor,product_name,product_serial,product_uuid,bios_version} |
| Interfaces and addresses | ip -d -j addr (-d adds each link's kind and VLAN id) |
| Installed packages | dpkg-query -W -f=…, or rpm -qa --queryformat=…, or apk info -v |
| Listening sockets and UDP bindings | ss -ltnup, falling back to /proc/net/{tcp,tcp6,udp,udp6} |
| Outbound peers (opt-in) | ss -tnup, falling back to /proc/net/{tcp,tcp6,udp,udp6} |
| Trust stores | find /etc/ssl/certs /etc/pki/tls/certs -maxdepth 1 -type f …, then reads the certificate files it lists |
product_serial and product_uuid are root-readable only on most
distributions. An agent running unprivileged simply does not report them — the
fields are absent, never blank, because a blank serial shared across the
estate would merge every host into one asset.
macOS
| What | Command or file |
|---|---|
| OS name, version, build | sw_vers |
| Kernel | uname -r |
| Host and domain names | uname -n, hostname -f |
| Hardware model, serial, platform UUID, boot ROM | system_profiler SPHardwareDataType -json |
| Interfaces and addresses | ifconfig -a |
| Installed packages | pkgutil --pkgs, plus each /Applications/*/Contents/Info.plist for names and versions |
| Listening sockets and UDP bindings | lsof -nP -iTCP -sTCP:LISTEN, lsof -nP -iUDP |
| Outbound peers (opt-in) | lsof -nP -iTCP -sTCP:ESTABLISHED and connected UDP rows |
| Trust stores | /etc/ssl/cert.pem, /private/etc/ssl/certs |
The System and login Keychains are not read. Reading a keychain means asking for access to a store that also holds private keys and passwords, which a host inventory has no business holding.
Windows (PowerShell, run as powershell -NoProfile -NonInteractive
-EncodedCommand …)
| What | Command |
|---|---|
| OS name, version, build, machine and domain names | Get-CimInstance Win32_OperatingSystem, Win32_ComputerSystem |
| Hardware vendor, model, UUID, serial, BIOS | Get-CimInstance Win32_ComputerSystemProduct, Win32_BIOS |
| Installed programs | Get-ItemProperty over the Uninstall keys under HKLM:\SOFTWARE\… and HKLM:\SOFTWARE\WOW6432Node\… |
| Interfaces and addresses | Get-NetAdapter, Get-NetIPAddress |
| Listening sockets and UDP bindings | Get-NetTCPConnection -State Listen; Get-NetUDPEndpoint is retained separately with unknown role |
| Outbound peers (opt-in) | Get-NetTCPConnection -State Established; this API exposes no UDP peer |
| Certificate stores | Get-ChildItem Cert:\LocalMachine\{Root,CA,My} — subject, issuer, thumbprint and expiry only |
Remote collection on Windows uses PowerShell over SSH, which needs the
OpenSSH Server feature enabled. The minimum supported Windows version is
Windows Server 2019 or later (or Windows 10 1809 and later) — that is where the
OpenSSH Server feature is available. WinRM is not supported: its Go client would
add sixteen modules, including a Kerberos stack, to a cross-compiled agent in
order to run one PowerShell command over a channel SSH already provides. Asking
for "transport": "winrm" returns an error that says so.
What is never collected¶
- No private keys, ever. Certificate stores are read for certificates only:
a PEM block that is not a
CERTIFICATEis skipped without being decoded, and the shape a certificate is stored in has four fields — subject, issuer, SHA-256 fingerprint, expiry — with nowhere for key bytes to go. A private key found loose in a trust directory is reported only as a count of non-certificate blocks. - No passwords, tokens or keys of any kind. Nothing here reads a configuration file, an environment block, or a credential store.
- No command transcripts. A step that fails records the step name and a short message, never the output it could not parse.
- No process arguments. A listening socket records the process name that holds it, which is the service identity; a command line can contain a password.
- No file contents beyond
/etc/os-release, the DMI fields listed above, application manifests, and certificate files.
Reading the result¶
A collection reports each section as ok, failed or unsupported. This
matters: a host with no packages listed and packages: ok genuinely has none,
while packages: failed means the agent could not look. The two are never
collapsed, so an unanswered question is not displayed as a clean answer.
If a report is too large¶
A very large host can produce a report the platform will not accept. The agent logs this distinctly:
Host inventory NOT submitted — report too large: 41231922 bytes exceeds the
platform's cap. This will not fix itself: the next collection (in ~24h) will be
the same size.
That is not a transient failure and the agent does not retry it — the same host
produces the same size every run. Either reduce what is collected on that host
or raise the platform's cap. The cap is 32 MiB, which is roughly three times the
largest report the collector's own limits can produce (20,000 packages and 1,500
trust-store certificates), so reaching it means something unusual: check the
package count in --host-inventory-once output first.
What appears in the inventory after a collection¶
One asset per host, and everything the collection learned about it:
| You will see | Where |
|---|---|
| The host, awaiting approval | Discovery → Approvals, class Unknown host |
| Its OS, kernel, hardware vendor/model/serial and interfaces | The asset's Facts |
| One entry per listening socket, with the process holding it and whether it is reachable from the network | The asset's Endpoints |
| Its installed packages | The asset's Software tab, and the Inventory Software lens |
| "Last host inventory: 2h ago — 412 packages, 18 listeners" | Discovery → Sensors & Agents, on the agent's row |
| The same counts per run | Discovery → Discovery Jobs, under the run's status |
The asset arrives pending approval, like everything else the platform discovers: it appears in Approvals for someone to admit deliberately, and does not count against your inventory until they do.
The class is left as Unknown host, on purpose. The agent measures an operating system and a hardware model; deciding that those make the machine a server rather than a laptop is a rule, not a measurement, and a plausible-looking guess is the kind that gets approved without being read. Set the class yourself when you approve the asset.
A second collection updates the same asset. It is matched on the agent's own installation id, which is the strongest identifier the platform has — so re-collections do not create duplicates, and packages that have been uninstalled are marked removed rather than deleted, which is what keeps "this library was here in March and is gone now" answerable.
If a section failed, nothing is invented for it. A host whose package database could not be read reports no package count and no software changes at all — it is not recorded as a host with no software, and its existing software list is left exactly as the last successful collection left it. The same applies to its listening sockets: a collection that could not read the socket table leaves the endpoint list alone rather than reporting every service as stopped.
A service that stops listening is marked closed, not deleted. The agent reads the machine's own socket table, so it is the only thing that can tell you a port is genuinely no longer served — a network scan can only report what answered, and silence there might be a firewall. On the next collection a socket that has gone shows as closed on the asset's Endpoints tab, keeping its history; if it comes back it goes active again. Endpoints that something else found — an active scan, a cloud connector — are never touched by this, because the agent is only authoritative about its own host. Sockets are closed only by a collection the platform recognised as this host: a report that could only be held for review in Discovery → Observations closes nothing.
Addresses. Every address the host reports is used to recognise it, plus the address the platform reached it at (a remote collection) or the address the agent reported for itself, so a collection without a network-interfaces section still lands on the right asset. Addresses on virtual interfaces — container bridges, veths, tunnels — are recorded on the asset as evidence but never used to match it, because the same bridge address exists on every host running the same software.
The Endpoints tab's Exposure column has three states, and blank is one of them. "Bound to localhost" and "reachable" are both measurements the host itself made. A blank means nobody established it, which is every endpoint found by a network scan — a scan sees what answers, not what a socket is bound to. It is deliberately not shown as "reachable": that would be an exposure claim nobody measured.
If the same agent id turns up on different hardware — a restored image, a cloned VM, a replacement chassis with the agent's state copied over — the platform does not silently merge the two. It opens a merge proposal in Approvals naming both, because two different hardware serials are two different machines whatever else they have in common, and asks you to settle it.
Collections taken before this shipped are not back-filled. They are still on the job, complete, and nothing was lost — but they were collected under an older projection and replaying them would produce assets identified by rules that no longer apply. The next scheduled collection from each agent lands normally, so on the default 24-hour interval every agent's host appears within a day of upgrading. There is no reprocess action and none is planned.
Heartbeat¶
The agent sends a heartbeat to the platform every 60 seconds to indicate it's alive and ready for jobs. The heartbeat also reports the agent's binary version and the addresses bound on its host, which is how the fleet list shows a multi-homed agent's full address inventory — the platform cannot observe those itself, because NAT and ingress rewrite the connection source.
Viewing the fleet¶
Enrolled agents appear under Discovery → Sensors & Agents, in their own Discovery agents table below the network sensors. Sensors and agents are different things and are listed separately: the table shows, per agent, the host and its addresses, what the agent is permitted to interrogate, when it last ran a job and how many it has run in total, its version, and whether it is currently checking in.
An agent that has reported a host inventory also shows "Last host inventory: 2h ago — 412 packages, 18 listeners" on its row. That is a separate line from the job counts because it answers a different question: a host inventory is not work anybody queued, so an agent busy interrogating firewalls that has never described its own host looks perfectly healthy on "47 jobs · 2h ago". The line is absent when the agent has never reported one, which usually means HOST_INVENTORY_ENABLED is not set.
An agent whose last heartbeat is stale shows as offline even if its status column still reads active — nothing rewrites that column after enrollment, so the heartbeat is the authority.
Removing an agent¶
Discovery → Sensors & Agents → Discovery agents → ✕ on the row, then confirm. Requires the discovery.manage permission.
Removing an agent:
- takes it out of the fleet list and stops it being assigned any further work;
- revokes its client certificate;
- returns its queued jobs to the pool, so another agent — or the platform's own in-cluster worker — picks them up. A queued job that names no device cannot be reassigned and is marked failed, because nothing else can resolve its target;
- marks any job it was currently running as failed. That job is not retried automatically, since the agent may have already carried out the work before it was removed; re-run it from the device if you need the result.
This does not uninstall the agent. The binary keeps running on its host and will keep polling. Stop and remove it separately on its host (stop the service, then delete its install directory and config). Until you do, its polls are rejected with a 404 — harmless, but noisy in its logs, and covered under Troubleshooting below.
Security Considerations¶
Credential Handling¶
- Credentials are encrypted at rest in the platform database
- Credentials are wrapped in an AES-256-GCM envelope per job (key derived from the job ID and the agent's secret, which the platform issues at enrollment)
- Agent decrypts credentials in-memory only during job execution
- Backward compatible: accepts pre-decrypted credentials if
encrypted_datafield absent - Credentials are never written to disk or logged
- Credentials are cleared from memory after job completion
Network Security¶
- Agent uses outbound-only HTTPS communication
- No inbound ports required
- All communication is initiated by the agent
- TLS 1.2+ required for all connections
Access Control¶
- Registration key required for initial bootstrap only, and not kept on the agent afterwards
- Client certificate (issued at registration) and agent ID are used for job polling and results
- Job credentials are protected by a per-agent secret issued at enrollment and rotated with the certificate (see What the agent keeps after enrollment)
- Jobs are scoped to specific agents
- Agent can only access jobs assigned to it
Troubleshooting¶
HTTP 401 on job polling or heartbeat¶
Usually means the agent request hit a route that still required a tenant JWT (browser session) instead of agent mTLS. Ensure the platform runs a build where outbound routes (/agents/:id/jobs, /results, /heartbeat) are protected by agent auth, not only JWT. Also confirm agent_id in the config file matches the enrolled agent (after registration, the agent should persist agent_id automatically).
"This agent cannot store its enrollment"¶
The agent could not create or write its data path (<data_path>/certs) or its
config file, so it stopped before contacting the platform. The registration
key is still unused. Install with install-device-agent.sh, run the agent as a
user that can write the data path, or set data_path in its config to a
directory that user can write.
Agents older than this check sent the key first and then failed to save. They
logged Failed to save certificates to disk, and every later call failed with
certificate signed by unknown authority. That enrollment cannot be recovered:
delete the agent in the console, generate a new key, and install again.
Behind an egress proxy¶
The agent honours the standard HTTPS_PROXY, HTTP_PROXY and NO_PROXY
environment variables both before and after enrolment, including after its
mTLS client certificate is issued or rotated. Set them in the agent's service
environment (for the systemd unit, an Environment= line) and restart it. The
proxy must allow a tunnelled connection (CONNECT) to the platform's mTLS
endpoint, because the agent presents its client certificate end to end.
The agent reports "rotated" but you see lock-outs after a restart¶
Certificate rotation revokes the old certificate on the platform. If the agent
cannot save the new one (a full disk, a read-only data directory) it keeps
running on the new certificate in memory, logs
Certificate rotated on the platform but the new certificate could NOT be saved
at error level, and repeats the message on every heartbeat while retrying the
save. Fix the permissions or free space on the data path; the next heartbeat
saves the certificate and logs that it did. Restarting before that point
leaves the agent unable to authenticate, and it must be re-enrolled.
Invalid or rejected registration key¶
- Wrong key type: use a device interrogation pending key, not a network sensor key.
- Key expired, already used, or database was reset: create a new pending key and enroll again.
The platform is presenting a certificate for another hostname¶
Setup stops before offering you a trust decision, and names the certificate's
real identity. A name like <hash>.traefik.default means the platform's TLS
secret is missing and its ingress controller is serving a self-issued
placeholder. This is a platform-side fix; no CA you pin on the agent changes the
outcome, because hostname verification runs before signature verification. See
Platforms with a privately-signed certificate.
HTTP 404 "Agent not registered" on every poll¶
The agent was deleted from the platform (Discovery → Sensors & Agents), but the binary is still installed and running. This is the expected, deliberate response: a removed agent is rejected at the door and receives no jobs and no credentials, even though its certificate file is still on disk.
It is not a network fault and there is nothing to fix on the platform side. Either stop and uninstall the agent on this host, or — if it was deleted by mistake — mint a fresh registration key and enroll it again. The old key cannot be reused.
Agent Not Receiving Jobs¶
- Verify registration key is correct
- Check agent status in platform UI — if the agent is missing from the Discovery agents table entirely, it has been deleted (see the 404 entry above)
- Verify network connectivity to platform
- Check agent logs for errors
Job Execution Failures¶
- Verify device credentials are correct in platform
- Check device connectivity from agent location
- Review job error messages in platform UI
- Enable verbose logging:
VERBOSE=true
Connection Issues¶
- Verify
PLATFORM_URLis correct - Check firewall rules allow outbound HTTPS
- Verify TLS certificate is valid
- Check DNS resolution for platform URL
Service Management¶
systemd (Linux)¶
Create /etc/systemd/system/device-agent.service:
[Unit]
Description=Device Interrogation Agent
After=network.target
[Service]
Type=simple
User=device-agent
WorkingDirectory=/opt/device-agent
ExecStart=/opt/device-agent/device-agent --config /etc/device-agent/config.yaml
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl enable device-agent
sudo systemctl start device-agent
Windows Service¶
Use NSSM (Non-Sucking Service Manager) or similar to run as a Windows service.
Monitoring¶
Health Checks¶
The agent sends periodic heartbeats to the platform. Monitor agent status in the platform UI.
Logs¶
Logs are written to stdout/stderr. For production, redirect to log files:
./device-agent --config device-agent.yaml >> /var/log/device-agent.log 2>&1
Updates¶
To update the agent:
- Download new binary version
- Stop current agent
- Replace binary
- Restart agent
The agent will automatically re-register with the platform if needed.
Support¶
For issues or questions: - Check platform documentation - Review agent logs - Contact platform support