Axiom Border

Axiom Border

Axiom Border is a network and asset monitoring probe you install at the edge, close to the equipment it watches. It is not an agent that reports to a cloud: it observes, decides and acts on its own, on the machine, and centralising into OpenGate is something you switch on afterwards if a fleet-wide view is useful to you.

It works fully disconnected, and it does four jobs that normally take four separate tools:

flowchart TB
    A["Your network<br>servers ยท devices ยท PLCs"]:::ext --> P["<b>Axiom Border</b>"]
    P --> M["Metrics<br>and alarms"]
    P --> S["Security<br>assessment"]
    P --> C["Web console"]
    M --> OG["OpenGate<br><i>optional</i>"]:::ext
    S --> OG

    classDef ext fill:#e9edfa,stroke:#486ac9,color:#101010
  1. Monitors the hosts and devices around it, collecting metrics and keeping the history locally.
  2. Raises alarms on its own, from rules you define, with no need to reach a server first.
  3. Assesses security โ€” discovers assets, scans ports and services, detects vulnerabilities, queries SNMP and observes traffic without emitting a packet. It carries its own suite of 57 industrial protocol checks and recognises sixteen OT protocols as it listens, four of them carried directly over Ethernet with no IP address involved. That is what sets it apart from a general-purpose scanner.
  4. Integrates with OpenGate when you want a fleet-wide view โ€” and keeps working exactly the same when the link is down.
Start here

Presenting Axiom Border explains the product in seven short readings: what it is, the loop it runs, its four probes, how passive and active analysis hand over to each other, how an event becomes an actionable alarm, the models that run on the probe, and the security posture it deploys with.

Read that first. The rest of this section is how to operate what it describes.

What it needs to run

Axiom Border installs from a single self-contained package that carries everything it depends on, so the target machine needs no internet access and no manual preparation.

Requirement
Operating system Ubuntu 22.04 or 24.04, Debian 12 or 13 โ€” 64-bit
Machine 2 CPU cores and 2 GB RAM minimum; 4 cores and 4 GB recommended
Disk 4 GB free minimum, 20 GB recommended for history
Network Access to the network you want to monitor
Internet Not required

It installs in one of two roles โ€” central, the complete node, or monitoring, a remote node that reports into a central one. Only the central role carries the metrics database and the components behind the optional anomaly-detection feature.

Where to go next

New to the product: Presenting Axiom Border, and then Configuration to adapt a running probe to your environment.

Every catalogue the probe carries โ€” vulnerabilities, manufacturers, OIDs and MIBs โ€” where each comes from and how to keep it current: Catalogs: vulnerabilities, OIDs, MIBs and more.

Getting a running probe to do something useful: Metrics and monitoring to bring data in, Security assessment to examine what is around it, and Alarms and rules to react automatically.

Integrating with other systems: MQTT and OpenGate.

Before you scan industrial equipment

Axiom Border can perform checks that write to industrial devices. This is switched off by default and needs two separate confirmations to enable โ€” but the responsibility for authorising it is yours. Read OT/ICS vulnerability scanning before enabling anything against a live plant.

Index

Subsections of Axiom Border

Presenting Axiom Border

Presenting Axiom Border

The rest of this section explains how to operate the probe. This part explains what it is and why it behaves the way it does โ€” the seven ideas that, once you have them, make every screen in the console read the way it was meant to.

Read it in order the first time. Each page assumes the one before it.

Page The idea it carries
1 What Axiom Border is Not an agent that reports to a cloud โ€” an autonomous node that observes, decides and acts by itself
2 The full loop Ingest, observe, decide, act: four stages, four clocks, one local state โ€” and nothing leaves by default
3 The four probes One listens, three ask. What each produces and the execution contract the three active ones share
4 Passive and active Two kinds of claim, not two views of the same data. Neither one erases the other
5 From event to alarm Coalescing by identity, acknowledgement that reopens, and rules that already know what changed
6 AI on the probe Inference on the node, in local containers, with no external call
7 Security posture What the two installation paths deploy, and where each kind of data lives
Documented version

The text describes Axiom Border v1.3.1. The console captures were taken on v1.2.0, so a few details have moved on since โ€” most visibly the vulnerabilities table, which v1.3.1 renamed and gave a potential-surface summary. Where a capture and the text disagree, the text describes the current console.

Index

Subsections of Presenting Axiom Border

What Axiom Border Is

What Axiom Border is

Most edge monitoring works one way: a collector gathers data, ships it somewhere, and a platform decides what it means. Take the link away and the collector is a data recorder at best.

Axiom Border is built on the opposite premise. It is not an agent that reports to a cloud. It is an autonomous node that observes the network around it, decides what matters against rules it holds locally, and acts on that decision โ€” on the machine, with no server in the loop. Centralising into OpenGate is something you switch on afterwards if a fleet-wide view is useful to you. It is never what makes the probe work.

That single decision is why the product looks the way it does:

  • The decision engine is on the probe. Alarms are raised where the data is, not where the platform is.
  • Everything it needs travels with it. The vulnerability database, the vendor registry, the MIB catalogue, the scan templates, the metrics store, the container runtime and the web console are all installed with the product. A probe in an isolated network is useful the moment it starts.
  • Nothing is fetched at run time. There is no feed to update before a scan means anything, and no service to call before a finding can be interpreted. What exactly it carries, where each data set comes from and how to refresh it in the field is set out in Catalogs: vulnerabilities, OIDs, MIBs and more.
Why this matters in an industrial network

Isolated OT segments are isolated on purpose. A tool that needs to reach the internet to stay useful is a tool that is either useless there or a hole in the isolation. Axiom Border needs neither.

The specification, in one table

Deployment roles central โ€” the full node: probe, metrics store and the container runtime for the AI capability. monitoring โ€” a remote node that reports into a central one
Installation path A single supported path, the Keystone deployment agent, and it is 100 % offline. Roughly 5โ€“8 minutes for a central node, 1โ€“2 for a monitoring one
Footprint About 141 MB for a central core package and 89 MB for a monitoring one, plus the bundled data packs, which are published once and reused across versions
Management surface The embedded web console and a REST API, both served by the probe itself, both over HTTPS unconditionally โ€” localhost included
Internet access Not required, at install time or afterwards
HTTPS is not optional

There is no plaintext listener to fall back to. A probe reached over http:// refuses the connection, and that applies to localhost as much as to anything else. On first boot the probe issues its own certificate from a local authority, so it works with no PKI and no internet โ€” see Security posture for replacing it with your own.

The two roles

A single probe covers a site on its own. The roles exist for when one site is not the whole picture.

flowchart LR
    M1["monitoring<br><i>remote node</i>"] --> C
    M2["monitoring<br><i>remote node</i>"] --> C["<b>central</b><br>probe ยท metrics store<br>console ยท AI"]
    C -.->|optional| OG["OpenGate"]:::ext
    classDef ext fill:#e9edfa,stroke:#486ac9,color:#101010

A central node is a complete Axiom Border: it watches its own network, holds the history, serves the console, and can additionally receive from monitoring nodes. A monitoring node is the light end of the fleet โ€” it watches the machine it runs on and reports into the central one.

The dotted arrow is the point. OpenGate sits outside the boundary, it is optional, and it starts switched off.

How you work with it

Through the console. The probe serves its own web interface; point a browser at its address and log in. Everything on the pages that follow is described from that interface, because that is how the probe is meant to be operated.

A REST API exists underneath for integrating the probe with your own systems, and OpenGate drives the probe over it. It is not the operator’s path.

The Axiom Border console login

The console has six views, in the order the sidebar lists them:

View What it is for Where it is documented
Network status Everything discovered on the network: assets, ports, vulnerabilities, and the passive inventory behind them Security assessment
Hosts Status The machines running a monitoring agent, one block per node Metrics and monitoring
Alerts Raised alarms, alarm history, and the rules behind them Alarms and rules
Supervisions The monitoring agents reporting in and what each last delivered Metrics and monitoring
AI capabilities Anomaly-detection models running on the probe AI capabilities
Configuration The probe’s own settings, edited and applied from the browser Configuration

Network status is the landing view, which tells you where the product thinks the centre of gravity is: what is out there, and is it exposed.

Next: The full loop โ€” the four stages a single observation travels through.

The Full Loop

Ingest, observe, decide, act

Everything Axiom Border does fits into four stages. They run in parallel and on their own clocks โ€” the numbering follows a single observation through the product, not an order of execution. What binds them together is that all four write to the same local state, and all four can raise an alarm.

flowchart LR
    I["<b>1 ยท INGEST</b><br>from the machines"] --> O["<b>2 ยท OBSERVE</b><br>from the network"]
    O --> D["<b>3 ยท DECIDE</b><br>local rules"]
    D --> A["<b>4 ยท ACT</b><br>alarm and notice"]
    A -.->|optional| C["OpenGate"]:::ext
    classDef ext fill:#e9edfa,stroke:#486ac9,color:#101010

1 ยท Ingest โ€” what the machines report about themselves

Every monitored machine runs a lightweight agent that watches it from the inside and sends what it sees. Four monitors, each answering one question:

Monitor What it watches
SSH-Guard Interactive sessions and login attempts against the machine
USB-Guard Devices attached to and removed from its ports
IFACES-Guard The state of its physical network interfaces
METRIC-Guard The machine’s own health, and the heartbeat that proves it is still reporting

You see this stage in two views. Supervisions lists the agents reporting in, one card per monitor, with the last event each delivered. Hosts Status turns the same data around and shows one block per machine, with the alarms currently open against it.

2 ยท Observe โ€” what the probe works out for itself

Four probes on the network, one listening and three asking, each with its own scheduler. This is the stage that needs no cooperation from the equipment it watches, and it is covered in full on the next page: The four probes.

Everything they find lands in Network status.

3 ยท Decide โ€” rules that run on the probe

Two mechanisms, both evaluated locally against data the probe already holds:

  • Rules on each event, as it arrives. One rule set per probe, so a rule can react to a new host, a newly opened port or a changed value the moment it is observed.
  • Periodic checks against the history. Counting occurrences over a window, and detecting the absence of them โ€” a node that stopped reporting is a condition you can only see by looking for silence.

Both are edited from Alerts โ†’ Rules configuration. Neither needs the uplink, which is the whole point: a probe whose link is down still decides.

4 ยท Act โ€” the alarm, and who hears about it

A raised condition becomes a coalesced alarm โ€” one alarm with a count, not one alarm per occurrence โ€” which lands on the operator’s Alerts view and in the probe’s live feed, so the console’s counter moves immediately. From event to alarm covers how that works and why it is built that way.

And then, only if you asked for it, the same alarm goes out to OpenGate.

Nothing leaves the machine by default

This is the commercially important half of the loop, so it is worth stating plainly:

  • The OpenGate integration is optional and starts inert. It has to be configured and enabled before a single byte goes anywhere.
  • The message broker the console listens on lives on loopback. It is not exposed to the network.
  • All four probes write to local storage before any decision to publish exists.

A probe you install and never configure for the cloud is a probe that never talks to one. What travels when you do enable it is the complete inventory of your network, which is exactly why the probe refuses to start that integration over an insecure address โ€” see Security posture.

Next: The four probes โ€” the observation stage in detail.

The Four Probes

One listens, three ask

Axiom Border looks at the network with four probes. The split that matters is not what they scan but whether they speak: one of them observes without emitting a single packet, and three of them ask questions and read the answers.

Probe How it works What it produces When it runs
Passive analysis Listens to the traffic reaching its capture interface. Emits nothing Assets indexed by MAC address, which address belongs to which device, LLDP neighbours, the services actually in use, sixteen industrial protocols in play โ€” four of which never touch IP, traffic volume per host, what kind of point the probe is plugged into, and vulnerabilities inferred from device identity Continuously, persisting what it has learned on a configurable interval
Network discovery Port scanning over TCP, and UDP if you ask for it The real state of a port โ€” open or closed โ€” the service behind it, and an operating-system guess On a schedule, or on demand
Vulnerability scanning A template engine that sends the probes a check needs Findings with their severity, the check that fired, and what it matched. Includes a purpose-built industrial suite for Modbus, IEC-104, DNP3, BACnet and OPC UA On a schedule. The layer that writes to devices needs two separate locks released
SNMP Queries v1, v2c or v3, using stored credential profiles Values by symbolic name or numeric OID, and full sweeps, resolved against the bundled vendor catalogue On a schedule, or on demand

All four feed the same Network status view, and their launch controls sit in its header:

Network status โ€” the consolidated inventory, with Passive analysis, Scan network and Automatic scans in the header
  • Passive analysis opens what the listening probe has worked out, on its own terms.
  • Scan network launches a discovery run now.
  • Automatic scans opens the schedules for all of them, in the Configuration view.
Turning off the passive probe does not stop discovery

The four are independent. Switching continuous passive analysis off leaves discovery, vulnerability scanning and SNMP populating the inventory as before โ€” and the reverse holds too. What you lose is the half of the picture that nothing else can produce, which the next page is entirely about.

The contract the three active probes share

Launching a scan does not block. The three asking probes behave identically here, and knowing the contract is what lets you tell “still working” from “gave up”:

  1. The request is accepted immediately and comes back with an identifier for that run.
  2. The run reports one of three states: in progress, failed, or finished successfully.
  3. Only one discovery run happens at a time. Asking for a second one while the first is going is refused, unless you explicitly force it โ€” which cancels the running one and marks it as failed rather than pretending it finished.

The Network status header shows the state of the most recent run, so the view always says whether what you are looking at is current or still being assembled.

A scan interrupted by a restart does not hang forever

If the probe restarts while a scan is running โ€” a reboot, a service restart, a power cut โ€” that run can never finish, because the process that owned it is gone. On start-up, every execution still marked as in progress is moved to failed.

The practical consequence: a run that shows as in progress is genuinely in progress. It is not a ghost left over from last week, and you do not have to guess which it is.

What each probe is for, in practice

  • Reach for network discovery to answer what is listening here, right now. It is the only one that can confirm a port is genuinely open, because it is the only one that knocks.
  • Reach for vulnerability scanning once discovery has given it a baseline to work from. Its industrial suite is what separates this from a general-purpose scanner, and its write-capable checks are switched off behind two locks โ€” read OT/ICS vulnerability scanning before enabling anything against a live plant.
  • Reach for SNMP when you want the device’s own account of itself. Symbolic names work without internet access because the vendor catalogue ships with the product.
  • Passive analysis you do not reach for. It is always running, and it is the one that works when the other three cannot.

Full operating detail for all four lives in Security assessment.

Next: Passive and active โ€” why these are two kinds of claim, not two views of the same data.

Passive and Active

Two kinds of claim, not two views

This is the piece that gets misread most often, so it is worth saying whole. Passive and active analysis are not two windows onto the same inventory. They produce different kinds of statement about the network, and the product is built so that neither one can quietly erase the other.

The console says so on the face of it. In the vulnerabilities table on Network status, every finding carries an Origin:

Origin What it means
probe The request was sent and the target answered. Demonstrated.
inferred Derived from the device’s identity, with no traffic emitted. It is not proof โ€” verify before acting.

An inferred finding also carries how complete the identity behind it was, and that completeness decides what the probe is allowed to claim: the exact version licenses a plain affected; vendor and product with no version is potentially affected, verify; and the vendor alone pins no CVE to the host at all, so it is reported as a potential surface to go and confirm. The probe never hands you a conclusion without telling you what it rests on.

Five things worth knowing

1. The passive probe decides whether an active scan means anything

The correct way to install a probe in an industrial network is on a mirror port or a TAP: it receives everything and cannot be answered. On such a port an active scan returns nothing โ€” and that nothing means “nobody can reply to me from here”, not “there is nothing here”.

So classifying the connection point is the first thing the passive probe does, and the console leads with the verdict. Open Notices on Network status:

The Notices panel on Network status, opening with the capture-point verdict

The probe listens on a mirror port, so it receives traffic addressed to other devices. Bear that in mind when reading an active scan: a host that does not answer may simply be unable to answer from here, rather than absent.

Without that line, half a day goes into working out why the discovery scan cannot find a network that is plainly there.

2. They discover along different axes

The passive probe indexes by MAC address. The active probes index by IP address. That is not a detail of bookkeeping โ€” it changes what each one is capable of seeing at all.

  • Passive analysis sees equipment with no IP address of its own: a fieldbus device, an unmanaged switch, a protection relay publishing multicast. None of these will ever produce a row in an active scan, however long you run it.
  • Active scanning sees ports nobody happens to be using right now. A service that is listening but idle emits nothing to listen to, so passive analysis cannot know it is there.

Neither set contains the other. In the capture below the passive inventory holds 16 devices where the address-indexed table on the same probe holds 12, and the coverage note explains the gap in the probe’s own words:

Passive network analysis โ€” the per-device inventory with vendor, type, role and confidence, above its coverage note

Note the closing caveat there, which is the honest limit of the technique: passive analysis only sees what talks โ€” a silent device is not in this figure.

3. The passive probe feeds the active ones

The relationship is not merely parallel. What listening establishes is used to make the asking sharper:

  • An SNMP target that is not yet in the inventory gets a quick discovery pass first, so the query goes to something known to be there.
  • A vulnerability scan that comes back with nothing gets a discovery pass too, for one specific reason: to tell “unreachable” apart from “nothing wrong with it”. Those look identical in an empty report and mean completely different things.

4. Neither origin overwrites the other

Both write into the same identity record, and neither is allowed to win by arriving last. Each leaves its claim, its confidence and the evidence behind it, and the value you see is derived from all of them.

You can read this directly in the console. A manufacturer is shown with a confidence score and the source that produced it โ€” the vendor registry installed on the probe, the device’s own declaration over its maker’s discovery protocol, or an active scan’s own table โ€” each with its caveat attached. A registry entry names the network card; a device declaring itself names the device. Those are not the same claim, and the console does not pretend they are.

So when the active scan says Schneider Electric where the registry said Moxa, that disagreement is kept as data rather than resolved in silence. Nearly always it is telling you something true and useful: a network card from one vendor inside another vendor’s cabinet.

“Undecided” is a result, not a gap

A device the passive probe analysed without any rule matching is labelled undecided, and one written before passive analysis was available is labelled not analysed. They are shown differently on purpose: the first is a conclusion, the second is an absence of one. Neither is a failure, and neither is presented as an unknown you should chase.

5. An active scan does not close a passive inference

The one to hold on to.

A passive inference is not the outcome of a test. It is a statement about what a device is, drawn from how it behaves on the wire. An active scan failing to reproduce it does not refute it โ€” the scan may not have looked for it, may not have been able to reach it, or may have been reading a mirror port where nothing can answer in the first place.

“I did not find it” is not “it is gone.” So a scan that comes back quiet leaves existing passive findings standing, and the console keeps showing them with their origin and their confidence. If you want one retracted, retract it deliberately โ€” do not let an empty scan do it for you.

What it understands on the wire

Listening is only worth as much as what the probe can make sense of. It reads industrial traffic at two levels, and the second is the one a general-purpose scanner does not have at all.

Over IP โ€” thirteen industrial protocols

Recognised from the conversation itself, which means the probe also records which side is serving and which is asking. That distinction is half the value: a station polling a controller and the controller answering are very different things to find on a segment.

Protocol Port Protocol Port
Modbus/TCP 502 BACnet/IP 47808/udp
S7comm 102 EtherNet/IP 44818 ยท 2222/udp
OPC UA 4840 PTP 319โ€“320/udp
DNP3 20000 HART-IP 5094/udp
IEC 60870-5-104 2404 KNXnet/IP 3671/udp
CODESYS 1200 FINS 9600/udp
MELSEC 5007

Four of those โ€” CODESYS, MELSEC, KNXnet/IP and FINS โ€” sit on ports their vendors use by convention rather than by registration, which someone else is entitled to use for something unrelated. They are still reported, because leaving a controller untyped merely because its maker never registered a port is the more expensive mistake. They are simply published at a lower confidence, visible to you in the console, so weaker evidence never passes for strong evidence.

