Every command to a water plant, checked against physics before it runs.
Live demo: backflow.design (plays a recording of the real system on a loop)
On 30 July 2026 the FBI and EPA put out a warning: attackers were changing settings on internet-facing pump controllers at water systems in at least seven US states.
The targets weren’t big city utilities with security teams. They were small towns, where one operator looks after the whole system and is on call at 3 a.m. The controller that runs the chlorine doser sits on the same network the contractor uses for remote access. If someone gets in and sets the doser to 100 %, nothing stops it. The controller does what it’s told.
Firewalls don’t help here because the attack looks completely normal. It’s a valid Modbus write, from an address that’s allowed to write, to a register that’s meant to be written. The only way to see it’s wrong is to ask what the command would do to the water.
So we built a box that asks exactly that.
Backflow is a safety gateway that sits between remote access and the pump controller. Every command goes through it, and every command gets run forward through a physics model of the plant before it’s allowed to reach the controller.
COMMAND ➔ PARSE ➔ POLICY + PHYSICS ➔ ALLOW or HOLD ➔ AGENT INVESTIGATES ➔ OPERATOR DECIDES ➔ REPORT
predict()
returns what breaks and when; step() drives a digital twin that plays the real world for the demo. remote access ──► GATEWAY ──(allowed only)──► ESP32 STATION ◄── PLANT TWIN
policy + physics (relays, sensors) (tank level, chlorine)
│
held commands
▼
AGENT ──► operator briefing ──► Approve / Reject ──► incident report
What’s real and what’s simulated: the Modbus traffic, the gateway, the block, the relays, the sensors and the alarm are real. The tank level and chlorine are computed by the twin, and the ESP32 stands in for an industrial controller.
python -m venv .venv
.venv\Scripts\Activate.ps1 # Mac/Linux: source .venv/bin/activate
pip install -r requirements.txt
python tasks.py demo --sim # software station, twin and gateway; drop --sim to use the ESP32
Open http://localhost:8000 and sign in as operator. In a second terminal, run an attack:
python -m attacks.normal_day # allowed
python -m attacks.chlorine_overdose # held, with the physics reason
There are six scenarios in attacks/: normal day, chlorine overdose, drain the town, overflow,
logic tamper and operator on site. Only run them against your own station or the software station. The
register map is in shared/registers.yaml, the site is documented in
dashboard/README.md, and the firmware in firmware/README.md.
python tasks.py test runs the test suite.