Hands-on industrial control systems training lab built for ICS Village (DEF CON village). Runs the entire DMZ + Process Control fabric β firewall, DHCP, DNS, virtual PLCs, Modbus + DNP3 outstations, master polling loop, Suricata IDS, and an operator dashboard β as containers on one Raspberry Pi.
ββββββββ operator browser βββββββ
β β
βΌ β
ββββββββββββββ single Raspberry Pi 5 ββββββββββββββββββββ
β ββ
β βββ DMZ Β· dmz-br0 Β· 192.168.75.0/24 (L3.5) ββββββ ββ
β β firewall Β· dhcp-dmz Β· OTLab Dashboard β ββ ββ https://<pi>:8000/
β βββββββββββββββββ¬ββββββββββββββββββββββββββββββββββ ββ
β β firewall conduit (iptables) ββ
β βββ PCN Β· pcn-br0 Β· 10.20.30.0/24 (L1/L2) ββββββ ββ
β β firewall Β· dhcp-pcn Β· modbus-master β ββ
β β sensor-sim Β· dnp3-outstation β ββ
β β plc-1-virt Β· plc-2-virt (OpenPLC) β ββ
β βββββββββββββββββββββββββββββββββββββββββββββββββββ ββ
β ββ
β + Suricata IDS sniffing pcn-br0 ββ
β + Cockpit / Portainer / EdgeShark admin UIs ββ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β β
βββ wlan0 β internet βββββββββββββββββ
One Pi. One wlan for internet. That's it. Everything else lives in containers.
Status: Standalone single-Pi lab is shipped and working. Optional physical expansion (additional Pis with real GPIO + real Conpot honeypot fabric) is documented separately. Curriculum + Attack/Detect/Defend exercises + CTF challenges are the next chunk of work β looking for contributors here (see CONTRIBUTING.md).
A complete teaching environment for ICS / OT security, all on one Pi:
| Category | What |
|---|---|
| Network segmentation | Industrial DMZ (L3.5) + Process Control Network (L1/L2), enforced by a containerized iptables firewall with SNAT + DNS forwarding |
| DHCP / DNS | Per-zone dnsmasq DHCP with static reservations Β· DNS forwarder integrated into the firewall Β· all queries logged for "DNS exfil at the firewall" teaching |
| Master / outstation loop | modbus-master container polling sensor-sim at 10 Hz β deterministic, observable Modbus TCP traffic on the wire |
| OpenPLC | Two virtual OpenPLC instances with web UIs (port :8081, :8082) for IEC 61131-3 click-around lessons |
| DNP3 | Pure-stdlib DNP3 outstation on :20000 |
| IDS | Suricata sniffing pcn-br0 with OTLAB rules for Modbus FC5/6/15/16 writes from non-master IPs + SSH brute-force detection |
| Dashboard | 7-tab Flask + vanilla JS operator surface: Overview Β· Architecture Β· IDS Β· Firewall Β· DHCP Β· Live Data Β· Teaching |
| Admin UIs | Cockpit (Linux), Portainer (Docker), EdgeShark (live packet capture in browser) |
You need: one Raspberry Pi 5 (16 GB recommended, 8 GB works) with WiFi (for internet), an SD card or NVMe, and operator workstation (macOS or Linux laptop) on the same network.
# === 1. Image a fresh Pi OS Lite (64-bit Bookworm) on the SD/NVMe ===
# In Pi Imager β Advanced: set hostname `l3-mon-01`, configure WiFi,
# enable SSH, set username + password.
# === 2. From your operator workstation, clone this repo ===
git clone https://github.com/aaroncorvo/otlab.git
cd otlab
# === 3. Push ssh keys to the freshly-imaged Pi ===
ssh-copy-id <imager-user>@l3-mon-01.local
# === 4. Bootstrap (creates otadmin + otuser, installs Docker, lab venv) ===
./scripts/bootstrap-users.sh <imager-user>@l3-mon-01.local
./scripts/bootstrap-pi.sh otadmin@l3-mon-01.local
./scripts/bootstrap-l3-mon-role.sh otadmin@l3-mon-01.local
# === 5. Deploy the lab fabric ===
./scripts/install-virtual-lab.sh otadmin@l3-mon-01.local
# === 6. Add Suricata IDS + admin UIs (optional but recommended) ===
./scripts/install-suricata.sh otadmin@l3-mon-01.local
./scripts/install-cockpit.sh otadmin@l3-mon-01.local
./scripts/install-portainer.sh otadmin@l3-mon-01.local
./scripts/install-edgeshark.sh otadmin@l3-mon-01.localTotal time: ~30 min on the first run (most of which is the OpenPLC source build β cached on re-runs).
After deploy, browse to:
| URL | What | Login |
|---|---|---|
https://l3-mon-01:8000/ |
OTLab Dashboard (the main thing) | otlab / P@ssw0rd! |
https://l3-mon-01:9090/ |
Cockpit (Linux admin) | otadmin / your sudo password |
https://l3-mon-01:9443/ |
Portainer CE (Docker UI) | set on first visit |
http://l3-mon-01:5001/ |
EdgeShark (live pcap in browser) | none |
http://l3-mon-01:8081/ |
Virtual OpenPLC #1 web UI | openplc / P@ssw0rd! |
http://l3-mon-01:8082/ |
Virtual OpenPLC #2 web UI | same |
Lab convention: intentionally-public passwords for booth use. Rotate per event so creds don't leak between cohorts.
Full step-by-step walkthrough: docs/setup-from-scratch.md.
30-second demo: fire a Modbus FC6 write from the Teaching tab β see it land as an OTLAB-1004 alert on the IDS tab β see the firewall counter tick on the Firewall tab. (If the GIF above is missing, see reference/screenshots/ for the capture playbook.)
Seven tabs β full walkthrough in docs/dashboard-tour.md:
| Tab | What | Screenshot |
|---|---|---|
| Overview | Live process state (animated SVG synoptic) + cards for every container + live Modbus poll telemetry from the master | overview.png |
| Architecture | Purdue model with the lab's actual assets placed at their canonical levels + auto-discovered network topology | architecture.png |
| IDS | Suricata stats β counts (5m / 1h / 24h), 24h timeline, top signatures, top sources, top targets, recent alerts | ids.png |
| Firewall | Live iptables (5 chains) with packet counters Β· conntrack snapshot Β· DNS query stats + log | firewall.png |
| DHCP | Per-zone (DMZ + PCN) lease tables + static reservations + recent transactions | dhcp.png |
| Live Data | System health, audit log, pcap captures | live-data.png |
| Teaching | Risks, walkthroughs, runnable test library, Modbus Write Playground, Inject Fault, Cohort Reset | teaching.png |
For a classroom with N students each on their own single-Pi OTLab, there's a guided installer + reset workflow:
# Per Pi (interactive β asks role + student number, bakes per-student IPs)
./scripts/otlab-install.sh otadmin@otlab-teacher.local
./scripts/otlab-install.sh otadmin@otlab-student-05.local
# Between lab steps β fast wipe, keeps teacher access
./scripts/otlab-reset.sh --step otadmin@otlab-student-05.local
# End of class β full wipe, Pi back to fresh-install state
./scripts/otlab-reset.sh --full otadmin@otlab-student-05.localThe classroom comes with:
| Component | What | Where |
|---|---|---|
| Teacher Admin Panel | Auto-discovers + lays out student Pis on a canvas; lock roster, optional FortiGate port monitor | teacher/ |
| SIEM stack | Loki + Grafana + Promtail receiving Suricata/dashboard/firewall logs from every student, indexed by student_id |
teacher/siem/ |
| MikroTik router config | One-paste RouterOS 7.x config: 21 DHCP reservations, 60 static routes (3 fabric layers Γ 20 students), inter-student deny ACL | reference/router-configs/mikrotik/ |
| Per-student subnets | Unique fabric subnets per student (10.{75,30,50}.N.0/24) so SIEM correlation works by source IP | docs/classroom-installer.md |
Full walkthroughs: docs/classroom-installer.md (install + reset), docs/classroom-network.md (network map), teacher/README.md (teacher panel), teacher/siem/README.md (SIEM).
Teacher panel was originally built by @LogicGateOperator (Dillon Lee / ICSVillage).
Once the single-Pi setup is working, you can extend it for richer teaching scenarios:
| Expansion | What it adds | Doc |
|---|---|---|
| Add a physical OpenPLC Pi | Real GPIO, real Modbus on the wire, Phase 2 hardware (relays, indicators, pushbutton) | setup-from-scratch.md Β§ Stage 2 |
| Add a physical Conpot Pi | Three vendor honeypot personas (Siemens / Schneider / Rockwell) on a separate physical box | setup-from-scratch.md Β§ Stage 2 |
| Add an RS485 Modbus device | Connect a real industrial sensor (temp, energy meter, etc.) via a Waveshare RS485-to-Ethernet gateway | setup-from-scratch.md Β§ Stage 3 |
| Add wireless IoT | ESP32 Modbus client over WiFi joining the PCN segment | setup-from-scratch.md Β§ Stage 4 |
Each stage is independent and optional. The single-Pi lab is fully functional on its own β you don't need any of these expansions to teach the core curriculum.
.
βββ README.md β you are here
βββ docs/ # Architecture + setup + curriculum
β βββ setup-from-scratch.md β linear from-zero playbook (start here)
β βββ dashboard-tour.md β 7-tab dashboard walkthrough
β βββ lab-architecture.md β deep-dive build doc
β βββ virtualization.md β ContainerLab fabric architecture
β βββ naming-schema.md β canonical names, IPs, MAC reservations
β βββ network-topology.md β physical NIC β virtual fabric
β βββ architecture-evolution.md β phase plan + decision log
β βββ curriculum.md β lessons, MITRE ATT&CK coverage
β βββ phase-1-modbus-loop.md β first lesson walkthrough
β βββ arduino-setup.md β Arduino UNO breakout boards
βββ virtual/ # ContainerLab fabric
β βββ topologies/otlab.clab.yaml # full topology (9 nodes + 2 bridges)
β βββ dockerfiles/ # 7 OTLab images
βββ dashboard/ # Dashboard source (mounted into container)
βββ plc/ # Python services + OpenPLC programs + scenarios
βββ honeypot/ # Conpot persona configs (optional Pi #3)
βββ scripts/ # Bootstrap + install scripts
βββ reference/ # Diagrams, BOMs, vendor OIDs, pcaps
# Topology state
ssh otadmin@l3-mon-01.local 'sudo containerlab inspect -t /home/otuser/lab/virtual/topologies/otlab.clab.yaml --format table'
# Tail a container's log
ssh otadmin@l3-mon-01.local 'sudo docker logs -f clab-otlab-modbus-master'
# Live firewall counters
ssh otadmin@l3-mon-01.local 'sudo docker exec clab-otlab-fw-dmz-pcn iptables -nvL FORWARD'
# Recent IDS alerts
ssh otadmin@l3-mon-01.local 'sudo grep "event_type\":\"alert" /var/log/suricata/eve.json | tail -10'
# Reset between cohorts (browser): Teaching tab β Reset Lab for Next Cohort
# Disaster recovery β nuke + redeploy
./scripts/install-virtual-lab.sh otadmin@l3-mon-01.localThis lab is intentionally public so people can learn from it, fork it, and improve it. See CONTRIBUTING.md for areas where help is welcome.
Currently looking for:
- Curriculum + exercises β Attack/Detect/Defend scenarios mapped to MITRE ATT&CK for ICS, written as runnable scripts in the Teaching tab's Test Library
- CTF challenges β flag-based exercises across the existing protocols (Modbus, DNP3, DNS, IDS rules)
- Additional Conpot personas β there's a Conpot Docker image; we could ship virtual personas alongside the physical ones so single-Pi users see honeypot data too
- Live Modbus wire feed β a sidecar sniffer container on pcn-br0 streaming decoded frames to the dashboard (currently the wire-feed panel gracefully degrades to a "no traffic visible" message)
- Take-home topologies β minimal ContainerLab YAML that runs on a student laptop (no Pi needed)
- Documentation β tutorials, video walkthroughs, blog posts
MIT. Use it, fork it, teach with it. Attribution appreciated but not required.
ICS Village at DEF CON. Big thanks to the ICS Village community for the multi-year container of curiosity and the encouragement to share this stuff publicly.