Directly over Ethernet โ€” where there is no IP to work with

The traffic that matters most in a substation or on a production line often has no IP layer at all. Nothing that keys off addresses and ports can describe it. Axiom Border reads it natively:

Protocol What it establishes
IEC 61850 GOOSE The protection and control messaging between relays
IEC 61850 Sampled Values The measurement stream feeding those relays
PTP / IEEE 1588 Who the network’s grandmaster clock is โ€” the time source the protection equipment trusts, where an unexpected one is a finding in itself
PROFINET DCP Devices announcing and being addressed on a PROFINET segment

PTP appears in both lists because it is genuinely spoken both ways, so the two tiers together come to sixteen distinct protocols, not seventeen.

Without these, a probe on such a segment could report a busy cable and nothing more. Traffic carried inside stacked VLAN tags is followed through the tags rather than measured from a fixed offset, so a QinQ segment reads correctly instead of producing confidently wrong values.

What it will not guess

The probe names a device’s role only where the evidence supports it โ€” a Modbus server, a DNP3 outstation, an IEC-104 controlling station, a GOOSE publisher, a PTP node. It deliberately stops short of labelling something a PLC, an RTU or an HMI from traffic alone, because that is a claim about the device’s function that the wire does not actually establish. An honest protocol role beats a plausible-sounding guess.

Side by side

Active scanning Passive analysis
Footprint on the network Emits. Against a PLC, an aggressive scan is a genuine operational risk Zero packets. Nothing a control device could misread
Identity is based on The IP address The MAC address, which survives a change of addressing
Equipment with no IP Invisible Inventoried
On a mirror port or TAP No reply means no results Works normally โ€” and says why the active scan came back empty
Topology Does not see it Neighbours with their chassis and port, gateway verdict, address conflicts
Services The real state of the port, confirmed The services actually in use, and which side is serving
Traffic volume โ€” Counters per host, and the breakdown behind them
Industrial protocols The five the OT suite tests against Thirteen over IP, plus four that never touch IP at all
Industrial posture What the templates test for What the equipment declares about itself as it operates

They are complementary, and the product treats them that way. Use the active probes to confirm; use the passive one to know what is worth confirming, and to see the part of the network the active ones cannot reach.

Next: From event to alarm โ€” what happens once one of them finds something.

From Event to Alarm

Three paths, one gate

Three different things can raise an alarm on an Axiom Border probe:

  • A periodic check counting occurrences of an event over a window and finding too many.
  • A dead-machine check finding a node that has gone silent โ€” alarming on absence, which is the only way to catch a node that stopped reporting rather than one reporting something bad.
  • A rule deciding, in code, that what it just saw is worth an alarm.

All three arrive at the same gate, and what that gate does is the single most operationally significant thing in this part of the product.

Coalescing by identity

Two alarms that say the same thing about the same device are the same alarm. When a condition trips repeatedly, the probe does not create a new alarm each time. It recognises the identity of the alarm โ€” which agent, which device, which name and type, which severity, which underlying description โ€” and folds the repeat into the alarm that is already open. What changes are its fields:

Field Meaning
First seen When the condition first held
Last seen The most recent time it fired
Count How many times it has fired

This pays twice over, and the two payoffs are worth separating because they matter to different people.

For the operator. A brute-force attempt that trips a threshold every five minutes for an hour is one thing happening, and it should read as one row with a count of twelve. Twelve identical rows are not twelve times the information โ€” they are the same information, twelve times, pushing everything else off the screen. An alert repeated a thousand times is an alert, not a thousand alerts.

For the probe. Every distinct alarm is a distinct series in the history. Minting a fresh identity per occurrence makes that history grow without bound and slows every query made against it. Coalescing keeps the history of a busy probe queryable months later.

One row with a count is the signal, not a summary

When you see a count of 40 on an alarm, the probe is not telling you it collapsed 40 alarms to save room. It is telling you the condition has held 40 times since it first fired, and it is still open. The count is the severity information.

Acknowledging is not silencing

ACK on the Alerts view marks an alarm as seen, which closes the coalescing window on it.

The consequence is deliberate: the next occurrence after an acknowledgement opens a fresh alarm. It does not silently add one to a count on something you already dealt with. If the condition comes back, you get told again.

The Alerts view, listing raised alarms with their severity, the rule behind them and the ACK action

Two tabs sit above the table. Recent reads the probe’s live feed โ€” the alarms it already holds in memory, answered instantly. Range queries a window of history instead, which is what you want when reconstructing an incident after the fact.

Open alarms survive a restart

The state of what is currently open is kept on the probe itself, not held in memory and lost on the next service restart. A probe that reboots comes back knowing which alarms were open and which had been acknowledged โ€” and a quiet week does not make it forget either.

Rules that already know what changed

Rules are edited from Alerts โ†’ Rules configuration, where the declarative alarm rules sit in the upper table and the scripted Expert System sits below with its own enable switch:

The Alarm config screen, with the alarm rules table above and the Expert System below

There is one rule set per probe โ€” one for discovery, one for vulnerability scanning, one for SNMP and one for passive analysis โ€” and each receives that probe’s findings already enriched with what changed since the last time the probe saw the same thing:

  • whether this is new,
  • whether the value changed,
  • and what the previous value was.

That is why a rule can say “this port is new” or “this value moved” without querying any history to find out. The comparison has already been made by the time the rule runs.

Each probe reports at its own granularity โ€” one finding per vulnerability, one per host and one per port for discovery, one per value for SNMP, one per traffic flow and per port for passive analysis.

Rules are per probe, not across probes

A rule sees one probe’s findings. Correlating across probes โ€” this port opened and that vulnerability appeared โ€” is outside what the rules engine does; each evaluation belongs to a single probe. Build that correlation where you have the whole picture, in OpenGate.

The live feed

Every alarm is also pushed onto a small in-memory feed of the most recent events, oldest dropping off the end. It is what the console reads to paint its counter and its Recent tab, which is why those respond instantly however much history the probe has accumulated.

Full operating detail โ€” creating checks, writing rules, the fields available to them โ€” is in Alarms and rules.

Next: AI on the probe โ€” when the rule needs a model to decide.

AI on the Probe

Inference happens on the node

Axiom Border can detect anomalies with machine-learning models, and the important word is where: the models run on the probe, in containers on the same machine. There is no call to an external service, no data sent away to be scored, and no dependency on a link being up.

That follows from the same premise as everything else here. A probe that had to reach a hosted model to decide would be a probe that stops deciding the moment the link drops โ€” and would mean the traffic patterns of an isolated industrial network leaving it in order to be analysed.

The images the models run from are installed with the product, so an air-gapped probe can train and infer with nothing fetched from anywhere.

The circuit, end to end

flowchart LR
    D["Agent data<br>on the probe"] --> T["Trainer<br>container"]
    T --> M["Model<br>on the probe"]
    R["Rule"] -->|asks| M
    M -->|anomaly| R
    R --> A["Alarm"]

Training. The probe exports the data it holds for one agent and hands it to that agent’s trainer container. Retraining is scheduled rather than manual, can be cancelled while it runs, and its state is kept โ€” so you can tell a model that is training from one that failed to.

Inference. The rule for an agent asks the model to score what it just received. If the answer comes back as an anomaly, the same rule raises the alarm.

And that alarm is an alarm like any other. It goes through the same coalescing gate described in From event to alarm โ€” same identity rules, same count, same acknowledgement behaviour. An anomaly detected by a model is not a second class of signal with its own inbox; it lands on Alerts beside everything else, and an operator does not need to know which mechanism produced it to act on it.

Health. A rule can also read the model’s own metrics, and the probe polls the models’ health endpoints. This matters more than it sounds: a model that has stopped answering is then an observable fact rather than a silence that looks exactly like “no anomalies today”.

Where you manage it

AI capabilities in the console lists the models and trainers configured on the probe:

The AI capabilities view, listing the models and trainers on the probe

Adding one walks a wizard: upload the trainer package, or pick one already on the probe, and choose the agent it applies to.

Adding an AI capability: the trainer package and the agent it applies to

Deploying, restarting, deleting and importing images are all driven from here. The rule that calls the model is enabled in step with the capability itself, so a model you deploy is a model that gets asked.

Linux, and the central role only

The container runtime the models need is Linux-only, and only the central role installs it. A monitoring node does not run models, and neither does a probe running natively on Windows โ€” which is a development scenario, not a supported production one. On such a host the AI capability simply does not start, and the deployment controls have nothing behind them.

If you intend to use anomaly detection, deploy the probe as central on Linux.

Full operating detail โ€” image formats, the training cycle, calling inference from a rule โ€” is in AI capabilities.

Next: Security posture โ€” the probe’s own attack surface, and where each kind of data lives.

Security Posture

The posture is deployed, not offered

Axiom Border sits inside the network it watches, which makes its own surface part of the product. What follows is not a list of hardening options to consider. It is what the installation puts in place, on both roles, with no decisions required from you.

Front What is deployed
Console and API HTTPS unconditionally, localhost included. There is no plaintext listener to fall back to
Certificate Issued by a local authority on the probe at first boot โ€” no external PKI, no internet. Replaceable with your own, and reloaded without dropping connections
Sessions Token-based, signed with a key generated for that installation. Repeated failed logins from one address are throttled, and every login is written to the audit trail
Message bus The embedded broker listens on loopback only, and its WebSocket listener is off. The console reaches it through a bridge inside the API’s own encrypted connection, using a single-use ticket that expires in seconds
Metrics ingestion Not open to the network. The authentication step is waived only for callers inside a configured range, which defaults to loopback
Configuration Reading the configuration back returns it with every secret redacted. Comments and formatting survive untouched
Cloud egress Optional, and inert until configured. An insecure address stops the integration from starting at all

Why the certificate is self-issued

An air-gapped probe cannot reach a certificate authority, and requiring one before the console works would mean either no encryption or no probe. So the probe issues its own on first boot and is usable immediately over HTTPS.

Your browser will not recognise that authority, which is expected on first contact. If your organisation runs its own PKI, install its certificate on the probe instead โ€” see Configuration.

The same applies to anything else that talks to the probe. Command-line examples throughout this documentation pass curl -sk, where -k is what accepts the probe’s own certificate; once you have installed a certificate your systems already trust, drop it.

Why an insecure cloud address is an error, not a warning

If you point the OpenGate integration at a plaintext address, the integration refuses to start. That is deliberate and it is not adjustable.

What travels over that link is the complete inventory of your network โ€” every asset, every open port, every vulnerability found โ€” together with the credential that authorises it. Sending that unencrypted is not a configuration preference with a trade-off. It is a mistake, and the probe treats it as one.

Secrets are never shown back to you

The Configuration view reads and writes the probe’s settings from the browser, and it says plainly what it does with credentials:

The Configuration view, with its redaction and restart banners above the settings

Each stored credential reads as redacted rather than as its value: leave it untouched to keep it, or type a new value over it to change it. Saving rewrites the file while preserving its comments โ€” and a save that would quietly turn a protection off is rejected rather than accepted in silence.

Configuration is read at start-up

The banner at the top of that view is worth reading before you save: settings are read when the service starts, so nothing saved here takes effect until the probe restarts. The console offers to do it for you where the deployment allows.

Where each kind of data lives

Three stores, each chosen for what is asked of it:

Store What it holds
The probe’s local state Everything that must be exactly right and must survive a restart: alarm rules and scripts, device aliases, scan executions, SNMP credential profiles, the host and port inventory, findings, the provenance behind every identity claim, and the alarms currently open
The metrics database Everything that is a series over time: agent metrics, the audit trail, network traffic, availability, scan metrics and alarm history
The in-memory feed The most recent events per stream, so the console’s live counters answer instantly. Kept across restarts so a fresh probe comes back warm, and reconciled with the history in the background

The practical consequence for an operator: the console’s Recent tabs read the third one and are instant; the Range tabs read the second and are as fast as the window you ask for. Neither is more correct than the other โ€” they are the same events, reached two ways.

What backing up means here

Because the three stores hold different things, a backup that covers only the metrics database keeps your history and loses your rules, profiles and inventory. Operation and maintenance covers what to back up and how to restore it.

That closes the tour. From here, Configuration is where you adapt a running probe to your environment.

Configuration

Configuration

Axiom Border reads a single file, configuration.yaml, from the directory given by the AXIOM_CONFIG_DIR environment variable (default: config). An example file, configuration_example.yaml, is supplied alongside it โ€” copy it rather than editing it in place, so you always keep an untouched reference.

cp config/configuration_example.yaml config/configuration.yaml

You will rarely edit the file by hand: the console’s Configuration view and the REST API write this same file โ€” see Changing the configuration below.

No hot reload โ€” restarts are mandatory

Every field in this file requires a service restart to take effect. There are no exceptions. The file is read once, when the service starts. If you change a value and nothing happens, you have not restarted the service. Saving from the console on a managed deployment restarts the service for you; everywhere else the restart is yours to run:

sudo systemctl restart axiom-border
An invalid file stops the service

If configuration.yaml is missing or is not valid YAML, Axiom Border refuses to start rather than running with defaults. This is intentional โ€” a monitoring probe silently running on the wrong configuration is worse than one that will not come up. Check the log if the service does not start.

Changing the configuration

Three paths write the same file. The web console is the recommended one: it edits the settings a running deployment actually tunes, validates them before writing, and on a managed deployment restarts the service for you. The REST API covers automation and driving a probe without shell access. Editing the file over a shell always works โ€” it is just the least guarded of the three.

Open Configuration in the side menu. The view edits configuration.yaml itself, one group of settings per tab:

The Configuration view, on the Automatic scans tab, with the restart and credentials notices above the fields

Each field is labelled in plain language rather than by its YAML key โ€” nmap: period for schedule, vulnScan: depth for level โ€” and the reference further down this page maps them to the keys they write. The two notices at the top are permanent, not the result of saving: they are there to tell you before you edit that nothing applies until the service restarts, and that the file holds credentials which are preserved for you.

Tab What it edits
Automatic scans The securityProbes switches and intervals: enable and schedule for nmap, vulnScan and snmp; the vulnerability scan’s level, severity and OT switches; the sniffing block, including where the probe reads its catalogues from
OpenGate The whole opengate block โ€” connection, collect and provision
MQTT The whole mqtt block โ€” the embedded broker with its WebSocket and TLS listeners, the internal publisher and the operations client

Everything else โ€” logger, login, influxdb, apiPort, pagination and the per-probe details not listed above, scan targets included โ€” is changed through the API or the file.

Network status has a shortcut straight here: its Automatic scans action opens this view on the first tab, which is where you land when a scheduled scan looks stale or too frequent.

The forms are a window onto the file, not a copy of it. Saving rewrites the whole file but preserves everything the forms do not manage: comments, credentials, and every key outside the three tabs. Leaving a field blank removes its key from the file, returning that setting to its unset behaviour. Durations, ports and cron expressions are checked as you edit, and Save stays disabled while any field holds an invalid value โ€” an invalid document never reaches the probe.

What happens after Save depends on the deployment:

  • On a managed deployment, the console asks the deployment manager (Keystone) to restart the service and waits until it reports healthy again โ€” when the green confirmation appears, the change is already live. The console is unresponsive for the few seconds the restart takes.
  • On a plain systemd installation, the file is written and an amber banner stays on screen until you restart the service yourself: sudo systemctl restart axiom-border.
  • If the automatic restart fails, the write has still succeeded. Restart by hand โ€” keystonectl restart axiom-border, or sudo systemctl restart axiom-border โ€” and the saved configuration applies.

Changing apiPort or the login credential raises an explicit lockout warning: the file is written anyway, so before restarting make sure you can reach the new port or know the new password.

GET /config returns the current YAML and PUT /config replaces it. This exists so a probe can be configured for a customer environment without shell access to the host.

# 1. Authenticate
curl -X POST http://localhost:8083/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"<user>","password":"<your-password>"}'

# 2. Download the current file as a starting template (authenticated โ€” it contains secrets)
curl -H "Authorization: Bearer <jwt-token>" \
  http://localhost:8083/config -o configuration.yaml

# 3. Edit it, then upload the COMPLETE file
curl -X PUT http://localhost:8083/config \
  -H "Authorization: Bearer <jwt-token>" \
  -H "Content-Type: application/x-yaml" \
  --data-binary @configuration.yaml

# 4. Apply
sudo systemctl restart axiom-border

The PUT response reports what happened:

{
  "restartRequired": true,
  "message": "configuration written; restart axiom-border to apply",
  "backup": "/opt/axiom-border/config/configuration.yaml.bak",
  "warnings": ["apiPort changes 8083 -> 9083 (may affect API access after restart)"]
}
Field Meaning
restartRequired Always true โ€” configuration only takes effect at startup
message Confirmation text
backup Path of the previous file’s backup. Omitted when there was no previous file
warnings Present only when non-empty. Raised when apiPort or the login credentials change, because either can lock you out

Console and API end in the same validated write, with three guarantees:

  • Validation before writing. The document is validated exactly as at startup and checked for the six mandatory fields. Invalid YAML or a missing field returns HTTP 400 and nothing is written.
  • Atomic replacement. The file is written to a temporary file in the same directory and renamed.
  • Backup of the previous file, forced to 0600 because it contains secrets.
The write is a full replacement, and the backup is a single level

PUT /config expects the complete file, not a patch โ€” fetch, edit, send back whole. The console handles this for you and sends the full document with your edits applied.

The backup is always the same filename, configuration.yaml.bak, and it is overwritten on every write. There is only one level of history. If you need more, copy it aside yourself before saving. To recover from a lockout, restore that file and restart.

Where the file lives

Deployment Path
Installed with install.sh (systemd) /opt/axiom-border/config/configuration.yaml
Managed deployment /var/lib/axiom-border/config/configuration.yaml โ€” a stable path outside the per-version working directory

Overriding any field with an environment variable

Every key can be overridden from the environment by upper-casing it and replacing dots with underscores. A .env file in the working directory is also loaded.

Configuration key Environment variable
logger.logLevel LOGGER_LOGLEVEL
influxdb.token INFLUXDB_TOKEN
login.pass LOGIN_PASS

This is the recommended way to handle secrets: keep the tokens and passwords out of the YAML file entirely and inject them through the environment or your secret manager.

Required fields

Only six fields are validated as mandatory. If any is missing, the configuration is rejected:

login.user ยท login.pass ยท influxdb.url ยท influxdb.token ยท influxdb.org ยท apiPort

Everything else is optional and has a documented default.

logger

Backend logging, to console and to size-rotated files.

Field Type Default Description
directory string ./logs Destination directory for rotated log files
inFile bool true Write to file
inConsole bool true Echo to stdout
colorInConsole bool true ANSI colour codes on stdout. Disable when piping to a file or to journald
logLevel string DEBUG DEBUG | INFO | WARN | ERROR
processId string "" Process label added to every line; empty means no label
fileName string egprobe.log Base log filename; rotation appends suffixes
maxSize int (MB) 20 File size before rotation
maxBackups int 20 Number of rotated files kept
compress bool true gzip rotated backups
Tip

DEBUG is the shipped default and it is verbose enough to flood a journal on a busy probe. For production, INFO is the sane choice.

login

The single credential that protects the whole API except /auth/login.

Field Type Required Description
user string Yes API username
pass string Yes SHA256 hex digest of the password, not the password itself

Generate the digest before writing it:

echo -n "yourPassword" | sha256sum

At login, the probe hashes the submitted password and compares it against this value.

Change the shipped credential

The example file carries a placeholder digest so that a fresh installation can log in. It must be replaced before the probe is reachable by anything. Treat a deployment still carrying the example digest as unauthenticated.

influxdb

