Concluded·23 Aug 2026 · 4 min read
Counting the plant three ways
nmap 7.x SYN scan · Proxmox VE 9.1 · Ignition 8.3.8 file-based config · pve-firewall · 192.168.29.0/24 plant segment
Prompted by the July 2026 water-sector intrusions, where the recurring detail was that operators did not know their equipment was reachable. The question here is narrow: on an estate whose every component I chose myself, do the available inventories agree?
The rig
| Segment | 192.168.29.0/24, a flat plant network |
| Control system | Ignition 8.3.8, file-based config, an edge gateway |
| Scanner | nmap, SYN scan, -Pn -n --open |
| Firewall | pve-firewall on the hypervisor |
| Hosts probed in full | the five plant machines |
Inventory A — the control system's configuration
Grepping every resource under the gateway's config tree for network endpoints:
$ grep -rhoE '[0-9]+[.][0-9]+[.][0-9]+[.][0-9]+(:[0-9]+)?' config/resources/core/ignition/
1 192.168.29.76:5544
One. The historian. Every other JDBC and OPC string in the tree is either
localhost or vendor documentation text.
Plus two gateway-network peers:
gateway-network-incoming/central-a-master_3007b9a9…/config.json
{ "securityStatus": "Approved" }
gateway-network-incoming/central-a-backup_2829c77e…/config.json
{ "securityStatus": "Approved" }
Both refer to machines that have since been destroyed. The trust survived the hardware. This is the same property measured in what a gateway backup actually restores — approval is a config resource, so it outlives whatever it approved.
Inventory B — what answers
A 22-port sweep of ports you would actually expect on an industrial segment (22, 80, 443, 502, 1883, 4840, 5432, 5544, 8060, 8088, 8443, 9090, 3000, 1880, 18083, 8883, 20000, 44818, 47808, 5900, 102, 161):
| host | role | open |
|---|---|---|
| .1 | ISP router | 80, 443, 8443 |
| .36 | SCADA gateway | 22, 8060, 8088 |
| .76 | container host / historian | 22, 5544 |
| .106 | knowledge-graph host | 22, 1883 |
| .203 | ops host | 22 |
| .228 | unidentified | 80 |
| .239 | IoT stack | 22, 1883, 3000, 8883 |
| .10 | unidentified | 22 |
Eight hosts, sixteen open ports. A ping sweep found eleven hosts alive, so three answer to ICMP but expose nothing on the expected ports.
.239 deserves attention. It runs Mosquitto, Grafana, TimescaleDB, Telegraf, a
simulator and a web app — a complete second plant data path, with its own broker
on 1883 and 8883. It appears in inventory A not at all.
The scan that tripled
The 22-port list is a reasonable list. That is exactly the problem with it. Re-running against all 65,535 ports on the five plant hosts:
| host | 22-port scan | full scan |
|---|---|---|
| .36 SCADA gateway | 3 | 3 |
| .76 container host | 2 | 16 |
| .106 knowledge graph | 2 | 15 |
| .203 ops host | 1 | 3 |
| .239 IoT stack | 4 | 5 |
| total | 12 | 42 |
Nothing on the network changed between the two runs. .76 exposed fourteen
services I had not thought to ask about, including 11434 (a local model server),
8096, 9696, 8989, 7878 and a 6881 BitTorrent tracker port. .106
exposed thirteen, including 6432 pgbouncer, 10051 Zabbix and a spread of
808x application ports.
The 22-port scan was not wrong. It answered precisely the question it was asked, and the question was the wrong one. The port list is the finding: an inventory built from a scan inherits every blind spot of whoever wrote the list.
Note also that the earlier 22-port sweep missed .239's web app on 8080
entirely, so even the "second plant" I did find was under-reported by my own
instrument.
Inventory C — what is permitted
$ pve-firewall status
Status: disabled/running
$ iptables -L INPUT -n
Chain INPUT (policy ACCEPT)
Everything listening is reachable from anywhere on the segment. There is no third list; the third list is the second list.
This is defensible on an isolated lab segment and would be a serious finding on a utility network. The point for this experiment is that neither of the other two inventories would have told me, and I did not know it until I ran the command, on an estate I built.
Where the three disagree
| inventory A (config) | inventory B (scan) | inventory C (firewall) | |
|---|---|---|---|
| hosts | 3 named | 11 alive, 8 responsive | all |
| endpoints | 1 real, 2 ghosts | 42 open ports | unrestricted |
| catches decommissioned kit | no — keeps it, approved | yes — it does not answer | n/a |
| catches undeclared kit | no | only if you scanned its port | n/a |
| catches misconfiguration | no | no | yes |
Each inventory catches something the other two structurally cannot. Configuration knows intent, and is blind to reality. Scanning knows reality, filtered through your expectations. The firewall knows policy, and nothing about what is behind it.
Verdict
There is no single correct inventory, and the effort should go into the diff rather than into any one list. The two disagreements that mattered here were:
- Ghost trust — two approved peers for machines that no longer exist, invisible to scanning because dead hosts do not answer, and invisible to the firewall because it does not care.
- The blind-spot multiplier — the same estate has 16 or 42 open ports depending only on the port list, which means any inventory derived from a targeted scan carries an unknown and unmeasured shortfall.
Run the full sweep occasionally. It is slow, dull, and the only version capable of telling you something you did not already believe.
The argument for a general audience is in there is more on the plant network than anyone thinks.
Newsletter
New essays, by email.
SCADA, cloud, AI, and the plant floor — a short email when something new is published. No noise, unsubscribe anytime.