Building an OT security lab, wrapping it in a Command Centre, and why the operator's screen never noticed.
2026-11-06
Most of my testing has been IT: web apps, networks, Active Directory. OT, the systems that run factories, water works and power stations, has been on my list for a while, and you cannot exactly ask a customer if you can practise on their reactor. So I built a plant I am allowed to break, and then a control panel to run it from.
The heavy lifting is GRFICSv3 from Fortiphyd Logic, an open-source training range. It simulates a chemical reactor with two feeds, a purge and a product outlet, controlled by a real OpenPLC runtime over Modbus, with a Scada-LTS operator screen, a firewall between the operations and control zones, a Kali box in the DMZ, and Wazuh and Zeek watching the network. Nine containers in all.
It runs on my Mac under Colima with the native arm64 images. The one fiddly part is Zeek:
it gets its traffic from a mirror set up on the Docker host with tc, which
disappears every time the container restarts. My wrapper script rebuilds it on every start
so I never have to remember.
Nine systems on their own ports, each with its own login, gets old quickly. So I built one page that shows the state of the plant (emergency stop, PLC, pressure, valves, the gas analyser), a light and restart button for every system, and a workspace where any two of them sit side by side. On top of that sit eight missions with objectives, flag questions, XP and medals, and a written guide that says exactly where to type each command and what you should see. It is a small Python server with no dependencies, bound to localhost.
One design choice I am pleased with: the dashboard reads plant values from the simulator, not from the PLC. If it polled the PLC over Modbus, my own dashboard would show up in the Zeek logs I was supposed to be analysing.
This is the bit that changed how I think about it. Modbus has no authentication. If you can reach port 502 you can read and write the PLC, and nothing asks who you are. From the Kali box it took a few lines of Python: flip one coil to put the controller into manual mode, then write four holding registers to open both feeds fully and close both outlets. Gas goes in, nothing comes out, and the pressure climbs about four kPa a second.
The operator's screen shows it all happening: manual mode on, the setpoints, the pressure rising. What it does not do is raise an alarm. Every write was a legitimate Modbus function doing exactly what it was designed to do. No exploit, no malware, no CVE. That is the core of OT security for me now: the question is not "is it patched" but "who can reach it, and would anyone notice".
The lab also taught me how fragile this kit is. Restarting the router once dropped the HMI's connection and the PLC runtime crashed outright. That is why you scan an OT network carefully, or not at all, and why the best time to test a control panel is in the factory before it ships.
The later missions swap sides. Zeek logs every Modbus request, so a new IP writing to the PLC stands out immediately when you list who talks to whom. Wazuh turns that into an alert. Then firewall rules between the zones stop the Kali box reaching port 502 at all, and you rerun the attack to prove it fails. The last mission is writing it up as a one-page factory acceptance test report, with each finding mapped to an IEC 62443-3-3 requirement and a fix for each.
I posted a screenshot on LinkedIn expecting a few likes. Tens of thousands of people saw it and a lot of them asked if they could have the Command Centre, so it is now on GitHub, free and MIT licensed: CyberZombi3/ot-lab-command-centre. It does not bundle GRFICSv3; a setup command fetches it from Fortiphyd, so the credit for the plant stays with them.
If you know IT security and want to see what is different about OT, it runs on a laptop with Docker and about 16GB of RAM. Keep it on your own machine, and never point the scripts at real equipment. There is more detail on the project page.