Connection to the metrics database (InfluxDB 2.x) that stores metrics, audit records, scan results and alarms.

Field Type Default Required Description
url string http://127.0.0.1:8086 Yes Metrics database endpoint
org string axiom Yes Organisation
token string โ€” Yes Token with read/write permission on the organisation. Prefer injecting via INFLUXDB_TOKEN
buckets []string see below No Buckets created at startup if they do not already exist

Default bucket set: network_bucket, ssh_bucket, metrics_bucket, usb_bucket, eg_alarms, audit_logs, networktraffic, availability, scanmetrics. The four per-channel event buckets are populated by the oda-lite collector, the rest by the probe itself โ€” see Metrics ingestion.

The probe starts even when the metrics database is unreachable, and every feature that depends on it is inoperative until it recovers. This is deliberate for edge deployments where the database may be a separate node that boots later.

alerts and audit

Block Field Type Default Description
alerts offset duration 10s Tolerance window applied to the alarm evaluation query range
alerts bucket string eg_alarms Bucket where alarms are written and read
audit bucket string audit_logs Bucket for audit events: logins, configuration changes, executions

Top-level keys

Field Type Default Required Description
apiPort string 8083 Yes HTTP service port. Binds on 0.0.0.0
outputParquetPath string ./ No Destination directory for on-demand Parquet exports
dbPath string "" No Local state database file. Empty resolves to ./db.dat, relative to the working directory
gojascriptsDir string "" No Directory holding rule scripts. Empty resolves to resources/gojascripts
gojaTimeout duration 8s No, but set it Maximum execution time for a rule script per event
Two settings worth pinning down

Always set dbPath to an absolute path in production (for example /var/lib/axiom-border/db.dat). Deployments that use per-version working directories will otherwise create a fresh, empty database on every upgrade and appear to have lost all state.

Always set gojaTimeout explicitly. Without a time limit, rule scripts do not run at all.

pagination

Controls the two read modes over the metrics database. Every field has a default, so the block can be omitted entirely.

Field Type Default Description
maxRecentEvents int 1000 Number of most recent events kept per stream, which is also the cap of the /recent/* feed
rangePageSize int 100 Default page size for range queries
rangeMaxPageSize int 500 Maximum accepted pageSize
rangeMaterializeMaxRows int 40000 Row threshold. Below it, the whole range is cached, which allows jumping to any page and reporting an exact total. Above it, results are delivered sequentially, page after page
rangeCacheTTL duration 60s Lifetime of a cached range entry
rangeCacheMaxEntries int 8 Maximum ranges cached at the same time. Beyond it, the least recently used range is discarded
rangeExportMaxRows int 500000 Row cap for the streaming CSV export. A range exceeding it returns HTTP 400 asking you to narrow the window

trainerDetails

HTTP client settings for the AI container. Only relevant on Linux with a container engine available โ€” see AI capabilities.

Field Type Default Description
serviceTlsCert string "" Client TLS certificate towards the AI container
serviceTlsKey string "" Matching private key
trainerContainerName string trainer Base container name. The effective name is <trainerContainerName>-<agent>

Three sub-blocks โ€” metrics, healthCheck and predict โ€” share the same four fields. The {port} literal in each URL is substituted at runtime with the port of the corresponding AI agent.

Sub-block url timeout retries timeBetweenRetries Purpose
metrics https://127.0.0.1:{port}/api/metrics 2s 1 2s Collect metrics from the AI container
healthCheck https://127.0.0.1:{port}/health 2s 20 5s Wait for container startup โ€” 20 attempts at 5 s gives roughly 100 s of margin
predict https://127.0.0.1:{port}/api/predict 10s 1 2s Inference, invoked from rule scripts
Note

Use whole seconds for timeBetweenRetries; fractions of a second are not honoured.

securityProbes

Security probes run two ways: automatically on an interval (schedule), and manually from the API or UI. Results are stored in local state and in the metrics database โ€” scanmetrics for aggregates, availability for up/down.

Probes ship disabled, and the first scan runs at startup

nmap, vulnScan and snmp all default to enabled: false. Enable them only after confirming their dependencies are present on the target host, because the first scan of an enabled probe runs as soon as the service starts, not after the schedule interval has elapsed. A probe enabled without its dependencies โ€” the nmap binary on PATH, a readable templates directory, a valid capture interface โ€” will log errors on every boot.

securityProbes.nmap

Host discovery, port scanning and optional fingerprinting. Requires the nmap binary on PATH.

Field Type Default Description
enabled bool false Enable the scheduled probe
schedule duration 2h Interval between sweeps. Zero or negative falls back to 2h
targets []string ["192.168.1.0/24"] Hosts and CIDR ranges to scan
portFilter string "" Port list such as "22,80,443". Empty means nmap’s top 1000
udp bool false Add a UDP scan. Slow
udpPorts string common UDP ports UDP ports probed when udp: true
timing string "" T1โ€“T5. T2 is cautious, T4 is reasonable on healthy networks
scanPorts bool true When false, ping scan only (-sn)
service bool false Service and version detection (-sV)
os bool false OS fingerprinting (-O). Implies a SYN scan, which needs raw sockets and therefore root
timeout duration 5m Abort if nmap does not finish

securityProbes.vulnScan

Template-based vulnerability scanning, including the embedded OT/ICS suite. The scan engine ships inside the product โ€” there is no scanner binary to install.

Field Type Default Description
enabled bool false Enable the scheduled probe
schedule duration 2h Sweep interval. Also re-triggered by event, with debounce, when new hosts are recorded
severity string (CSV) info,low,medium,high,critical Severity filter, given as one comma-separated string
level string profundo Depth, translated to template tags โ€” see below
templatesDir string "" Templates directory override for the scheduled probe
defaultTemplatesDir string ./vulnscan-templates Deployment-wide fallback
timeout duration 5m Scan cut-off. A request may override it
allowTemplateUpdates bool true Whether the probe may refresh templates from the network
allowTemplateBundleUpload bool true Whether an authenticated operator may upload a template set. Separate key on purpose โ€” see below
allowIntrusive bool false Master lock for OT/ICS intrusive mode
enableOT bool false Add read-only OT templates to the scheduled sweep
userAgent string "" HTTP User-Agent for the web checks. Empty keeps a neutral, randomised browser value per request; set it only when a deployment needs a deterministic one

level to tag mapping:

level value Tags applied
ligero, rapido, fast tech,ssl
medio, medium cve,misconfig,default-login,tech,ssl
profundo, full, deep cve,misconfig,default-login,exposure,network,ssl,tech,ot
ot, ics, industrial ot only
Use one of the listed level values

A level value not in the table above applies no tag filter at all, so the scan walks the entire template tree with only the severity filter. This is rarely what anyone intends and is dramatically slower. Check for typos.

Templates directory resolution order: the templatesDir field of the scan request, then securityProbes.vulnScan.templatesDir, then defaultTemplatesDir, then a vulnscan-templates directory next to the product binary. Relative paths resolve against the working directory.

allowTemplateUpdates behaviour:

  • true (default, including when the key is absent): best-effort refresh to the latest template release. Being offline or having GitHub blocked does not abort the scan โ€” local templates are kept. An empty directory triggers a full download.
  • false: strict offline kill-switch. The network is never touched, even when the directory is empty. If nothing is on disk the scan fails with a clear error.

allowTemplateBundleUpload is a different question, which is why it is a different key. allowTemplateUpdates asks may this process reach the network; this one asks may an authenticated operator replace the template set. The deployed recipe sets the first to false โ€” correctly, an OT segment has no route out โ€” so reusing it for uploads would have left the offline update path disabled on every real deployment, which is the one scenario the product exists for. Set this to false to freeze the templates at whatever the deployment installed; that is a posture, not the same decision as closing egress. See Offline maintenance.

The OT suite never depends on this

The OT/ICS templates ship inside the product and the bundled set is restored after every template update, so they survive a wipe-and-replace by the template manager and work with allowTemplateUpdates: false on an air-gapped network. See OT/ICS vulnerability scanning.

allowIntrusive governs the write/control layer of the OT suite over both REST and MQTT. A request asking for intrusive mode while this is false is rejected, and the execution ends as failed with an explicit message. The scheduled probe is never intrusive regardless of this flag.

enableOT adds the read-only ot tag to the scheduled sweep even when level would not include it. It never enables the intrusive layer. Some older PLCs are fragile in the face of unexpected connections; enable it only if the OT network tolerates periodic probing.

Recommended steady state for a probe on an industrial segment:

securityProbes:
  vulnScan:
    enabled: true
    level: "medio"
    enableOT: true            # continuous read-only OT visibility
    allowIntrusive: false     # write layer bolted shut
    allowTemplateUpdates: false   # air-gapped: never reach for the network
    defaultTemplatesDir: "/opt/axiom-border/vulnscan-templates"

securityProbes.snmp

Field Type Default Description
enabled bool false Enable the scheduled probe
schedule duration 2h Interval. Zero or negative falls back to 2h
targets []string ["192.168.1.0/24"] Hosts and CIDR ranges to interrogate
mibDir string MIBS/JSON-FORMAT MIB catalogue in JSON format
mib string synology Default MIB when no device match is found
port int 161 SNMP destination port
timeout duration 2m SNMP operation cut-off
oids []string sysDescr, sysName, sysObjectID OIDs fetched by GET
walkRoot string "" Root OID for the walk. Empty means sysDescr
workers int 32 Walk parallelism. Zero or negative becomes 1
Host pre-discovery has a fixed 30 s limit

This probe uses nmap for host pre-discovery, with a fixed 30 second limit. A /24 range at T2 timing will exhaust it. Narrow the range or raise the main nmap timing value.

With neither oids nor walkRoot set there is nothing for the probe to do.

securityProbes.sniffing

Continuous traffic capture with periodic persistence to the networktraffic bucket.

Field Type Default Description
interfaces []string [] Interfaces to capture. An empty list disables sniffing โ€” there is no separate enabled switch for this block
bpf string "" BPF filter. Empty captures all IPv4
promiscuous bool true Put the NIC in promiscuous mode
backend string pcap Capture mode. pcap is the supported value
duration duration 10m Default window for a manual capture
flushInterval duration 5m Persistence interval for continuous capture. Zero or negative becomes 1 minute
passiveVuln bool true in the deployed recipe Infer vulnerabilities from the passive identity (CPEโ†’CVE) after each window, emitting no traffic
passiveVulnDelay duration 5s Grace period after a flush, so the inventory settles before the inference reads it
passiveVulnRowLimit int 30 How many CVEs a device whose product is known but version is not may list one by one. Above the cap that device shows a summary with the count and severity breakdown instead. Version-confirmed findings are never summarised. A negative value removes the cap
cveFeedDir string "" Directory the local CVE database is seeded from at startup, offline. Empty disables seeding
ouiFile string "" IEEE OUI registry (oui.csv, verbatim) laid on top of the manufacturer table compiled into the binary. Its assignments win for the prefixes it carries; everything else keeps resolving from the built-in table, so empty, absent or unreadable is the pre-existing behaviour and cannot degrade anything. Refreshed in place with POST /security/oui-overlay
This table is not exhaustive

The sniffing block has grown several fields that are not listed here yet (enabled, filterTargets, interestNetworks, maxHostsPerFlush, snaplen). config/configuration_example.yaml is complete and is the reference until this page catches up.

Interface naming is platform-specific and is the most common source of a probe that captures nothing:

Platform Format Example
Linux Simple name eth0, enp3s0, wlan0
macOS BSD name en0
Windows Npcap NPF device path \\Device\\NPF_{GUID}

On Windows, the friendly name (Ethernet, Wi-Fi) does not work. Discover the NPF path with nmap --iflist and read the WINDEVICE column.

opengate

Optional cloud integration: inventory reporting (collect) and provisioning (provision). The whole section is inert when enabled: false.

Field Type Default Description
enabled bool false Master switch
apiKey string "" Secret. Sent as the X-ApiKey header over HTTP, and used as the default MQTT password when collect.mqtt.password is empty
cron string */30 * * * * Five-field cron expression for provision and collect. Descriptors such as @hourly are accepted. Empty or invalid means the integration does not run
minPeriod duration 30m Throttle. If the cron interval is shorter than this, the cron is ignored and a plain ticker at minPeriod is used instead
macDiscoveryTimeout duration 10s Limit for resolving the local host MAC

opengate.collect

Field Type Default Description
enabled bool false Enable collected-data reporting
mode string mqtt http or mqtt
urlTemplate string OpenGate south collect endpoint URL template; {{deviceID}} is substituted
deviceId string "" Force a fixed device ID. Empty derives one per host from IP and MAC
sendByParts bool false Split the payload into components: ports, SNMP, vulnerabilities
partSize.ports int 100 Port rows per chunk
partSize.snmp int 100 SNMP entries per chunk
partSize.vulnerabilities int 100 Vulnerabilities per chunk
retryCount int 3 Retries for the collect publish or POST
retrySleep duration 5s Wait between retries
mqtt.broker string "" OpenGate broker URL. Required for mode: mqtt; empty skips sending
mqtt.username string "" MQTT username
mqtt.password string "" Secret. Empty falls back to opengate.apiKey
mqtt.topic string "" Publish topic, accepts {{deviceID}}. Required for mode: mqtt
address.* string โ€” Asset location metadata: country, region, province, town, postal, address

opengate.provision

Field Type Default Description
enabled bool false Enable bulk provisioning
bulkUrlTemplate string OpenGate north bulk endpoint Template with {organizationName} and {provisionProcessorId} placeholders
searchUrl string OpenGate north bulk search endpoint Endpoint for querying bulk status
organizationName string "" Target OpenGate organisation. Needed when enabled
provisionProcessorId string "" Provision processor ID. Needed when enabled
retryCount int 3 Retries for the bulk file upload
retrySleep duration 5s Wait between retries
pollMaxAttempts int 10 Maximum bulk result polls
pollSleep duration 5s Wait between polls

mqtt

Three independent blocks: broker is the MQTT broker embedded in the product, client is the internal publisher of executions and alarms, and ops is the client that listens for OpenGate operations and answers them.

mqtt.broker

Field Type Default Description
enabled bool true Start the embedded broker
host string 0.0.0.0 TCP listener interface
port int 1883 MQTT TCP port
username string "" Broker authentication. Empty username and password together allow anonymous access
password string "" Secret
ws.enabled bool true Secondary WebSocket listener, used by the web UI
ws.host string 0.0.0.0 Not configurable โ€” the WebSocket listener always binds on all interfaces
ws.port int 1888 WebSocket port
ws.path string /ws WebSocket endpoint path. A leading slash is added if missing
ws.tls bool false TLS on the WebSocket listener
tls.enabled bool false TLS on the main MQTT listener
tls.host string "" TLS listener interface. Empty inherits broker.host
tls.port int 8883 MQTT-over-TLS port
tls.verify bool false Verify client certificates (mTLS)
tls.caFile string "" CA used to validate clients. Needed when verify is on
tls.certFile string "" Server certificate. Needed when TLS is enabled
tls.keyFile string "" Server private key. Needed when TLS is enabled
Anonymous by default

With username and password both empty the embedded broker accepts anonymous connections. On any network you do not fully control, set credentials and enable TLS. If you do enable TLS, supply a complete and valid certificate set: an incomplete TLS block prevents the broker from starting at all.

mqtt.client

Field Type Default Description
enabled bool true Start the internal publisher
broker string tcp://127.0.0.1:1883 Broker URL, by default the embedded one. Empty disables the client with a warning
clientId string "" Empty generates a unique client ID automatically
username string "" Only sent when non-empty
password string "" Secret, only sent when non-empty
topic string axiom-border/executions Execution-event publish topic. Empty publishes nothing
qos int 1 Publish QoS
retain bool true Retain flag on published messages

Auto-reconnect is always on, retrying every 5 seconds, with a 10 second initial connection timeout. Neither is configurable.

mqtt.ops

Field Type Default Description
enabled bool true Start the operations listener
broker string tcp://127.0.0.1:1883 Broker to subscribe against
username string "" MQTT username
password string "" Secret
topicSubscribe string odm/operation Incoming OpenGate operations topic
topicPublish string odm/response/{device-id} Response topic; {device-id} is substituted at runtime
qos int 1 QoS for both subscribe and publish
retain bool false Retain flag on responses

If broker, topicSubscribe or topicPublish is empty, the operations module does not start.

Metrics Ingestion and Audit

Metrics ingestion and audit

Axiom Border’s data comes from two origins. The first one the probe captures by itself: the traffic it sniffs and the scheduled scans it runs against the network around it, with no cooperation from the equipment it watches. The second one is reported to it: every monitored machine runs an oda-lite agent that watches the machine from the inside and sends what it sees.

flowchart TB
    NET["The network around<br>the probe"]:::ext
    MACH["The monitored machines<br><i>an oda-lite agent on each</i>"]:::ext
    NET -->|"sniffing and<br>scheduled scans"| P["<b>Axiom Border</b>"]
    MACH -->|"SSH ยท USB ยท interface<br>ยท health events"| P
    P --> ST[("Metrics store<br><i>history and exports</i>")]
    P --> LP["Live pipeline<br><i>rules ยท alarms ยท audit</i>"]
    classDef ext fill:#e9edfa,stroke:#486ac9,color:#101010

Whatever the origin, the data ends in the same two places: the metrics store, which keeps the history behind queries and exports, and the live pipeline, where rules are evaluated, alarms are raised and the audit trail is written. This page follows that flow โ€” the two origins first, then the pipeline, then how to read everything back.

What the probe captures by itself

The network-facing half needs nothing installed on the monitored equipment:

  • Traffic capture โ€” continuous sniffing of the configured interfaces, persisted periodically to the networktraffic bucket. Configured under securityProbes.sniffing.
  • Scheduled assessments โ€” the discovery, vulnerability and SNMP probes run on their configured intervals, keep the asset inventory current, and record per-scan aggregates (scanmetrics) and host up/down state (availability).

Both are documented in Security assessment. What matters on this page is that their results are data like any other: they land in the store, their executions are audited, and the audit log’s agent filter accepts the probe names (network, vulns, snmp, sniff) alongside the agent channels.

What the agents report

The host-facing half is delivered by oda-lite, a lightweight monitoring agent that ships alongside Axiom Border as a separate component with its own release cycle โ€” the version you have is the one your package includes. It is the whole of the monitoring deployment role: a machine with that role runs only the agent, reporting to a central probe. The probe’s own machine runs one too, so the central host is watched exactly like every other node.

An agent watches four things, and each maps to a channel โ€” the “Guards” of the console views:

Channel In the console What arrives
ssh SSH-Guard SSH activity: connections, successful and failed logins, logouts, per-session byte counters โ€” tagged with user, ip and port
iface IFACE-Guard Network interfaces appearing, disappearing or changing state, with the interface name, type and MAC address
usb USB-Guard USB devices connected and disconnected, with device names, ID and manufacturer
metrics METRIC-Guard The node’s reporting-health heartbeat: ram_usage, prepared_vars and sent_vars. Its silence is what a node stopped reporting alarm keys on

Those four names are what the API calls {agentType}: they appear in the read paths (/recent/{agentType}), in the audit log’s agent filter, and as the group routing tag on every event.

flowchart TB
    AG["oda-lite agents<br>on each machine"]:::ext
    OTH["Other reporters<br><i>optional</i>"]:::ext
    AG -->|":9090/agents<br>line protocol"| COL["oda-lite on the probe host<br><i>central profile</i>"]
    OTH -->|":9091/metrics<br>JSON"| COL
    COL -->|"one bucket<br>per channel"| ST[("Metrics store")]
    COL -->|"POST /telegraf"| LP["Live pipeline<br>rules ยท alarms ยท audit"]
    classDef ext fill:#e9edfa,stroke:#486ac9,color:#101010

On the probe host, oda-lite runs in its central profile: besides watching its own host, it listens for everyone else on two ports โ€”

Listener Format Who posts there
:9090/agents InfluxDB line protocol The oda-lite agents on the monitored machines
:9091/metrics JSON Other reporting processes. A metric arriving without a channel tag that carries the reporting-health fields is routed to the metrics channel automatically

Everything it receives โ€” its own guards’ events included โ€” goes two ways at once: each channel is written to its bucket in the metrics store, and every event is forwarded to the probe’s live pipeline, so rules, alarms, audit and the recent feeds see it immediately.

Two operational handles worth knowing:

  • The agent is a systemd service on every node: systemctl status oda-lite. Its configuration is generated by the installer at /etc/oda-lite/oda-lite.conf โ€” this is also where the listener ports move if 9090 or 9091 collide with something else.
  • Agents buffer briefly and flush every few seconds, so an event appears in the console with at most a few seconds of delay โ€” instantly is the wrong expectation, but a minute is a problem.
SSH events need verbose sshd logging

The SSH monitor reads authentication logs, which means sshd must log at LogLevel VERBOSE and rsyslog must be populating /var/log/auth.log. The installer configures both for the roles that need them. On a host where SSH events never appear, check those two things first.

Seeing what is reporting in

Supervisions in the console lists the channels the probe is receiving data from. Each card is one channel, with a status dot, the last event it delivered, and the nodes it is monitoring.

The Supervisions view, with one card per channel: the interface, SSH and USB monitors reporting in, and the metrics one with no data

This is the first place to look when data is not arriving: a channel with no recent event means the agent has either stopped, or never reached the probe. In the capture above METRIC-Guard is in exactly that state โ€” there is no data to display. All events opens the full history for that channel.

The same data, turned around โ€” Hosts Status

Supervisions answers which channels are reporting. Hosts Status answers how is each machine doing, which is the same data indexed the other way: one block per node, a status dot per monitor, the last event each one delivered, and the alarms currently open against that node.

Hosts Status โ€” per-node monitors and the alarms open against each node

The value of this view is that a problem is visible two ways at once. In the capture above the METRIC-Guard dot is red on all three nodes, and the alarm table beside it reads Node stopped reporting โ€” the missing telemetry and the alarm it raised, side by side. That is the same silence the Supervisions view shows as an empty channel, seen from the node’s point of view instead of the channel’s.

Use Supervisions when you are asking whether a monitor is working, and Hosts Status when you are asking whether a machine is healthy.

The live pipeline: rules, alarms and audit

Events reach the probe itself through a single door, POST /telegraf โ€” the same endpoint the collector forwards into.

The ingestion door is not open to the network

Reporting processes are unattended, so they cannot carry a session the way an operator does. Rather than leaving the endpoint public, the probe waives the token only for callers inside a configured range, which defaults to loopback. Everything else must authenticate like any other request.

Widen that range only as far as the machines that genuinely report in. Anyone who can post here can inject measurements โ€” and therefore trigger rules and raise alarms.

Payload format

The body is {"metrics": [...]} and every metric requires all four fields:

Field Type Meaning
name string Becomes the measurement in the store. In practice the machine ID of the reporting node
timestamp int64 Epoch in seconds, not milliseconds
tags map of string Must include group, which routes the metric to a channel
fields map The event’s values, numeric or string

The group tag is the routing key:

group value Channel History bucket
SSH ssh ssh_bucket
USBS usb usb_bucket
IFACES iface network_bucket
METRICS metrics metrics_bucket
SECSCAN Routed by the additional probe tag scanmetrics

Beyond group, the conventional tags are eventType, ip, port and user โ€” these are what rules and queries filter on.

curl -sk -X POST https://192.168.1.10:8083/telegraf \
  -H 'Content-Type: application/json' \
  -d '{
    "metrics": [
      {
        "name": "<machine-id>",
        "timestamp": 1746535617,
        "tags": {
          "group": "SSH",
          "eventType": "conn",
          "ip": "192.168.1.80",
          "port": "56390",
          "user": "unknown"
        },
        "fields": { "authevent": 1 }
      },
      {
        "name": "<machine-id>",
        "timestamp": 1746535623,
        "tags": {
          "group": "SSH",
          "eventType": "login_attempt_fail",
          "ip": "192.168.1.80",
          "port": "56390",
          "user": "operator"
        },
        "fields": { "authevent": 1 }
      }
    ]
  }'

The response is 200 with an empty body, or 400 if the JSON does not parse.

A 200 does not mean the rules ran

The recent feeds are updated immediately, so a /recent/* query straight after ingestion will show the event. Rule evaluation, auditing and alarm generation are asynchronous, so the 200 confirms acceptance, not evaluation. If you are testing a rule, allow a moment and check the alarms feed rather than inferring from the ingestion response.

This endpoint feeds the pipeline, not the history

POST /telegraf drives the recent feeds, the rules engine and the audit trail โ€” it does not write the event itself to the metrics store. History is written by the collector. An event posted directly here can raise alarms and will show in the recent feeds, but it leaves no history and will not appear in range queries or exports. To feed your own data in fully, post to the collector’s listeners โ€” :9090/agents in line protocol or :9091/metrics in JSON โ€” and let it fan out to both places.

Reading data back

Everything up to here is visible in the console. What follows is not: pushing data in and pulling data out are the two jobs the console does not cover, because they exist to feed something else โ€” a dashboard, an export, an integration. This is the part of the product where the API is the right tool rather than a shortcut.

There are three ways to read, and choosing the right one is the difference between a responsive UI and a slow one.

Recent feeds โ€” for live views

These answer in microseconds, serving the most recent events the probe already holds without running a historical query. Feed depth is capped by pagination.maxRecentEvents, default 1000.

curl -sk -X POST https://192.168.1.10:8083/recent/ssh \
  -H "Authorization: Bearer <jwt-token>" \
  -H 'Content-Type: application/json' \
  -d '{"limit": 50, "orderBy": "desc"}'
{
  "count": 50,
  "warming": false,
  "elements": [ { "_measurement": "<machine-id>", "_time": "2026-05-06T13:27:03.000Z", "eventType": "login_attempt_fail", "user": "operator", "authevent": 1 } ]
}

Optional request fields: limit, uuids to filter by node, and orderBy for presentation order. Note that orderBy only affects how the returned page is sorted โ€” the feed always yields the most recent events, so asc does not page backwards through history.

The warming flag is true while the feed is still being seeded after a restart. Surface it in a UI as a loading state rather than presenting a partial feed as complete.

Three feeds exist: /recent/{agentType} for events, /recent/alarms for alarms, and /recent/auditlog for audit entries. The audit variant takes timeOrder instead of orderBy and adds eventsId and agent filters โ€” the latter also accepting the probe names network, vulns, snmp and sniff.

There is no total or pages in the response. The client paginates locally over what it received.

Paginated ranges โ€” for historical queries

curl -sk -X POST https://192.168.1.10:8083/datapagination/ssh \
  -H "Authorization: Bearer <jwt-token>" \
  -H 'Content-Type: application/json' \
  -d '{
        "from": "2026-08-01T00:00:00Z",
        "to": "2026-08-03T00:00:00Z",
        "page": 1,
        "pageSize": 100,
        "orderBy": "desc"
      }'

A range read is answered in one of two ways, and the response metadata tells you which one you got:

meta field Meaning
randomAccess true: page numbers work, so you can jump to any page. false: follow nextCursor instead
exact true: total and pages are exact. false: total is an upper bound

Narrow ranges answer with full random access. Wide ones switch to sequential reading, and nextCursor carries the position of the following page.

{
  "elements": [ "..." ],
  "meta": {
    "total": 3412, "pages": 35, "page": 1, "pageSize": 100,
    "randomAccess": true, "exact": true,
    "range": { "from": "2026-08-01T00:00:00Z", "to": "2026-08-03T00:00:00Z" }
  }
}

To continue a sequential read, send only the cursor field โ€” no from, no to, no page.

Read randomAccess before drawing a pager

When randomAccess is false, page numbers are meaningless and total is an upper bound, not a count. A UI that renders “page 12 of 340” from that metadata will be wrong. Read meta.randomAccess on every response and switch between a numbered pager and a “load more” control accordingly.

Pagination limits are configurable โ€” see Configuration.

Exports โ€” for taking data elsewhere

Streaming CSV, which is the one to use:

curl -sk -X POST https://192.168.1.10:8083/data/ssh \
  -H "Authorization: Bearer <jwt-token>" \
  -H 'Content-Type: application/json' \
  -d '{
        "uuids": ["<machine-id>"],
        "orderby": "desc",
        "range": { "initTime": "2026-08-01T00:00:00Z", "finishTime": "2026-08-03T00:00:00Z" }
      }' -o export.csv

The range is mandatory here, and a range exceeding pagination.rangeExportMaxRows (default 500 000) is rejected with 400 asking you to narrow it. That guard is deliberate.

Parquet, which behaves differently:

curl -sk -X POST https://192.168.1.10:8083/parquet/ssh \
  -H "Authorization: Bearer <jwt-token>" \
  -H 'Content-Type: application/json' \
  -d '{"uuids": ["<machine-id>"]}'
Parquet export writes server-side and overwrites

This endpoint returns 200 with an empty body โ€” it does not return the file. It writes to a fixed path, <outputParquetPath>/influxquery.parquet, so every call overwrites the previous one. Unlike the CSV export, the range is optional and no row-count limit applies, so a broad query can produce a very large export. Always pass a range on a populated bucket, and collect the file from the server before the next call.

Other read helpers

Endpoint Purpose
GET /lastevent/{agentType} Latest event per device as semicolon-separated CSV. Sets X-Recent-Warming: true while the feed is still seeding
GET /uuids/{agentType} Node UUIDs present in one channel’s bucket
GET /uuids Map of UUID to the channels it appears in

Aliases โ€” making UUIDs readable

Nodes are identified by machine ID, which is unreadable. An alias maps one to a name, and every read endpoint accepts ?alias=true to perform the translation. The console displays aliases wherever it lists nodes, but creating them is an API operation:

curl -sk -X POST https://192.168.1.10:8083/alias \
  -H "Authorization: Bearer <jwt-token>" \
  -H 'Content-Type: application/json' \
  -d '{"alias": "plc-line-1", "uuid": "<machine-id>"}'

With ?alias=true, the uuids filter in a request body also accepts alias names, so a client can work entirely in readable names. Note that DELETE /alias takes the alias name, as {"name": "..."}, not the UUID.

Audit

Every significant action is audited to the audit_logs bucket. Two ways to read it:

# Live tail
curl -sk -X POST https://192.168.1.10:8083/recent/auditlog \
  -H "Authorization: Bearer <jwt-token>" \
  -H 'Content-Type: application/json' \
  -d '{"limit": 100, "timeOrder": "desc"}'

# Historical, filtered to vulnerability scans
curl -sk -X POST https://192.168.1.10:8083/auditlog \
  -H "Authorization: Bearer <jwt-token>" \
  -H 'Content-Type: application/json' \
  -d '{
        "from": "2026-08-01T00:00:00Z",
        "to": "2026-08-03T00:00:00Z",
        "agent": "vulns",
        "pageSize": 100
      }'

The agent filter accepts the four channel names and the four probe names. Filtering by vulns is how you answer “what was scanned, when, and by whom” โ€” including the warning entry that every intrusive OT scan generates.

Auditing follows the rule script

An agent’s or probe’s activity is audited when its rule script is enabled. Disabling a script therefore costs you the audit records for that agent or probe as well as its rules โ€” the events are still ingested and stored, but they leave no audit trail.

If you need an audit trail for a particular agent, check the enabled state with GET /scripts and make sure its script exists and is enabled, even if the script itself does nothing. See Alarms and the rules engine.

Storage layout

Where each kind of data ends up. The four channel buckets are written by the collector; everything else is written by the probe itself:

Bucket Contents
ssh_bucket, usb_bucket, network_bucket, metrics_bucket Per-channel agent events
eg_alarms Raised alarms
audit_logs Audit trail
networktraffic Passive sniffing flow data
availability Host up/down state from the probes
scanmetrics Aggregated scan metrics

Buckets are created on first start if absent. Note that no retention policy is applied by default โ€” configure retention in InfluxDB according to your disk budget, or the metrics buckets will grow without bound. An Influx bucket with no retention is a common cause of a probe slowly filling its disk.

Alarms and the Rules Engine

Alarms and the rules engine

Axiom Border decides locally. A probe that only forwards data is useless the moment the uplink drops, so alarm evaluation runs on the device, against data it already holds, with no dependency on the platform.

There are two ways to express what should raise an alarm:

Mechanism Use it for Defined via
Checks โ€” declarative rules Thresholds, event matches, dead-machine detection /checks API
Scripts โ€” rules written in JavaScript Anything with logic: correlation, state, arithmetic, calls to the AI model /scripts API

Checks cover most of what monitoring needs and need no code. Scripts exist for the cases checks cannot express.

Where you work with alarms

Alerts in the console is the operator’s view: the alarms currently raised, the history of what has been logged, and the Rules configuration button that opens the editor for the rules described below. An autoreload toggle keeps the list current while you watch an incident develop.

The Alerts view listing raised alarms with their severity and originating rule

Each row names the alarm, the check type behind it, the node it came from, the agent, the severity and a description that includes when the condition was first seen. ACK acknowledges the selected alarm.

Two tabs sit above the table. Recent reads the live feed โ€” the most recent alarms the probe already holds, answered immediately. Range queries a time window from history instead, which is what you want when reconstructing an incident after the fact:

The Range tab, querying alarm history over a time window

Rules configuration opens the Alarm config screen, which is where both kinds of rule live โ€” the declarative alarm rules in the upper table, and the JavaScript Expert System at the bottom with its own enable switch:

The Alarm config screen, listing alarm rules and the Expert System section

Everything on this page can be done from that screen or from the API, and both paths are shown together wherever they differ.

Checks โ€” declarative rules

A check watches one event type from one agent over a time window, and raises an alarm when its condition holds.

flowchart TB
    EV["Event arrives"] --> MATCH{"Matches<br>a check?"}
    MATCH -->|no| DROP["Stored only"]
    MATCH -->|yes| TYPE{"Check type"}
    TYPE -->|count| CNT{"Count over<br>threshold?"}
    TYPE -->|deadmachine| SILENT{"Silent for<br>the window?"}
    CNT -->|no| WAIT["Keep counting"]
    CNT -->|yes| FIRE["Raise alarm"]
    SILENT -->|yes| FIRE
    FIRE --> COAL{"Alarm<br>already open?"}
    COAL -->|yes| INC["Increment count"]
    COAL -->|no| NEW["New alarm"]

The two check types

ctype Fires when threshold
count The event occurs at least threshold times within freqs Required
deadmachine No events arrive from the node within freqs Not used, send 0

deadmachine is the one worth calling out: it alarms on absence, which is how you detect a node that stopped reporting rather than a node reporting something bad.

Alarming on every occurrence

To raise an alarm whenever an event happens at all, use a count check with "threshold": 1 and a short freqs. The first matching event trips the condition immediately.

Creating a check

From Alerts โ†’ Rules configuration, use New alarm rule. The wizard walks three steps โ€” Rule config, Alarm config and Summary โ€” and collects exactly the fields described above:

Step one of the new alarm rule wizard

Rule type is the check type, Agent and event choose what to watch, Trigger sets the window and the count, and UUIDs selects the nodes. Only agents actually reporting in are offered, which is why a rule cannot be created before its agent has sent data.

curl -sk -X POST https://192.168.1.10:8083/checks \
  -H "Authorization: Bearer <jwt-token>" \
  -H 'Content-Type: application/json' \
  -d '{
        "checkName": "ssh-bruteforce",
        "agent": "ssh",
        "ctype": "count",
        "event": "login_attempt_fail",
        "freqs": "5m",
        "threshold": 10,
        "level": "critical",
        "alarmName": "SSH brute force attempt",
        "alarmDescription": "More than 10 failed SSH logins in 5 minutes",
        "uuids": ["<machine-id>"],
        "enable": true
      }'

Every field is required on create. freqs is a duration string โ€” 30s, 5m, 1h. level is info, low, medium, high or critical. uuids lists the nodes the check applies to, and accepts alias names when the request carries ?alias=true.

A worked example: catching a node that went quiet

The most valuable check is usually the one that alarms on silence. In the wizard, choose dead machine as the rule type, pick the agent that reports the node’s health, set the window to something comfortably longer than its reporting interval โ€” ten minutes against a one-minute heartbeat โ€” and select the nodes it applies to.

There is no threshold to set: the condition is that nothing arrived. Give the alarm a name an operator will understand at three in the morning, such as Node stopped reporting.

Managing checks

The rules table on Alerts โ†’ Rules configuration lists every check, and each row carries its own edit and delete actions. Editing reopens the same wizard.

Three things cannot be changed after creation

Editing a check keeps its rule type, its agent and its event. To change what a check watches, or how it evaluates, delete it and create it again. Everything else โ€” threshold, window, severity, alarm text, nodes, whether it is enabled โ€” edits normally.

To pause a check without losing its definition, switch it off rather than deleting it.

Alarm coalescing

A brute-force attempt that trips a threshold every five minutes for an hour should be one alarm with a count, not twelve identical alarms. Axiom Border coalesces repeated firings of the same rule against the same node into a single open alarm:

Field Meaning
firstSeen When the condition first held
lastSeen The most recent firing
count How many times it has fired

The alarm stays open and accumulating until someone acknowledges it. Acknowledgement is what closes the coalescing window โ€” the next occurrence after an acknowledgement opens a fresh alarm, so the operator gets a new signal rather than a silent increment on something they already dealt with.

Acknowledging

Select the alarm on the Alerts view and use ACK. It is marked as seen, with who acknowledged it and when.

If someone else got there first, the action is refused rather than applied twice โ€” worth knowing when two operators are working the same incident.

Reading alarms

The two tabs above the table reach the same events by different routes:

Tab Reads Use it for
Recent The probe’s live in-memory feed Watching an incident unfold. Answers instantly, however much history the probe holds
Range The stored history over a time window Reconstructing what happened after the fact
The Range tab, querying alarm history over a time window

Columns filter individually, and the autoreload control keeps the list current while you watch.

Each alarm carries its name and description, the check type behind it, the node and agent it came from, its severity, when it was raised, whether it has been acknowledged, and the coalescing trio of first seen, last seen and count.

A small tolerance window absorbs clock skew

Evaluation applies a short grace period โ€” ten seconds by default โ€” around each window boundary, so an event landing milliseconds outside one is still counted. Without it, a node whose clock drifts slightly would silently miss conditions it genuinely met.

Rule scripts

For logic that a declarative check cannot express, Axiom Border runs rule scripts written in JavaScript. They are evaluated on the device, server-side, on events you did not trigger.

In the console these are the Expert System, edited from Alerts โ†’ Rules configuration โ†’ Edit script. The editor opens on a working skeleton, so you can see the shape a script must have before writing one:

The Expert System script editor, showing the process(metrics) skeleton

Note the Enable switch beside the editor on the Alarm config screen: a saved script does nothing until it is turned on.

How scripts are organised

A script is identified by name, and the name determines when it runs. Scripts named after an agent โ€” ssh, iface, usb, metrics โ€” run on events for that agent. Scripts named after a probe โ€” network, vulns, snmp, sniff โ€” run on that probe’s findings. A general-purpose rulesengine script runs over every ingested batch.

Every script has an enabled state, set with the ?enabled= query parameter when you upload it and reported by GET /scripts. A disabled script is stored but never invoked.

The script contract

A script must define a function called process, which receives the batch of metrics:

function process(metrics) {
  for (var i = 0; i < metrics.length; i++) {
    var metric = metrics[i];
    metric.SetT0();
    // your logic here
    metric.SetT1();
  }
}
metrics is not a real JavaScript array

Iterate it by index with .length, as above. .filter(), .map() and .forEach() are not available and will throw. This is the single most common mistake when writing a first rule.

SetT0() and SetT1() bracket your processing and are what populate the timing fields in the audit record. They are not required for the rule to work, but including them is the convention and it makes slow rules visible.

Available globals

Function Purpose
newAlarm(metric, criticality, description, alarmName, eventType) Raise an alarm
newPredict(payloadJSON, agentType) Call the AI model for inference
newGetMetrics(agentType) Fetch the AI model’s own metrics
filterByIp(metrics, ip) Subset of the batch matching an IP
filterByProbe(metrics, probe) Subset matching a probe name
distinctIps(metrics) Distinct IPs, in first-appearance order
print(...) Write to the log at debug level

Each metric exposes: GetTagByName(key), GetFieldByName(key), ExistsField(key), GetMetricName(), GetAliasName(), GetAgentType(), GetMonitoredEvents(), GetEventValues(), GetTime(), SetT0(), SetT1() and String().

A worked example, raising an alarm when a scan finds a risky port open:

var RISKY_PORTS = { 23: "telnet", 21: "ftp", 3389: "rdp", 445: "smb", 5900: "vnc" };

function process(metrics) {
  for (var i = 0; i < metrics.length; i++) {
    var m = metrics[i];
    m.SetT0();

    if (m.GetTagByName("kind") === "port" && m.GetFieldByName("portStatus") === "open") {
      var port = m.GetFieldByName("portNumber");
      if (RISKY_PORTS[port]) {
        newAlarm(m, "critical",
          "Risky service " + RISKY_PORTS[port] + " exposed on port " + port,
          "Risky port open", "port_discovery");
      }
    }

    m.SetT1();
  }
}

The tags and fields available per probe are documented in the header comment of each deployed script, so read the script with GET /scripts/{scriptname} before writing rules against it.

What probe rules can react to

Probe rules react to appearances and changes: a new host, a new open port, a changed SNMP value. To alarm on a node that stops responding, use a deadmachine check against that node’s agent data instead โ€” that is exactly what checks on absence are for.

Saving and enabling a script

Everything happens in the Expert System editor on the Alarm config screen: write or paste the script, save it, and use the Enable switch beside it.

A saved script does nothing until it is enabled

Saving and enabling are two separate actions, and the most common report of “my rule never fires” is a saved script with the switch still off. The editor shows the switch right next to it for exactly this reason.

Scripts can also be managed through the API โ€” /scripts lists them with their enabled state, and each one can be read, replaced or deleted by name. That is what an automated deployment would use; from a browser the editor is the shorter path.

Always set a script timeout

Each script invocation is bounded by gojaTimeout. Define it explicitly: without a value the limit is zero and no script runs at all, which shows up as missing alarms rather than as an error. The shipped value is 8s.

gojaTimeout: "8s"

Scripts can call the AI model

A script can invoke inference against a deployed AI capability, using the predict endpoint configured under trainerDetails.predict. This is what connects anomaly detection to alarm generation: the model scores an event, and the script decides whether that score warrants an alarm.

Timeouts and retries for that call come from trainerDetails.predict โ€” default 10 s, one retry. Note that the call is bounded by gojaTimeout as well, so a predict timeout longer than the script timeout cannot complete. Keep gojaTimeout comfortably above the predict timeout if your rules use inference.

See AI capabilities for deploying a model in the first place.

Security probe results reach the rules engine too

Security probe findings are evaluated by the rules engine and audited exactly like agent metrics. A scan finding can therefore raise an alarm the same way a metric can โ€” a newly discovered host, a critical vulnerability, a port that opened when it should not have. See Security probes.

Choosing between a check and a script

Reach for a check when the condition is “this event, this many times, this window”. It is declarative, visible in the API, and cannot fail in interesting ways.

Reach for a script when you need to remember something between events, combine two signals, compute a value, or ask the model. The cost is that scripts are code: they need a timeout that works, they fail in ways checks do not, and they are harder to audit at a glance.

If a check can express it, use the check.

Where alarms go next

Locally, alarms land in eg_alarms and surface through the feeds above and in the web console.

If OpenGate integration is enabled, alarms and executions also publish over the embedded MQTT broker and are forwarded to the platform, which is how a fleet of probes becomes a single operational picture. See MQTT and OpenGate operations.

Security Probes

Security probes

Axiom Border assesses the network it sits on with four probes. They share one execution model, one audit trail and one accumulated view of results, so the findings of each one reinforce the others instead of living in separate reports.

Probe What it answers Requires
Network discovery What is out there, and what is it listening on? Nothing beyond a target range
Vulnerability scanning Which of those services are vulnerable? Nothing โ€” the checks ship with the product
SNMP What does the device say about itself? A reachable SNMP agent and credentials
Passive analysis What is actually crossing the wire? A capture interface

Each runs two ways: automatically on a schedule you configure, and manually from the console when you want an answer now.

flowchart TB
    DISC["Network discovery<br><i>live hosts and ports</i>"]
    DISC --> VULN["Vulnerabilities"]
    DISC --> SNMP["SNMP identity"]
    PASS["Passive analysis"] --> INV
    VULN --> INV
    SNMP --> INV
    DISC --> INV[("Consolidated<br>inventory")]
    INV --> ALARM["Alarms and<br>audit trail"]

Network discovery establishes the baseline for the two probes that ask questions, and that ordering matters: a vulnerability scan with no explicit target scans every host in the baseline, so discovery must have run for it to have anything to do. Newly discovered hosts also trigger a vulnerability scan on their own, grouped together after a short delay, so new assets get assessed without waiting for the next scheduled sweep.

Passive analysis stands apart โ€” it needs no baseline, because it is not asking anyone anything. See Passive and active for why the two must not be read as the same kind of result.

Network status โ€” where all of it lands

Network status, listing discovered hosts with their type, manufacturer, ports and vulnerabilities

The header names the last assessment that ran and how it finished, so you can tell at a glance whether what you are looking at is current. Three buttons launch and configure the probes:

Button What it does
Scan network Runs a discovery scan now, against a target you choose
Passive analysis Opens what the listening probe has worked out, with its evidence
Automatic scans Jumps to the schedules for all of them, in Configuration

Each host row carries its own actions โ€” Ports, Vulnerabilities and SNMP โ€” each opening that host’s detail and each able to re-scan just that host, so you can reassess one asset without sweeping the whole range.

Network discovery and port scanning

Scan network opens the launch dialog. Every option the probe supports is here, so there is no reason to drop to the API for a scan:

The Launch network scan dialog
Field What to know
Target Whole network uses the configured range; Custom IP scans a single address
Ports Leave empty and it scans all 65,535 per host. On a /24 at cautious timing, that is a long scan โ€” narrow it
Timing Five presets from cautious to flat out, each with a one-line description of what it trades away
Timeout Raise it whenever you widen the target or the port range
Scan ports ยท UDP ยท Service detection ยท OS detection UDP is slow โ€” restrict the port list rather than sweeping. OS detection needs the probe to run privileged
A port list worth copying for industrial networks

1-1024,502,2404,20000,47808 covers the common IT range plus the Modbus, IEC-104, DNP3 and BACnet ports, so industrial assets show up in the baseline the vulnerability probe later works from.

One discovery scan at a time

Launching a second while one is running is refused. Forcing it cancels the running scan and marks it failed with the reason, rather than letting it look as though it completed.

What discovery gives you per host

A host’s Ports panel lists what it is listening on, with the service the scan identified and when each port was first and last seen:

The Ports panel for a single host

The first-seen and last-seen pair is what makes this more than a snapshot: a port with a recent first seen is new, and that is usually the thing worth acting on. The panel re-scans just this host, so you can confirm a change without sweeping the range again.

Scheduled discovery is configured under Configuration โ†’ Automatic scans, in the nmap block.

Vulnerability scanning

Two distinct halves. The generic half uses the bundled upstream template set. The OT/ICS half uses Axiom Border’s own suite of 57 industrial protocol checks, which travel inside the product and work with no internet access. Both are covered in Catalogs: vulnerabilities, OIDs, MIBs and more.

Open a host’s Vulnerabilities panel and use Re-scan IP address vulnerabilities:

The vulnerability re-scan dialog, with severity filters, depth and the intrusive switch

Depth is the setting that matters most, and the dialog explains each option as you select it:

Depth What runs
Light Technology detection and TLS only. Checks no CVEs
Medium Adds known CVEs, misconfigurations and default credentials
Deep All of the above, plus exposures, network services and the OT/ICS suite
OT/ICS (industrial) The industrial suite on its own โ€” Modbus, IEC-104, DNP3, BACnet, OPC UA

Selection is by check family rather than by folder, so it does not depend on how the template set is laid out on disk.

Severity toggles which findings are reported at all, and Intrusive mode (OT/ICS) is the switch that adds checks which write to the device rather than only reading it.

Intrusive mode needs two locks released, not one

The switch in this dialog is only one of two locks. The probe must also be configured to permit intrusive scanning at all, and that setting is off on every installation.

With it off, the scan is not quietly downgraded to a read-only run โ€” it is accepted and then ends as failed, and nothing is scanned at all. That matters more than it sounds: a request for intrusive mode you were not authorised to make does not come back as a clean read-only report, it comes back as no report.

The Network status header names the reason, so you are not left looking at a scan that did nothing and said nothing. It is recorded with the execution and in the service log as well.

Read OT/ICS vulnerability scanning before enabling anything against a live plant.

Reading a finding

The Vulnerabilities panel for one host

Name says what was confirmed โ€” Modbus/TCP Diagnostics Function Exposed Without Authentication โ€” and Description explains how it was confirmed and why it matters. CVE is empty whenever the finding is an exposure rather than a published vulnerability, so Template ID is the stable way to refer to one.

Origin is the column to read first: probe means the request was sent and the target answered, while inferred means it was derived from the device’s identity with no traffic emitted โ€” a lead to verify, not a demonstrated fact. Confidence qualifies it further, and where an inference has evidence behind it, the Evidence toggle in that column opens it.

Checks from the write/control layer are marked twice over: the name ends in [INTRUSIVE] and the description opens with INTRUSIVE / GATED.

Detected, and what is merely potential

The table is titled Detected vulnerabilities, and the word is doing real work. It holds what was confirmed by an active scan, plus inferences where the product was actually identified. What it does not hold is the surface attributable to a device known only down to its manufacturer โ€” that would be hundreds of rows the probe cannot stand behind, burying the findings you can act on.

That surface is not discarded. It is summarised above the table, as a count with its severity breakdown and a recommendation to scan the host actively.

An amber Vulnerabilities button means look inside

On Network status, a host whose potential surface is being summarised rather than listed shows its Vulnerabilities row action in amber. Without that cue a host with a hundred unconfirmed CVEs behind it reads exactly like a clean one from the collapsed row โ€” which is the one thing an assessment tool must never do.

There is a cap on how many unconfirmed CVEs a single device lists individually before it switches to a summary โ€” 30 by default. Version-confirmed findings are never summarised, however many there are. The cap is sniffing.passiveVulnRowLimit in Configuration; a negative value removes it and lists everything.

SNMP โ€” interrogation and profiles

Open a host’s SNMP action to query it. The panel takes symbolic names as readily as numeric OIDs and carries a Standard OIDs helper that explains the common ones in plain language, so you do not need a MIB browser open beside you:

The per-host SNMP panel with the Standard OIDs helper expanded

Results render as a table or as a tree, and the tree is the one to use after a walk. How names resolve to numbers and back is covered in Catalogs: vulnerabilities, OIDs, MIBs and more.

Profiles instead of credentials

Rather than supplying credentials with every query, store them once as a profile and bind it to the hosts it applies to. The console has a wizard for this, with predefined profiles to start from, reachable from the SNMP panel’s profile selector.

Supported versions are v1, v2c and v3. For v3 the console explains the three security levels as you choose between them โ€” only authPriv both authenticates and encrypts, and it is the only one that hides what is being polled rather than merely proving who is asking.

Resolution order when a scan runs: the profile named in the request, then the profile bound to that host, then the configured default. A target that resolves to no profile is skipped, and the execution says so.

Prefer profiles over one-off credentials

Credentials passed inline with a single query are persisted with that execution record and appear in its detail afterwards. Profiles keep them out of execution history entirely. Use inline credentials for one-off diagnostics only.

Note

The scheduled SNMP probe runs a discovery pass first, with a fixed budget. A /24 at cautious timing will exhaust it, so narrow the range or raise the discovery timing value in Configuration.

Passive analysis

The one probe you do not launch. It listens continuously on its capture interface and emits nothing, building an inventory indexed by MAC address alongside the address-indexed one the active probes fill.

Passive analysis on the Network status header opens what it has established:

Passive network analysis โ€” inventory, LLDP neighbours and findings

Three tabs: the per-device Inventory with vendor, type, role and the confidence behind each, the LLDP neighbours it has overheard, and Findings such as the capture-point verdict, gateway identification and address conflicts.

The type and role columns are where the industrial coverage shows. The probe recognises thirteen industrial protocols carried over IP โ€” Modbus, S7comm, OPC UA, DNP3, IEC-104, EtherNet/IP, BACnet, CODESYS, MELSEC, PTP, HART-IP, KNXnet/IP and FINS โ€” and records which side is serving and which is asking. It also reads four protocols that travel directly over Ethernet with no IP layer at all: IEC 61850 GOOSE and Sampled Values, PTP/IEEE 1588, and PROFINET DCP. On a substation or production segment those are frequently the bulk of the traffic, and nothing that keys off addresses and ports can describe them. See Passive and active for the full picture, including which ports are vendor convention rather than registered and are therefore reported at lower confidence.

The neighbours tab is LLDP only. The probe sees and counts Cisco’s own discovery frames, but it does not derive neighbours from them โ€” so a segment of Cisco equipment speaking CDP rather than LLDP produces traffic the probe accounts for and a neighbour table that stays empty. That is a limitation, not a fault to chase. The coverage note above the table states plainly what the figure leaves out โ€” passive analysis only sees what talks, so a silent device is not counted.

Everything about it is configured under Configuration โ†’ Automatic scans, in the sniffing block: which interfaces to capture on, an optional capture filter, which observed addresses reach the host inventory, how often what it learns is persisted, and whether it infers vulnerabilities from identity.

The interface name is the usual culprit

Capture failing silently almost always comes down to the interface name, which must be the one the capture library uses: eth0 or enp3s0 on Linux, en0 on macOS. The friendly name from the operating system’s own network settings does not work.

The Notices panel on Network status reports capture conditions directly โ€” a rejected capture filter, or analysis stopped by an error โ€” so check there before assuming the network is quiet.

The shared execution model

Every launch is asynchronous: the request is accepted, the work continues in the background, and the Network status header reports the state of the most recent run as it progresses. A run ends as finished or failed, and a run interrupted by a probe restart is moved to failed on start-up rather than being left in progress forever.

Executions also publish over MQTT

For integrations that would rather not poll, the embedded broker publishes started, finished and failed events for every execution. See MQTT and OpenGate operations.

The merged result view

An individual execution tells you what one probe found. The Network status table tells you what is known about a host, merged across all four probes: address and hostname, MAC address, manufacturer and device type with their provenance, operating system, status, when it was first and last seen, then the per-probe contributions โ€” ports from discovery, findings from vulnerability scanning, values from SNMP, and traffic counters from passive analysis.

The first-seen and last-updated pair is what turns the baseline into change detection: a host with a recent first-seen is new to the network.

This same merged view is available through the API for dashboards and integrations, at GET /security/results/last.

Feeding results into rules

Probe results reach the rules engine, so a finding can raise an alarm the same way a metric can โ€” a new host appearing, a critical vulnerability, a port opening that should not be open. See Alarms and rules.

Index

Subsections of Security Probes

OT/ICS Vulnerability Scanning

OT/ICS vulnerability scanning

Generic vulnerability scanners are built for IT. Point one at an industrial segment and two things go wrong: it probes for a web server that a PLC does not have and drops the host from the scan, and when it does find something it has no idea what a coil or a Common Address is.

Axiom Border ships its own suite of 57 checks for five industrial protocols, written specifically for this problem. They are installed with the product and made available to every scan automatically, so they travel in every deployment role and work with no internet access and no upstream template feed.

Industrial equipment is not a web application

A write to the wrong register on a live PLC has physical consequences. Axiom Border defaults to read-only and requires two independent switches before it will send a single write frame. Read the safety model before you enable anything, and never enable intrusive mode without written authorisation for the segment you are testing.

Protocol coverage

Protocol Port Checks Of which intrusive
Modbus/TCP 502/TCP 19 6
IEC 60870-5-104 2404/TCP 11 4
DNP3 20000/TCP 10 3
BACnet/IP 47808/UDP 10 1
OPC UA 4840/TCP 7 2
Total โ€” 57 16

Every check is labelled with the protocol it targets, and the ones in the write/control layer are additionally labelled as intrusive. That intrusive label is what the safety model keys on.

What each layer detects

The suite is organised in four layers of increasing invasiveness:

flowchart TB
    L2["<b>Layer 2 โ€” Detection</b><br>Is this protocol here?<br>Identify vendor and version"]
    L3["<b>Layer 3 โ€” Exposure</b><br>Structural weaknesses reachable<br>without authentication"]
    L3B["<b>Layer 3b โ€” Recon</b><br>Read process data and object lists<br>without writing anything"]
    L4["<b>Layer 4 โ€” Intrusive</b><br>Write / control confirmation<br><i>gated behind two locks</i>"]
    CVE["<b>CVE checks</b><br>Vendor-specific, only where<br>fingerprinting is reliable"]

    L2 --> L3 --> L3B --> L4
    L2 --> CVE

    L4:::danger

    classDef danger fill:#fff0ed,stroke:#ff664e,color:#101010

Detection confirms the protocol is listening and extracts identity where the protocol allows it โ€” Modbus device identification, DNP3 Object Group 0 device attributes, BACnet vendor identifier, OPC UA BuildInfo software version.

Exposure reports structural problems that need no credentials to observe: Modbus and DNP3 responding to unauthenticated requests, IEC-104 accepting a station interrogation, BACnet/IP being usable as a reflection and amplification source, OPC UA offering endpoints with None security policy or anonymous authentication.

Recon reads real process data โ€” Modbus holding registers via FC03, Modbus diagnostics via FC08 sub-function 0, IEC-104 counter interrogation and read commands, DNP3 event classes and class-0 integrity polls, BACnet Who-Is discovery and object enumeration. These checks read; they never write.

Intrusive confirms a write is actually possible. It is described in detail below.

CVE checks exist only for the cases where the protocol itself reveals enough to be sure: Schneider Modicon over Modbus/UMAS, Delta enteliBUS (CVE-2019-9569) and Contemporary Controls (CVE-2025-13926) over BACnet, plus a multi-vendor Modbus fingerprint that maps identity to known advisories.

Why some known CVEs are deliberately absent

Axiom Border only reports what it can actually confirm. Three known CVEs cannot be confirmed over the industrial protocol itself, so no check claims to detect them: DNP3 CVE-2020-6996 (the Triangle MicroWorks stack version is not observable over DNP3), OPC UA stack versions below 1.5.374.158 (only the Basic128Rsa15 precondition is observable, and that is already covered by an exposure check), and the IEC-104 device CVEs (IEC-104 carries no native device identifier). For those, use SNMP interrogation to fingerprint the device and correlate the version externally.

Scan depth and how OT checks get selected

Depth decides which checks run. OT checks are never included by accident โ€” you either choose a deep scan, choose OT explicitly, or opt in for scheduled scans:

Depth in the console Web templates OT read-only checks Notes
Light Yes No Unless the scheduled probe has OT checks enabled
Medium Yes No Unless the scheduled probe has OT checks enabled
Deep Yes (full) Yes Full web scan plus a separate OT pass
OT/ICS (industrial) No Yes The industrial suite only โ€” for dedicated OT segments
flowchart TB
    REQ["Scan request"] --> LVL{"Depth"}
    LVL -->|"Light<br>Medium"| W["Web templates<br>only"]
    LVL -->|"Deep"| WOT["Web scan +<br>OT read-only"]
    LVL -->|"OT/ICS"| OT["OT/ICS<br>checks only"]
The configuration file uses different words

In configuration.yaml the same four depths are written ligero, medio, profundo and ot. The console labels them Light, Medium, Deep and OT/ICS. They are the same four settings โ€” worth knowing if you move between the two.

Scheduled scans with OT checks enabled add the read-only OT layer to whatever depth they run at.

The OT pass runs separately, on purpose

When a scan includes OT checks, that part runs as a pass of its own that does not discard hosts without a web server. A general-purpose scan pre-filters targets by HTTP reachability, which would drop a PLC or RTU before a single Modbus request was ever sent. Running the OT pass separately is what makes industrial assets visible at all.

The safety model: two locks

Intrusive checks send write or control frames. Enabling them requires two independent switches that live in different places, so neither an operator nor a configuration mistake can unlock them alone:

  1. Per-scan opt-in โ€” Intrusive mode (OT/ICS) in the scan dialog, off every time you open it:
The vulnerability scan dialog. Intrusive mode is the switch at the bottom, off by default
  1. Deployment kill-switch โ€” vulnScan: authorise intrusive mode in Configuration โ†’ Automatic scans, which is off on every installation and has to be turned on deliberately, by someone with access to the probe’s configuration.

If the deployment lock is closed, an intrusive scan is rejected outright, not silently downgraded. The run is accepted and then ends as failed, and no checks execute โ€” not even the read-only ones the same request asked for.

So an unauthorised intrusive request gives you nothing, never a partial result you might mistake for a clean bill of health โ€” and the Network status header names the setting that blocked it, so the refusal is visible where you launched the scan rather than buried in a log.

Neither lock can be bypassed โ€” the same rule applies to scans launched by the platform as to scans launched from the console.

The scheduled sweep is never intrusive

Whatever those two switches say, the scheduled vulnerability scan does not run write or control checks. Intrusive mode is a deliberate, manual, per-scan act โ€” it can never become the background behaviour of a probe someone configured months ago and forgot.

flowchart TB
    REQ["Vulnerability<br>scan request"] --> Q1{"intrusive: true<br>in the request?"}
    Q1 -->|no| RO["Read-only scan<br><i>write layer excluded</i>"]
    Q1 -->|yes| Q2{"allowIntrusive<br>in configuration?"}
    Q2 -->|no| REJ["Request rejected<br><i>REST and MQTT</i>"]
    Q2 -->|yes| INT["Write layer enabled<br><i>audited as WARN</i>"]

    REJ:::danger
    INT:::danger

    classDef danger fill:#fff0ed,stroke:#ff664e,color:#101010

Three further guarantees hold regardless:

  • The scheduled probe never runs the intrusive layer. Automatic periodic scans are always read-only, whatever the configuration says. Intrusive checks only ever happen because someone asked for one, explicitly, right now.
  • Every intrusive execution is audited. A WARN entry goes to the log and a notice is attached to the details field of the execution record.
  • Intrusive means “no net change”, not “no writes”. The approach is read-then-write-back โ€” read the current value, write the same value back โ€” or SELECT-only for command protocols, issuing the select phase without the execute phase. Genuinely destructive actuation is excluded from the suite entirely.

What the intrusive layer actually does, per protocol

Protocol Intrusive checks Technique
Modbus/TCP write-single-register (FC03โ†’FC06), write-single-coil (FC01โ†’FC05), write-multiple-registers (FC03โ†’FC16), write-multiple-coils (FC0F), mask-write-register (FC22), read-write-multiple-registers (FC17) Read current value, write the identical value back
IEC 60870-5-104 control-select (C_SC), double-command-select (C_DC), setpoint-select (C_SE), regulating-step-select (C_RC) SELECT phase only, S/E=1, execute phase never sent
DNP3 analog-output-writeback (g40โ†’g41), binary-output-crob (g10v2โ†’g12v1), crob-select-only Write-back of the read value; CROB in SELECT-only form
BACnet/IP writeproperty-noauth Writes present-value back verbatim, preserving the original tag encoding
OPC UA anonymous-session, node-write-back Establishes an anonymous session; writes a read value back
Real physical risk

“No net change” is a design goal, not a law of physics. A PLC may react to the act of being written to โ€” some stacks latch, some log, some fault. A SELECT without an execute leaves a control point reserved on some IEC-104 implementations. Treat intrusive mode as an operation on live plant, because that is what it is: schedule it, get authorisation, and have someone watching the process while it runs.

Three ways to run it

All three start the same way: open the host’s Vulnerabilities panel and use Re-scan IP address vulnerabilities. What changes is what you set in the dialog.

A dedicated OT segment scan, read-only

The common case โ€” assess industrial equipment without touching a single register.

Set Depth to OT/ICS (industrial) and leave Intrusive mode off. That runs the industrial suite on its own: detection, exposure and recon layers, all read-only. Nothing else runs, so there is no web scanning noise against equipment that has no web interface.

A deep scan covering both IT and OT

Set Depth to Deep. That is everything โ€” technology detection, CVEs, misconfigurations, default credentials, exposures, network services and the OT/ICS read-only suite.

Use it on a mixed segment where industrial equipment sits alongside ordinary servers. On a pure OT segment prefer OT/ICS, which does the same industrial work without the rest.

An intrusive confirmation scan

Only once the deployment lock has been opened, and only against equipment you are authorised to write to.

Set Depth to OT/ICS, turn Intrusive mode (OT/ICS) on, and โ€” this is the part that matters โ€” run it against one host, not a subnet. Open the panel for that single asset rather than sweeping a range.

Narrow the target before you open the second lock

A read-only sweep across a /24 is routine. An intrusive sweep across a /24 sends write and control frames to every industrial device that answers, including ones you did not have in mind.

Intrusive runs are per-asset by discipline, not because the product forces it.

Enabling OT checks on the scheduled probe

To have the periodic automatic scan include the OT read-only layer without changing its level:

securityProbes:
  vulnScan:
    enabled: true
    level: "medio"
    enableOT: true          # adds the read-only OT layer to scheduled scans
    allowIntrusive: false   # keep the master lock closed

This is the recommended steady-state configuration for a probe sitting on an industrial segment: continuous read-only OT visibility, with the write layer bolted shut.

Reading the results

OT findings surface through the same execution model as every other probe โ€” poll GET /security/executions/{uuid} for status, and read the findings from the execution record once the status reaches finished. See Security probes for the shared asynchronous execution model.

Findings identify the protocol, the affected host and port, the check that fired, and the severity. For fingerprint checks the extracted identity (vendor, model, firmware or software version) is part of the finding, which is what makes the CVE correlation useful downstream.

Operational guidance

Start with level: "ot" on a narrow target. A /24 sweep of an industrial segment generates traffic that some networks are not used to. Validate against one host, confirm the findings make sense, then widen.

Keep allowIntrusive: false as the normal state. Open it for the duration of an authorised test window and close it again. It is a configuration change and requires a service restart, which is a feature here rather than an inconvenience โ€” it makes the unlock deliberate and visible.

Expect true negatives. The multi-vendor Modbus fingerprint reports nothing when no listed vendor is present. That is correct behaviour, not a missed detection.

Segment scans do not need internet. The OT checks are installed with the product and run offline. If your OT network is air-gapped โ€” and it should be โ€” nothing about this capability degrades.

Catalogs: Vulnerabilities, OIDs, MIBs and more

What the probe knows before it is plugged in

A scanner is only as good as the data behind it, and the usual arrangement is to fetch that data on demand: a vulnerability feed pulled at scan time, a vendor lookup resolved against a web service, a MIB downloaded when an unknown OID turns up. Every one of those assumes a route to the internet.

Axiom Border carries all of it. Nothing is downloaded at run time โ€” not before a scan, not during one, not to interpret a result afterwards. A probe in an air-gapped plant resolves manufacturers, names OIDs, and matches vulnerabilities with exactly the same coverage as one sitting in an office.

The rule that decides what is updatable

Data is updatable in the field. Decisions are not.

The IEEE registry and the CVE database are facts about the world that change without anyone here deciding anything, so they can be refreshed on a running probe. The industrial port table and the device-typing rules are auditable decisions, each with its evidence, and they stay compiled into the product on purpose โ€” so that what the probe concludes about your network is reproducible, and changes only when the product does.

What ships, and where it comes from

Catalogue Source What is installed
Vulnerabilities โ€” CVE database The NVD, the US NIST National Vulnerability Database 13,275 unique CVEs across 218,111 vendorยทproductยทversion rows, in 20 vendor feeds
Manufacturers โ€” IEEE registry The IEEE, published registry of MAC address blocks 39,979 assignments, laid over the built-in table
Manufacturers โ€” built-in table The same IEEE registry, frozen into the product at build 38,242 prefixes. The floor that can never go missing
OIDs and MIBs Vendor-published MIB modules, gathered from public sources 12,547 modules in the format the probe reads, plus 15,869 in ASN.1 for your own tooling
Generic vulnerability checks The upstream nuclei-templates project, pinned to a fixed release 13,279 templates
OT/ICS vulnerability checks Written by amplรญa))) for this product 57 checks โ€” 41 read-only, 16 intrusive behind two locks

Every figure above is what the probe reports about itself. None of it needs a network to be true.

Vulnerabilities โ€” why those 20 vendors

The vendor list is not “who makes industrial equipment”. It is which vendors the probe can actually put a name and a version to from what it observes โ€” a neighbour announcement, a MAC address block, or a device declaring itself. A vendor the probe cannot identify would contribute rows that could never match anything.

Group Vendors
Network edge Fortinet ยท Cisco
Automation and control Siemens ยท Schneider Electric ยท Rockwell Automation ยท ABB ยท Mitsubishi Electric ยท Honeywell ยท Emerson ยท Yokogawa ยท Omron ยท WAGO ยท Beckhoff
Industrial networking Moxa ยท Belden (Hirschmann, GarrettCom, Tofino) ยท Phoenix Contact ยท Advantech ยท Schweitzer Engineering
In the plant anyway Synology ยท TP-Link

The last group is there on purpose, and it is not a category error. Neither vendor makes industrial equipment, but both turn up in plants regardless โ€” the NAS somebody parked the historical data on, the router somebody plugged in. The probe identifies them by MAC address block like anything else and their firmware carries versions, which is the only criterion this list applies.

What is in there, by severity: 1,341 critical, 4,853 high, 4,316 medium, 215 low, and 2,549 not scored, spanning disclosures from 1999 to 2026.

The database is what turns an identity into a finding without emitting a packet, and how complete the identity is decides what the probe is allowed to say:

What was identified What it reports
Vendor, product and exact version Affected โ€” a genuine “vulnerable to this CVE”
Vendor and product, version unknown Potentially affected, verify โ€” never presented as the same thing
Vendor only No CVE can be pinned to the host, so it reports the vendor’s known CVE surface as a count and severity breakdown, with a recommendation to scan it actively

That last row is the one worth reading twice. A host identified only down to its manufacturer is not a clean host โ€” it is one the probe could not narrow far enough to name a finding. Reporting it as an empty row would be the single most misleading thing an assessment tool can do, so it reports the size of what it cannot yet see instead.

Two vendors that looked present and were not

Hirschmann and Phoenix Contact originally returned nothing at all, while appearing perfectly healthy in the list: the NVD files them under Belden and phoenixcontact respectively. Both are manufacturers the probe identifies routinely, so the empty feeds would have meant silent blind spots on common industrial equipment โ€” a good illustration of why the coverage figures above are verified against the source rather than assumed.

Manufacturers โ€” the registry and the floor beneath it

The installed IEEE registry turns a MAC address into a vendor name. It wins for the prefixes it carries; everything else keeps resolving against the table built into the product. See Passive and active for how that claim is presented with its evidence and its caveats.

Only the built-in table is mandatory

Remove the installed registry and nobody loses their manufacturer โ€” resolution simply falls back to the table inside the product. That is deliberate: making the overlay compulsory would turn an offline-first probe into one that needs an external file to do its job.

Only full-length assignments are used

The IEEE publishes three sizes of address block. Axiom Border uses only the full-length (MA-L) assignments, because the two smaller kinds divide a single 24-bit prefix between several companies โ€” including them would confidently attribute one company’s block to a different company. A wrong manufacturer is worse than no manufacturer.

Against the registry documented here, the installed file adds 1,737 prefixes the built-in table cannot resolve at all and changes the name on 270 more. Most of that second number is the registry tidying its own punctuation and character encoding rather than a company changing hands, but the ones that matter are in there: Phoenix Contact and ABB, now Hitachi Energy, are the renamings you are most likely to notice on a real inventory.

Take those three numbers as a snapshot, not a specification. They are a comparison between two things that both move on their own โ€” the registry the IEEE republishes every few days, and the table compiled into whichever build you are running. The probe works the current figures out for itself and reports them; see checking what a probe actually has.

OIDs and MIBs โ€” the SNMP catalogue

An OID like .1.3.6.1.4.1.6574.2.1.1.5 means nothing on its own. Turning it into diskTemperature requires a MIB, and on an air-gapped probe you cannot look one up โ€” so the catalogue ships with the product.

Contents Used by
JSON catalogue 12,547 modules across 2,762 vendors The probe. This is the catalogue it reads
ASN.1 catalogue 15,869 modules Not the probe โ€” supplied for the host’s own SNMP tooling

The modules are vendor-published MIBs, gathered from the manufacturers’ own documentation and public MIB collections, and installed as a single catalogue. It is treated as an external source of truth: it is not hand-edited, and it is refreshed as a whole rather than patched.

The ASN.1 tree is a convenience, not a spare copy

The probe never reads it. It is there so that snmpwalk and similar tools on the same host can resolve names too โ€” copy it to /usr/share/snmp/mibs if you want that. Pointing the probe at it does not work.

Vulnerability check templates

Two origins, and the pin is what holds the offline model together.

The generic templates come from the upstream project, pinned to a fixed release, and the engine’s own self-update is switched off. Both are load-bearing: an engine allowed to chase “latest” would try to download templates the first time it ran a scan, and a probe with no route out would simply fail there.

The OT/ICS suite is written in-house and lives inside the probe itself rather than in the templates directory. It is written out to disk each time a vulnerability scan runs, and anything left over from a previous version is removed at the same time.

That mirroring is a safety property, not housekeeping

Because the suite on disk is rebuilt from the copy inside the product at every scan, a check withdrawn in an upgrade stops firing everywhere. That matters most for the intrusive ones: a write-capable check removed on purpose cannot survive on disk and keep running.

It also explains a figure that looks alarming and is not โ€” see checking what a probe actually has.

Where the probe looks for them

All three locations are set from Configuration โ†’ Automatic scans, and the defaults are correct for a standard installation โ€” you only touch these to point the probe at data you placed somewhere else.

Configuration โ€” the CVE feed directory and the IEEE OUI registry, with passive vulnerability inference enabled
Setting What it points at
Sniffing: CVE feed directory Where the CVE database is seeded from at start-up
Sniffing: IEEE OUI registry The registry file laid over the built-in manufacturer table
SNMP: MIB directory The MIB catalogue, in the format the probe reads

Sniffing: passive vulnerabilities, on the same screen, is the switch that turns identity into findings after each observation window. With it on and no CVE database seeded, you still get identity โ€” you just get no CVEs from it.

Working with OIDs from the console

Network status โ†’ the SNMP action on a host row opens the query panel. This is the normal way to interrogate a device, and it needs no knowledge of OID numbering at all:

The per-host SNMP panel, with the Standard OIDs helper expanded
  • Version and Port set how to talk to the device.
  • Add OID takes a symbolic name or a numeric OID. You can paste several at once, separated by commas, spaces or line breaks.
  • Standard OIDs expands into the common ones grouped by purpose โ€” System, Interfaces, Host resources โ€” each explained in plain language rather than by number. sysDescr is described as vendor, model and firmware, all in one string; sysObjectID as the vendor’s identifier for the model โ€” the usual fingerprint. Click one to add it.
  • Manual OID / Retrieve OID switches between naming OIDs yourself and reading back what the probe already holds for that host.
  • Results render as a Table or a Tree, and the tree is the one to use after a walk.

Credentials do not belong in this panel. Store them once as an SNMP profile โ€” the console has a wizard for it, with predefined profiles to start from โ€” and bind the profile to the host. See Security assessment.

How resolution works

Two directions matter, and they behave differently: symbols become OIDs before the request goes out, and OIDs get names again on the way back.

Inbound, when a query contains symbolic names, they are resolved before anything is sent on the wire: first a small built-in table of the universal system OIDs, then anything already numeric passes through untouched, then everything else is looked up in the vendor’s modules.

flowchart TB
    S["Requested name<br>sysDescr"] --> C1{"Built-in<br>system OID?"}
    C1 -->|"no"| C2{"Already<br>numeric?"}
    C2 -->|"no"| C3{"In the vendor's<br>modules?"}
    C3 -->|"no"| FAIL["Query fails<br>unresolved oids"]:::danger
    C1 -->|"yes"| OK["Numeric OID<br>sent on the wire"]
    C2 -->|"yes"| OK
    C3 -->|"yes"| OK
    classDef danger fill:#fff0ed,stroke:#ff664e,color:#101010
An unresolved symbol fails the whole query

If any requested symbol cannot be resolved, the run ends as failed โ€” it does not silently skip the unknown ones and query the rest. The failure names the symbols it could not resolve, so the fix is usually obvious: correct the spelling, pick the name from Standard OIDs, or use the numeric OID.

Outbound, every OID that comes back is put through four steps, first match winning: the built-in system OIDs; the vendor’s modules, trimming up to two trailing segments โ€” which is how indexed OIDs such as ifDescr.3 resolve to ifDescr; the SNMPv2 module; and a set of generic modules by exact match. If nothing matches, the name comes back as unknown. The value is still returned โ€” only the label is missing.

Ambiguity yields no name rather than a wrong one

When trimming produces more than one candidate match, resolution stops and returns no name. This is deliberate, and it is the same principle the probe applies to manufacturer identity: a value labelled with a plausible-but-wrong symbol is worse than an unlabelled one, because it silently misleads whoever reads the report.

The vendor hint

Symbol resolution needs to know which vendor’s modules to search. Axiom Border works it out in order:

  1. The MIB named in the query itself.
  2. The default MIB from Configuration โ†’ Automatic scans โ†’ SNMP: default MIB.
  3. Inferred from the host’s manufacturer, as already recorded in the inventory.

The third is the useful one, and it is why running discovery before SNMP pays off: the manufacturer already established for that host becomes the vendor hint automatically. Explicit rules exist for common vendors โ€” Synology, Cisco, HP, Huawei, Juniper, D-Link โ€” falling back to the first word of the manufacturer name.

The hint is only consulted when a query actually contains non-numeric symbols. All-numeric queries need no vendor at all.

Values in results

Each result entry carries the numeric OID, the resolved name (or unknown), the formatted value and a status. Values are formatted by type: numbers as numbers, IP addresses in dotted form, and byte strings as text when they are printable, otherwise as hexadecimal โ€” so binary values are legible rather than mangled.

Walks mark disappearances, single queries do not

A walk covers a whole subtree, so an OID that was previously known and is now absent is genuinely gone, and gets marked as down. A single-OID query only asks about what you listed, so absence proves nothing and nothing is marked. This is why change detection over SNMP inventory should use walks.

Browsing the MIB catalogue

Listing vendors, searching for a symbol and inspecting a module are not in the console โ€” the catalogue browser is available through the API only. Day-to-day this rarely matters, because the Standard OIDs helper covers the common cases and the vendor hint resolves the rest automatically. Reach for these when you are working out what a specific vendor exposes.

# Vendors and their modules โ€” inexpensive regardless of catalogue size
curl -sk https://192.168.1.10:8083/security/mibs \
  -H "Authorization: Bearer <jwt-token>"

# Search for a symbol. Always pass vendor when you can
curl -sk 'https://192.168.1.10:8083/security/mibs/search?q=diskTemp&vendor=synology&limit=50' \
  -H "Authorization: Bearer <jwt-token>"

# Inspect one module. Names are exact and case-sensitive
curl -sk https://192.168.1.10:8083/security/mibs/SYNOLOGY-DISK-MIB \
  -H "Authorization: Bearer <jwt-token>"

q is matched as a case-insensitive substring against object names. Always pass vendor when you can: with it the search covers one vendor’s modules, without it the whole catalogue, and that is the one operation whose cost grows with catalogue size. Everything else here is fast.

How a module ends up under a vendor

Vendor is derived from the module name: everything before the first hyphen, lowercased. So SYNOLOGY-DISK-MIB belongs to vendor synology, and A3COM-HUAWEI-DEVICE-MIB to a3com. A module name with no hyphen becomes its own vendor.

This is a heuristic rather than metadata read from the file. It works because MIB naming conventions are near-universal, but do not expect it to be perfect on unusual modules โ€” which is the other reason to search by symbol rather than by browsing vendors.

Extending the MIB catalogue

To add a vendor’s MIB, place its JSON module in the configured catalogue directory. New modules are picked up automatically, and changing the directory reloads the catalogue from the new location.

A missing catalogue does not stop the probe

If the catalogue cannot be loaded โ€” wrong path, unreadable files โ€” the probe logs a warning and carries on with an empty catalogue rather than refusing to start.

The signature of that particular mistake is distinctive and worth recognising: numeric OIDs work, sysDescr works, and everything else fails to resolve. Check SNMP: MIB directory in Configuration โ†’ Automatic scans before looking anywhere else.

Keeping the catalogues current

Three of them can be replaced on a running probe, with no restart and no internet: the CVE feeds, the IEEE registry, and the vulnerability check template bundle. You bring the file to the probe by whatever means your site allows, and the probe takes it from there.

This is not in the console yet

Updating these catalogues is currently an API operation โ€” the web console reads and edits where they live, but does not yet offer uploading a new one. The calls below are the supported path today.

The CVE database

curl -sk -X POST https://192.168.1.10:8083/security/passive/cve-feeds \
  -H "Authorization: Bearer <jwt-token>" \
  -F 'file=@nvd-siemens.json'

Accepts a single feed file or the whole bundle. Ingestion is idempotent per feed: re-ingesting a feed deletes its previous rows before writing the new ones, so a CVE withdrawn upstream disappears here too rather than lingering as a stale finding. Add ?replace=true to empty the directory first instead of merging into it.

The IEEE manufacturer registry

curl -sk -X POST https://192.168.1.10:8083/security/oui-overlay \
  -H "Authorization: Bearer <jwt-token>" \
  -F 'file=@oui.csv'

Takes the IEEE file exactly as published. The next observed frame already resolves against it โ€” there is no reload step.

It is validated on a copy before anything is replaced. A file that fails to yield a single usable assignment is rejected and the running registry is left untouched, so a truncated download cannot take your manufacturer resolution down with it.

The vulnerability check templates

curl -sk -X POST https://192.168.1.10:8083/security/vulnscan/templates/bundle \
  -H "Authorization: Bearer <jwt-token>" \
  -F 'file=@vulnscan-templates.tar.gz'

Checking what a probe actually has

curl -sk https://192.168.1.10:8083/security/passive/cve-feeds \
  -H "Authorization: Bearer <jwt-token>"
{ "dbFeeds": 18, "dbEntries": 55183, "directory": "/var/lib/axiom-border/cve-feeds",
  "feeds": [ { "name": "nvd-siemens.json", "entries": 1181, "size": 262144 } ] }

The same shape exists for /security/oui-overlay and /security/vulnscan/templates.

Files on disk and rows ingested are two different numbers

Reading both at once is deliberate, because their disagreement is the exact signature of the most common failure: the feeds are on the probe and nothing seeded them. A feed listed with a real size but entries: 0 is a file sitting in the directory that is not a feed the probe can read.

The OT check count reads zero until the first scan

Because the industrial suite lives inside the probe and is written out at scan time, a probe that has not run a vulnerability scan yet reports 0 OT checks. The suite is present and will be used; it simply has not been laid down on disk yet.

Do not read that zero as a missing installation.

Why this is worth the disk it takes

Every catalogue here could have been a web service call. Making them local costs a few hundred megabytes and buys four things that matter in an industrial network:

  1. The probe works where it is most needed. Isolated segments are isolated deliberately. A tool that phones out to stay useful is either useless there or a hole in the isolation.
  2. Results are reproducible. The same probe, the same version, the same data, gives the same answer next month. A feed that silently moved underneath you does not.
  3. Nothing about your network leaves it to be analysed. Resolving a vulnerability against a hosted service means telling that service what you have.
  4. A scan cannot fail because something upstream was unreachable, renamed, or rate-limited.

And where the world genuinely does move โ€” new CVEs, reassigned address blocks โ€” you refresh it deliberately, on your own schedule, with a file you can inspect before you install it.

AI Capabilities

AI capabilities

Axiom Border can run machine-learning models on the probe, in containers, to detect anomalies in the metrics it ingests. A model trains locally on local data, exposes an inference endpoint on loopback, and rules call it to decide whether an event is worth an alarm.

The point is the same as everywhere else in the product: no cloud round trip, no data leaving the site.

Linux and the central role are required

This feature requires Linux and the container engine that the central role installs, so deploy AI capabilities on that role.

Anywhere else the rest of the probe runs normally โ€” every other subsystem is unaffected โ€” but the AI capability does not start, and the controls that deploy or train a model have nothing behind them.

Anomaly detection covers system metrics

Deploy the capability with agent type metrics, which detects anomalies in the system metrics the probe ingests. The other agent types โ€” ssh, iface, usb โ€” appear in the API but are not supported yet.

Where you manage it

AI capabilities in the console lists the models configured on the probe. On a fresh installation it is empty, because model images are supplied separately from the product:

The AI capabilities view before any model has been deployed

From here you create a capability, follow its training state, force or cancel a training run, edit the rule that calls the model, and read the quality metrics the model reports about itself. Everything on this page is done from that view โ€” the sections below follow it in order.

The lifecycle

flowchart TB
    TAR["Model image tarball<br>supplied separately"]:::ext -->|"stage on disk"| IMG["Image imported<br>for one agent type"]
    IMG -->|"deploy"| EXPORT["Training data<br>exported"]
    EXPORT --> DEPLOY["Container deployed"]
    DEPLOY --> HEALTH{"Model<br>healthy?"}
    HEALTH -->|"not yet"| HEALTH
    HEALTH -->|"responds"| READY["READY"]
    READY -->|"retrain due"| TRAIN["TRAINING"]
    TRAIN --> READY
    READY -->|"predict"| INFER["Inference<br>from rules"]
    classDef ext fill:#e9edfa,stroke:#486ac9,color:#101010

Each agent type has a fixed loopback port, which is how a rule reaches the right model:

Agent Port Container name
iface 5555 <trainerContainerName>-iface
ssh 5556 <trainerContainerName>-ssh
usb 5557 <trainerContainerName>-usb
metrics 5558 <trainerContainerName>-metrics

Each model listens on the host’s loopback address, so rules reach it at 127.0.0.1:<port>.

Loading a model image

Model images are not part of the installation bundle โ€” they are supplied separately, because which model you run is a decision about your data rather than about the product.

Create new AI trainer collects everything needed in one step: the model package, and the agent type it applies to. You can upload the package from your machine, or tick Use file on disk and pick one already staged on the probe โ€” the wizard lists what is there, with the directory it is reading.

The new AI trainer wizard: the model package and the agent it applies to
Loading an image replaces the others

Importing a model keeps the image for the agent you named and removes the images loaded for other agent types. If you intend to run capabilities for more than one agent, importing them one after another will not accumulate them.

Deploying a capability

The same wizard deploys it. Two settings decide how it behaves afterwards:

Setting What it does
Retraining period How often the model retrains. Ten minutes is the minimum โ€” shorter is rejected. Leave it at zero and the model never retrains
Script enabled Whether the rule for this agent runs. It can be set to enable itself as soon as data arrives from that agent

Deployment then does four things in order: exports the training data the probe already holds, starts the model with that data available to it, waits for the model to report itself healthy, and marks the capability ready โ€” at which point it collects the model’s own quality metrics and enables the rule.

The health wait is bounded, roughly a minute and a half by default. A model that is slow to start needs that raised in Configuration; it is not a failure of the model.

Only one capability per agent type can exist at a time. Deploying over an existing one is refused โ€” remove it first.

Following it from the console

The table on AI capabilities is the whole operational picture: trainer and model name, type, the retraining period, when it last ran and when it runs next, whether the script is enabled, the data source and inferencer, the model version, and the status.

The AI capabilities view
Status Meaning
READY Trained and serving inference
TRAINING A training run is in progress
READY, LAST TRAINING FAIL Serving, but the most recent training run failed
CANCEL, WAITING TO RESUME A run was cancelled; the schedule decides what happens next
ERROR Something failed, with the detail alongside

Each row carries its actions:

Action What it does
Train now Forces a training run outside the schedule
Cancel training Stops a run in progress
Edit retraining Changes the retraining period
Edit script Opens the rule that calls this model, with a template if it has none yet
Show metrics The model’s own quality metrics from its last training
Audit log Every execution recorded for this capability
Delete Removes the capability
Forcing a run does not move the schedule

Train now updates the last-executed time but leaves the next scheduled run exactly where it was. It is also refused while a run is already going, and it waits for a scheduled run rather than colliding with it.

Cancel training stops the probe waiting for the model to come back rather than killing a computation mid-flight, which is why the capability lands in a waiting state rather than simply stopping.

Training makes inference briefly unavailable

Every training run re-exports the data, restarts the model and waits for health again โ€” so there is a window during which inference does not answer. A rule that asks the model during that window gets an error, and it should handle that as “no answer” rather than treating it as an anomaly. A model restarting is not a security event.

Deleting is best-effort, and says so

Removing a capability cleans up the container, the image, the rule script, the exported data and the stored record, and stops the retraining schedule. It reports success even when part of that cleanup failed, with the details alongside โ€” so read what it reports rather than assuming a silent success.

Calling inference from rules

This is where the capability earns its place. A rule script fetches recent metrics, builds a feature vector, asks the model to score it, and raises an alarm when the model says anomaly:

function process(metrics) {
  for (var i = 0; i < metrics.length; i++) {
    var metric = metrics[i];
    metric.SetT0();

    var payload = JSON.stringify({
      sent_vars:     metric.GetFieldByName("sent_vars"),
      prepared_vars: metric.GetFieldByName("prepared_vars"),
      ram_usage:     metric.GetFieldByName("ram_usage")
    });

    var response = newPredict(payload, "metrics");
    var result   = JSON.parse(response);

    if (result.prediction === 1) {
      newAlarm(metric, "critical", "New anomaly detected", "Anomaly", "AI Capability");
    }

    metric.SetT1();
  }
}

Two globals are available to scripts for this: newPredict(payloadJSON, agentType) calls the model’s inference endpoint, and newGetMetrics(agentType) fetches the model’s own metrics. Both target the loopback port for that agent, and both are configured under trainerDetails.

Keep the script time limit above the predict timeout

The predict call has its own timeout (default 10 s), but the entire script invocation is bounded by gojaTimeout (default 8 s). With those two defaults, a slow inference call cannot complete โ€” the script is stopped first.

If your rules call inference, set gojaTimeout comfortably above trainerDetails.predict.timeout, or lower the predict timeout. Always define gojaTimeout: without a time limit, scripts do not run at all.

The rule script for an agent is enabled through ruleEnabled when deploying the capability, and can be managed directly through the /scripts endpoints. See Alarms and rules.

Surviving a restart

Capabilities are part of the probe’s local state, and startup restores them: containers are resumed, model metrics are re-fetched, and retraining schedules resume from their stored next-run time.

One case is treated deliberately: a capability that was training when the process stopped is considered cancelled rather than resumed, because the training run did not finish. It moves to the appropriate cancelled state and waits for its next scheduled run.

Failures during this restoration are logged and do not prevent startup.

Configuration reference

The relevant block is trainerDetails โ€” container name, TLS material, and the three endpoint definitions for health, metrics and inference. See Configuration.

The health, metrics and inference endpoints are meant to be reached over loopback only. Keep them bound to 127.0.0.1 and do not expose those ports beyond the host.

MQTT and OpenGate Operations

MQTT and OpenGate operations

Axiom Border carries its own MQTT broker, so a probe is a message bus as well as a monitoring probe. Three independent pieces make up the messaging layer:

Piece Role
Embedded broker The MQTT broker running on the probe, on loopback. Optional TLS listener for remote clients
Event client Publishes execution lifecycle events. Publish-only
Operations client Subscribes to OpenGate operations and answers them

On top of that sits the optional OpenGate integration, which reports inventory upward (collect) and registers devices (provision).

The local bus, first: scans publish their lifecycle, and anything that can connect to the broker can follow it.

flowchart TB
    SCAN["Security scans"] -->|"started<br>finishedok<br>failed"| EVC["Event client"]
    EVC --> BROKER["Embedded broker<br>loopback TCP 1883"]
    BROKER --> UI["Web console<br><i>via the API bridge</i>"]
    BROKER --> SUB["External<br>subscribers"]:::ext
    classDef ext fill:#e9edfa,stroke:#486ac9,color:#101010

Then the platform side, where OpenGate drives scans and receives inventory.

flowchart TB
    PLAT["OpenGate platform"]:::ext -->|"operation"| OPS["Operations client<br>odm/operation"]
    OPS -->|"launches"| SCAN2["Security scan"]
    OPS -->|"response"| PLAT
    INV["Provision and collect<br>inventory reports"] -->|"HTTP or MQTT"| PLAT
    classDef ext fill:#e9edfa,stroke:#486ac9,color:#101010

The embedded broker

The broker listens on loopback only, on TCP port 1883. It is not reachable from the network, and that is the shipped posture rather than a suggestion.

The console does not connect to it directly. It reaches the bus through a bridge inside the API’s own encrypted connection: the console asks for a single-use ticket that expires in seconds, and exchanges it when the connection is upgraded. So the live notifications you see in the console travel over the same authenticated, encrypted channel as everything else, and no separate port is opened for them.

A TLS listener on 8883 exists for the case where you genuinely need remote clients, including mutual TLS with client certificate verification. It is off by default.

This bus is the one inbound path that does not check a session

Operations arriving on the broker launch scans on the probe, and they are not authenticated by the API’s session mechanism โ€” that is what makes the loopback default load-bearing rather than merely tidy.

If you point the operations client at a remote broker, the probe requires an encrypted connection and refuses a plaintext one unless you explicitly override it. Whoever can reach that broker can make the probe scan.

Before exposing the broker on any network you do not fully control: restrict the port at the network level, set broker credentials โ€” with both empty it accepts anonymous connections โ€” and enable TLS. After changing TLS settings, confirm the broker came back up: if the certificates cannot be read it does not start, and the log says so.

Execution lifecycle events

Every security scan publishes its lifecycle to a single topic, mqtt.client.topic, which defaults to axiom-border/executions. This is the alternative to polling the executions API.

{
  "mode": "manual",
  "uuid": "4a79214d-3deb-4ae8-933f-9874cbdc1128",
  "event": "finishedok",
  "type": "networkscan",
  "elapsedTime": "5m18s",
  "details": "success"
}
Field Values
event started, finishedok, failed
type networkscan, vulns, snmp, snmpwalk, sniffing
mode manual, auto, process
elapsedTime, details Present on completion

Note that type here is networkscan, while the REST API reports the same scan as nmap. Map the two values if you correlate the event stream with the executions API.

Events are published on scan start, on completion, and on startup recovery for executions interrupted by a restart.

Events are dropped, not queued, when disconnected

While the connection to the broker is down, or when the topic is left empty, lifecycle events are not buffered for later delivery โ€” they are simply not published. Do not treat this topic as an audit trail. The authoritative record is the execution history, readable through GET /security/executions; the MQTT stream is a convenience for live UIs.

Events are published with the configured QoS (default 1) and retain flag (default true), and the client reconnects every 5 seconds while the broker is unreachable.

OpenGate operations over MQTT

The operations client lets the OpenGate platform trigger scans on the probe remotely. It subscribes to mqtt.ops.topicSubscribe (default odm/operation) and replies on mqtt.ops.topicPublish (default odm/response/{device-id}, with the device ID substituted at runtime).

Request envelope

{
  "operation": {
    "request": {
      "timestamp": 1730000000000,
      "name": "hostVulnScan",
      "id": "<operation-id>",
      "deviceId": "<device-id>",
      "parameters": { }
    }
  }
}

name, id and deviceId are all mandatory. A request missing any of them, or one that does not parse, is logged and silently ignored โ€” no response is published. An unknown operation name does get a response, with ERROR_PROCESSING and Unsupported operation.

Response envelope

{
  "operation": {
    "response": {
      "name": "hostVulnScan",
      "timestamp": 1730000001000,
      "resultCode": "SUCCESSFUL",
      "resultDescription": "Success",
      "deviceId": "<device-id>",
      "id": "<operation-id>"
    }
  },
  "version": "1.0"
}

There are exactly two result codes: SUCCESSFUL and ERROR_PROCESSING.

SUCCESSFUL means accepted, not completed

The operation response is published as soon as the scan is launched, not when it finishes โ€” and it does not carry the execution UUID. There is no field for it in the envelope.

To follow a scan triggered over MQTT, subscribe to the execution events topic or poll GET /security/executions. In particular, an intrusive OT scan that the configuration lock rejects is still answered with SUCCESSFUL; the rejection shows up afterwards as a failed execution.

The four operations

Operation REST equivalent
hostNetworkScan POST /security/scan/network
hostVulnScan POST /security/scan/vulns
hostSnmpScan POST /security/scan/snmp
hostSnmpWalkScan POST /security/scan/snmp/walk

Both surfaces drive the same scan engine, so they share the same locking, history and safety gates. The differences are in what each surface exposes.

hostVulnScan takes targets, timeout, level, severity and intrusive โ€” all optional. It does not expose templatesDir, so the configured resolution cascade always applies. The intrusive flag is gated by securityProbes.vulnScan.allowIntrusive exactly as over REST.

{
  "targets": ["192.168.1.0/24"],
  "level": "ot",
  "severity": "high,critical",
  "timeout": "10m",
  "intrusive": false
}

hostNetworkScan takes targets, timeout, timing and portFilter. Four options are fixed for MQTT-launched scans: port scanning is always on, OS detection is always on (which requires privileges), service detection is always off, and UDP is always off. There is no force equivalent either, so a scan launched while another manual network scan is running is rejected with ERROR_PROCESSING and manual network scan already running.

hostSnmpScan and hostSnmpWalkScan take targets, port, timeout, credentials in an snmp object, and either oids (mandatory for the GET) or walkRoot (defaulting to the enterprises subtree .1.3.6.1.4.1).

{
  "targets": ["192.168.1.50"],
  "port": 161,
  "timeout": "5m",
  "oids": ["sysDescr", "sysName"],
  "snmp": {
    "snmpVersion": "v2c",
    "community": "<snmp-community>"
  }
}
The SNMP field names differ from REST

The MQTT snmp object uses different names than the REST customParams: user rather than username, authType rather than authProtocol, privKey rather than privPass, and privType rather than privProtocol. The version field is snmpVersion.

Also: only the first entry of targets is used, and these credentials form an ephemeral profile that ignores stored profiles and associations entirely. mib and profileName are not exposed over MQTT.

Request payloads are not logged

Because operation parameters can carry SNMP community strings and v3 passphrases, request payloads are deliberately not written to the log โ€” only the operation name, ID and device ID. Responses, which carry no credentials, are logged in full.

OpenGate provision and collect

Separate from operations, the integration reports upward on a schedule โ€” and it is inert until you enable it. Everything it needs is on one screen, Configuration โ†’ OpenGate:

Configuration โ€” the OpenGate tab, with the master switch off and the transport security block below it
Setting What it decides
OpenGate integration The master switch. With it off, neither collect nor provision runs
API key Authorises the probe to the platform. Also serves as the MQTT password when collect runs over MQTT and no separate password is set
Schedule When provision and collect run, as a cron expression โ€” unlike the scan periods, which are durations
Minimum period A floor that protects the platform from a too-frequent schedule

Below it, HTTPS transport security governs how the probe talks to the platform: whether a private certificate authority is needed, and whether the probe presents a client certificate of its own for mutual TLS.

An unencrypted platform URL stops the integration

Allow unencrypted HTTP is off, and leaving it off is the right choice. What travels on this link is the full inventory of your network together with the API key that authorises it โ€” in the clear, over plain HTTP.

With the escape hatch off, a plaintext platform URL does not start the integration: it disables it and records why. It exists only for an installation that genuinely cannot move to HTTPS yet.

Scheduling and throttling

A five-field cron expression (default */30 * * * *) drives both, with descriptors such as @hourly accepted. An empty or invalid expression means the integration does not run at all โ€” check the log for a warning if nothing is happening.

minPeriod (default 30 minutes) is a floor: if the cron expression would fire more often than that, the schedule falls back to a fixed interval of minPeriod and logs a warning. Cycles never overlap โ€” a cycle due while the previous one is still running is skipped and logged.

Provision

Provisioning registers discovered hosts with the platform. It builds an Excel workbook โ€” one row per host, with device ID, addressing, state and the location metadata from opengate.collect.address โ€” and uploads it to the bulk endpoint.

The upload requires exactly 201 Created; anything else aborts the provision. The result is then polled up to pollMaxAttempts times (default 10, every 5 seconds), and only counts as success when the platform reports every submitted host as successful.

Provision is HTTP only โ€” there is no MQTT variant.

Collect

Collect sends the accumulated per-host inventory: identity, ports, vulnerabilities and SNMP entries, with timestamps.

{
  "deviceId": "192.168.1.50-00.11.22.33.44.55",
  "ipAddress": "192.168.1.50",
  "macAddress": "00:11:22:33:44:55",
  "specificType": "HOST",
  "hostname": "plc-line-1",
  "os": "Linux 4.x",
  "manufacturer": "Siemens AG",
  "status": "up",
  "ports": [ { "portNumber": 502, "protocol": "tcp", "service": "mbap", "status": "open" } ],
  "vulnerabilities": { "hasVulns": true, "catList": "medium,high", "highDetect": "high", "vulnList": [ ] },
  "snmp": [ { "oid": ".1.3.6.1.2.1.1.1.0", "name": "sysDescr", "value": "..." } ]
}

Transport is chosen by collect.mode: mqtt, or http for anything else including an empty value. Over HTTP the payload is posted with the X-ApiKey header; over MQTT it is published to collect.mqtt.topic, falling back to opengate.apiKey as the password when no MQTT password is set.

Chunking. With collect.sendByParts: true, each host is split into several messages: a header with the scalar fields plus one chunked block at a time โ€” ports, then SNMP, then vulnerabilities โ€” sized by collect.partSize.* (default 100 each). This matters on metered or constrained links where a host with hundreds of findings would otherwise produce one oversized message. With chunking off, each host is a single message.

A failed chunk is logged and the cycle continues with the next one, so one bad message does not abort the whole report.

Device identity

The device ID is derived per host as <ip>-<MAC with dots>, for example 192.168.1.50-00.11.22.33.44.55. Hosts without a discoverable MAC get the placeholder AA.BB.CC.DD.EE.FF. This is the same format accepted by GET /security/results/last?deviceId=, so an ID seen in the platform can be looked up on the probe directly.

Before each cycle, hosts missing a MAC or hostname get a quick nmap ping scan to fill the gaps, bounded by macDiscoveryTimeout (default 10 s).

A fixed collect.deviceId behaves differently per transport

Setting collect.deviceId forces one identifier for all hosts over HTTP, but the MQTT path always uses the per-host derived ID. If you rely on this override, use HTTP transport โ€” or better, leave it empty and let each host keep its own identity, which is almost always what you want.

Configuration reference

Everything here is configured under the mqtt and opengate blocks. See Configuration for the field-by-field tables, including the TLS options and the retry parameters for both provision and collect.

Operation and Maintenance

Operation and maintenance

Service commands

Each role installs a different set of units. The standard systemd verbs apply to all of them:

sudo systemctl status axiom-border
sudo systemctl restart axiom-border
sudo systemctl is-active axiom-border
sudo systemctl is-enabled axiom-border

The units, depending on role: axiom-border, axiom-influxdb, containerd, oda-lite.

Where the logs live

This is the single most useful thing to know when something misbehaves, because the important detail is not in the journal:

Service Journal Own file Rotation
axiom-border systemd events and early startup errors only โ€” with the shipped default, very little /opt/axiom-border/logs/axiom-border.log โ€” all the detail: probes, MQTT, scans, rules Automatic, tunable in configuration.yaml
axiom-influxdb Everything: startup, crashes, slow queries, compactions None journald drop-in: 500 MB, 1 day per file
containerd Everything: runtime, namespaces, OCI errors None journald drop-in
oda-lite Startup, crashes and early warnings only โ€” 3 to 5 lines in normal operation /var/log/oda-lite/oda-lite.log โ€” plugin activity, HTTP errors towards central, emitted metrics Automatic: every 24 h or at 100 MB, 7 archives kept
Lifecycle scripts โ€” /var/log/axiom-border-install.log โ€” install, upgrade and uninstall appended together Not rotated; clear it by hand if it grows
The rule of thumb

To diagnose application logic, read the file. To diagnose a crash or a failure to start, read the journal. Looking for probe results in journalctl is the most common wasted hour.

Everything at once, which is often what you want:

sudo journalctl -u axiom-border -u axiom-influxdb -u containerd -u oda-lite \
  --since "5 minutes ago" --no-pager

Log levels

Axiom Border โ€” the logger block of configuration.yaml. Set logLevel to DEBUG, INFO, WARN or ERROR, then restart the service. See Configuration.

oda-lite agent โ€” /etc/oda-lite/oda-lite.conf, [agent] section: log_level and debug. Restart the service afterwards.

InfluxDB and containerd โ€” the level comes from binary flags in the unit file. Changing it means editing the unit, and is not recommended.

Turn DEBUG back off

DEBUG on a busy probe generates enough volume to fill a disk. Raise it to reproduce a problem, then put it back to INFO. Do not leave it on “just in case”.

Rotation and disk usage

Axiom Border’s own log rotates through the logger settings: maxSize (MB per file, default 20), maxBackups (default 20) and compress (gzip the rotated files).

The journald drop-in installed by the bundle is global โ€” SystemMaxUse=500M and MaxFileSec=1day apply to the entire host journal, not only to Axiom Border. On a host sharing other workloads, adjust it or replace it with per-unit drop-ins.

journalctl --disk-usage
sudo journalctl --vacuum-size=500M
sudo journalctl --vacuum-time=7d

To see what is consuming space:

du -sh /var/log/oda-lite /opt/axiom-border/logs /var/log/journal/

Backup and restore

Resource Path Suggested frequency
Configuration /opt/axiom-border/config/configuration.yaml On every change
Local state โ€” rules, aliases, executions /opt/axiom-border/db.dat Daily
InfluxDB data โ€” metrics, alarms /var/lib/influxdb2/ Per retention policy
Logs /opt/axiom-border/logs/ On demand

Stop the service first, for a consistent snapshot:

sudo systemctl stop axiom-border
sudo tar -czf /path/backup-axiom-border-$(date +%F).tar.gz \
  /opt/axiom-border/config /opt/axiom-border/db.dat /var/lib/influxdb2
sudo systemctl start axiom-border

Restore is the inverse:

sudo systemctl stop axiom-border
sudo tar -xzf backup-axiom-border-<date>.tar.gz -C /
sudo systemctl start axiom-border

Offline maintenance

Refreshing the data the security probes read

Three things the probe reads off disk are facts about the world that change on their own: the vulnerability templates, the local CVE database, and the IEEE OUI registry. All three ship with the bundle, so a freshly installed probe already works. All three can be refreshed through the API, without restarting the service and without an SSH session โ€” which is the point, because the probe is designed for a network with no route to the internet.

Data is updatable, decisions are not

The OUI registry and the CVE base change without anyone on this team deciding anything, so they can be replaced in the field. The OT port table and the device-type rules are auditable decisions with their evidence, and stay compiled into the product on purpose.

Vulnerability templates

# What is installed right now
curl -sk https://<probe>:8083/security/vulnscan/templates -H "Authorization: Bearer $JWT"

# Offline refresh โ€” the mode that matters on an air-gapped probe
curl -sk -X POST https://<probe>:8083/security/vulnscan/templates/bundle \
  -H "Authorization: Bearer $JWT" -F "file=@vulnscan-templates.tar.gz"

The archive is expanded and validated in full in a separate directory, the product’s own OT/ICS templates are re-mirrored on top of it, and only then is it swapped in atomically. A corrupt, empty or traversal-carrying archive is rejected with 400 and the previous templates still serving. The response reports what ended up active; a non-zero otTemplates is the check that the OT set survived the replacement.

The upstream mode (POST /security/vulnscan/templates/update) needs connectivity and allowTemplateUpdates: true. On a standard deployment it answers 409 and points at the bundle route.

Both modes are serialised against a running scan, and the exclusion waits before giving up: the request blocks for up to 20 s and only then returns 409.

otTemplates: 0 on a freshly installed probe is normal โ€” the OT templates live inside the binary and are written to the directory on the first scan. A zero after an update is not.

Local CVE database

curl -sk https://<probe>:8083/security/passive/cve-feeds -H "Authorization: Bearer $JWT"

# One vendor, merged into what is already there
curl -sk -X POST https://<probe>:8083/security/passive/cve-feeds \
  -H "Authorization: Bearer $JWT" -F "file=@nvd-siemens.json"

# The whole base, guaranteeing nothing older survives
curl -sk -X POST 'https://<probe>:8083/security/passive/cve-feeds?replace=true' \
  -H "Authorization: Bearer $JWT" -F "file=@cve-feeds.tar.gz"

Ingested immediately, no restart. The status reports both the files on disk and the rows in the database: the two disagreeing is exactly the symptom of “the feeds are there but nothing seeded them”. If a single file in the upload does not parse as a feed the whole upload is rejected โ€” half a CVE base is worse than none, because the probe then reports fewer vulnerabilities than there are and looks like it worked.

OUI registry (manufacturer by MAC)

The table compiled into the binary freezes at each release while the IEEE republishes every few days. The registry on disk is laid on top of it: it wins for the prefixes it carries, everything else keeps resolving from the built-in table. Removing it leaves nobody without a manufacturer.

curl -fLO https://standards-oui.ieee.org/oui/oui.csv        # on a machine with internet
curl -sk -X POST https://<probe>:8083/security/oui-overlay \
  -H "Authorization: Bearer $JWT" -F "file=@oui.csv"

The file is accepted exactly as the IEEE publishes it, so what you installed can be diffed against the source. It takes effect on the next frame. The response counts what it contributes: newPrefixes (hardware currently reported with no manufacturer at all), overrides, and renames โ€” the last being the number that matters, since it is how many devices are about to be reported under a different manufacturer. Hosts resolved this way publish manufacturerSource: "oui-overlay".

Only MA-L assignments are read. MA-M and MA-S share a 24-bit prefix between several companies, so including them would credit one company’s block to another.

A version update restores the bundled copy

All three directories are re-staged from the bundle on every install: they are bundle content, not operator state. An upload through the API is an update between versions. To make new data the baseline for the fleet, publish its asset pack and let the next bundle carry it.

Loading AI images

AI images are not part of the bundle โ€” they are distributed separately as OCI tarballs, and resources/iaimages/ ships empty as a placeholder.

sudo nerdctl --namespace axiom-border image load -i <image>.tar
sudo nerdctl --namespace axiom-border image ls

Upgrading

Extract the new bundle of the same role on the target and run upgrade.sh. It aborts if the bundle role does not match the installed role โ€” changing role requires uninstall followed by install.

sudo ./upgrade.sh
Flag Meaning
--force-conf-regen Regenerate the oda-lite configuration from the new template, saving the current one as .bak-<timestamp>
-h, --help Print help and exit
Variable Default Meaning
HEALTHCHECK_RETRIES 12 Attempts before declaring failure
HEALTHCHECK_INTERVAL 5 Seconds between attempts โ€” 12 ร— 5 s gives a 60 s window
AXIOM_API_PORT 8083 API port used for the health check

What it does: verifies the previous installation and role match, stops the role’s services (leaving InfluxDB and containerd running, so data in flight is not disturbed), takes timestamped backups of the binary and db.dat, replaces the binary and updatable assets, starts the service, and health-checks it.

Preserved across upgrade: config/, db.dat, .influx-creds, parquet/, logs/, /var/lib/influxdb2/, resources/iaimages/ and the oda-lite configuration.

Replaced: the binary, MIBS/, resources/gojascripts/, vulnscan-templates/, the documentation and the lifecycle scripts.

Failed upgrades roll back automatically

If the health check fails all 12 attempts, upgrade.sh restores the previous binary, confirms the old version answers with HTTP 200, and exits with code 2. The probe is left operational on the old version and the backup is kept at /opt/axiom-border/axiom-border.bak-<timestamp>. An upgrade that reports a rollback has not broken anything โ€” diagnose, then retry with a corrected bundle.

Back up db.dat before upgrading

The local state file db.dat is not migrated automatically. If a new version changes its format, the file may have to be regenerated, and that loses local state โ€” rules, aliases and execution history. Always take a copy before upgrading, and if the new version will not start cleanly, restore the copy and revert the binary.

Uninstalling

sudo /opt/axiom-border/uninstall.sh

It stops and disables the role’s services, removes only the units it created, removes the journald and sshd drop-ins, and asks whether to keep the data โ€” InfluxDB data, db.dat, logs and configuration.yaml. Answer non-interactively with AXIOM_KEEP_DATA=true|false.

It removes shared binaries only where the manifest marks them as installed by this deployment. It does not remove nmap, rsyslog or network-manager even if it installed them โ€” those are host infrastructure and are left to the operator. It does not revert the netplan switch by default.

Variable Effect
AXIOM_KEEP_DATA=true|false Answers the data-retention question without prompting
AXIOM_RESTORE_NETPLAN=1 Restores /etc/netplan/ from the backup, re-applies it, and switches back to systemd-networkd
AXIOM_FORCE_CLEAN=1 Destructive. Without a manifest, claims every detected artefact as its own โ€” including global binaries that may belong to other software such as Docker or Kubernetes

Running it on a clean system is safe and idempotent: it reports there is nothing to uninstall and exits 0.

AXIOM_FORCE_CLEAN can remove another product’s binaries

Use it only after confirming nothing else on the host depends on containerd, runc, nerdctl or the CNI plugins. Without the flag, a manifest-less uninstall stays conservative and touches only artefacts that are unmistakably Axiom Border’s.

AXIOM_RESTORE_NETPLAN=1 warns about possible connectivity loss. In practice an established SSH session usually survives, since the TCP connection is not torn down and the IP is retained โ€” but do not count on it over a link you cannot recover from out-of-band.

Troubleshooting by symptom

Quick triage

Symptom Look here first
Service will not start journalctl -u <service> -n 50 --no-pager
Service active but no results /opt/axiom-border/logs/axiom-border.log
Metrics not reaching InfluxDB journalctl -u axiom-influxdb plus the agent log
Monitoring node not reaching central tail -f /var/log/oda-lite/oda-lite.log | grep "outputs.http"
SSH, USB or interface events missing The agent log, filtering by plugin name
Service restart-looping journalctl -u <service> --since "5 min ago", looking for start-limit
Install or upgrade behaved oddly tail -200 /var/log/axiom-border-install.log
Disk filling up du -sh /var/log/oda-lite /opt/axiom-border/logs /var/log/journal/

Detailed cases

The service will not start. Three usual causes: the binary lost its execute bit (chmod +x /opt/axiom-border/axiom-border), a required port is occupied, or the YAML is invalid. Validate the configuration directly:

python3 -c "import yaml; yaml.safe_load(open('/opt/axiom-border/config/configuration.yaml'))"

Login fails with the correct password. login.pass must be the SHA-256 digest, not the plaintext. Recalculate and restart:

echo -n "<your-password>" | sha256sum | awk '{print $1}'

A port is in use. Identify the holder, then either stop it or move the Axiom Border port:

sudo ss -tlnp | grep -E ':(8083|1883|1888|8086|9090|9091) '

Ports 8083, 1883 and 1888 move via apiPort, mqtt.broker.port and mqtt.broker.ws.port. Ports 9090 and 9091 move in /etc/oda-lite/oda-lite.conf.

Vulnerability scans return zero findings, suspiciously. The web template directory is probably empty or incomplete:

ls /opt/axiom-border/vulnscan-templates/ | head

It should list per-protocol directories such as http/, network/ and ssl/. CVE templates live under http/cves/, not at the root. Refresh them as described above. OT/ICS findings are unaffected, because that suite ships with the product.

Sniffing captures nothing. Check the interface exists (ip -o link show), that the service runs as root (systemctl show axiom-border -p User), and that there is actually traffic (sudo tcpdump -i <iface> -c 10).

The UI loads but every API call returns 401. The JWT is missing or expired. Clear the browser’s localStorage and log in again.

nmap probes hang or take very long. A large target range with poor reachability, a high timeout, or udp: true with many ports. Narrow the range, lower the timeout, disable UDP.

High memory usage. Check for an AI capability working a large dataset, an Influx bucket with no retention policy, or logLevel: DEBUG left on.

“Orphaned installation detected.” Bundle artefacts exist without an .install-manifest. Three ways out: AXIOM_CLEAN_RESIDUALS=1 sudo bash install.sh moves the leftovers aside, sudo bash upgrade.sh preserves data, sudo bash uninstall.sh cleans best-effort.

upgrade.sh exits with code 2 reporting a rollback. The new binary failed its health check and the old one is back in service. Diagnose with the journal, the application log and the install log, then retry with a corrected bundle.

The monitoring node floods the journal with “connection refused” to port 9090. The central node is not up, a firewall blocks it, or the URL is wrong. The agent keeps retrying and buffers metrics while it waits, so a short outage loses nothing โ€” a long one eventually will.

Error executing 'netstat': exit status 1. Minimal Ubuntu Server images lack net-tools. Not blocking โ€” the other plugins continue:

sudo apt-get install -y net-tools && sudo systemctl restart oda-lite

NetworkManager is active but interfaces show as unmanaged. The netplan renderer was not switched over. Re-run install.sh --switch-to-network-manager; it detects the current state and completes the switch without reinstalling NetworkManager.

/etc/oda-lite/oda-lite.conf.new contains literal ${VARIABLE} placeholders. The file is an unexpanded template. Do not copy it over the live configuration โ€” the agent would not start. Delete it and re-run the upgrade with the current bundle.

scp or tar -xzf fail with Permission denied in /tmp. Some base images ship /tmp with wrong permissions. It should be drwxrwxrwt:

ls -ld /tmp
sudo chmod 1777 /tmp