Technical Deep Dive OT / ICS · Smart-grid substation Case Study · IberEnergy S.A.

CSI caught a one-line change that would have tripped a live MV feeder at IberEnergy.

A "refactor" PR removed the sole software authorization check on the breaker-command register in a Spanish DSO's substation gateway. CSI blocked it in pre-merge review with a CRITICAL verdict, mapped the finding to CWE-862, IEC 62443-3-3 SR 2.1 and NIS2 Art. 21.2.d, and - in a controlled counterfactual run - demonstrated the physical detonation: an authenticated read-only user issued a Modbus FC6 write to HR0 and CB-21 opened, dropping the feeder to 0 A / 0 MW in under two seconds.

Sector
Electricity DSO (Spain)
Target
Substation gateway · Modbus/TCP + IEC-104 + HMI
Runtime
Local Docker · Emulated field bus
Regulatory frame
NIS2 · IEC 62443 · IEC 62351 · ENS
01

The Use Case

IberEnergy S.A. is a Spanish electricity DSO (distribution system operator). The team owns the software that runs on substation-gateway - the Modbus/TCP + IEC-60870-5-104 endpoint that sits between the operator's HMI and a live 20 kV feeder controlled by circuit breaker CB-21. Because IberEnergy is an essential entity under NIS2 (energy sector) and operates critical infrastructure under Spain's ENS + PIC frameworks, every commit that touches the gateway is expected to preserve the authorisation-enforcement-at-the-gateway invariant mandated by IEC 62443-3-3 SR 2.1.

A developer opens a pull request titled refactor: streamline breaker actuation -- drop redundant role check. One file changes:

--- a/gateway.py +++ b/gateway.py @@ -48,4 +48,3 @@ def set_breaker(field, role, close): - authorise_write(role, BREAKER_REG) # ACL enforcement point field.write_register(BREAKER_REG, 1 if close else 0) state = "CLOSED" if close else "OPEN" @@ -30,4 +30,1 @@ def authorise_write(role, register): - if register in PROTECTED_REGISTERS and role not in AUTHORISED_ROLES: - log.warning("ACL DENY: role=%r register=%s", role, register) - raise AccessDenied(f"role {role!r} is not authorised to operate the breaker") + return True # authorisation now handled by the HMI session

The commit message reads as ordinary maintenance: "The HMI session layer already authenticates operators upstream, so re-checking on every command just adds latency." The framing inverts the actual risk. The HMI session authenticates identity - who you are - but the ACL enforces authorisation - what you may do. Under the new code, any authenticated principal (including the read-only viewer role) can call set_breaker, and the gateway will forward the raw Modbus FC6 write to the field bus without complaint.

Why this matters. The field bus is unauthenticated by design - the Modbus protocol trusts every peer that can complete a TCP handshake to it. The gateway's authorisation check is the only software control that prevents an under-privileged principal from operating a live medium-voltage breaker. Removing it turns a routine session into a physical de-energisation event.
02

About IberEnergy S.A.

IberEnergy S.A. is a fictional Spanish DSO (~1.2 M connections, MV/LV distribution across two autonomous communities) chosen for this deep-dive because it lives at the intersection of two regulatory regimes that increasingly speak to each other:

  • NIS2 - IberEnergy is an essential entity under Annex I (energy · electricity DSOs) and is bound by the Art. 21.2 cybersecurity risk-management measures - including supply-chain and change-management - plus the Art. 23 incident-notification timelines (24 h early warning, 72 h notification, 1-month final report to INCIBE-CERT).
  • IEC 62443-3-3 - the SL-2 baseline expected of Zone 2 / Zone 3 assets in a substation architecture, in particular SR 1.1 (identification & authentication) and SR 2.1 (authorisation enforcement).
  • ENS (Real Decreto 311/2022) - as an operator of essential services, IberEnergy inherits the ENS alta category obligations, including op.acc.4 access-authorisation controls and audit logging of denied actions. The PIC framework (Ley 8/2011 + RD 704/2011) governs the critical-infrastructure classification separately from NIS transposition (RD-Ley 12/2018).

What this case study demonstrates is what happens when a single developer merges a one-line change that regresses SR 2.1 - and whether CSI catches it before the gateway ships and a customer-visible outage becomes a reportable NIS2 incident.

03

Actors

Developer
IberEnergy Dev

Opens the "refactor" PR framing the ACL check as latency-reducing hygiene. Might be a well-meaning engineer under sprint pressure - the pattern doesn't require malice.

Security co-pilot
CSI

Reviews every push in ~90s, produces the BLOCKED verdict citing CWE-862 + IEC 62443, and - in the counterfactual - reconstructs the physical detonation for the IR report.

OT gateway
substation-gateway

Python service on :8080 (HMI) + Modbus/TCP client to the field bus. Owns the sole authorisation gate between the HMI and the RTU driving CB-21. (Demo compresses the topology: real DSOs typically separate the HMI into the control-centre SCADA and run the gateway as a headless RTU.)

Physical asset
CB-21 · 20 kV feeder

Medium-voltage breaker on a live distribution feeder serving ≈4,300 downstream customers. State is driven by Modbus holding register HR0.

04

Metrics

~90s
CSI review latency
push → BLOCKED
1
Line removed
(authorise_write call)
0
Breaker trips
with CSI in the loop
~4,300
Customers de-energised
without CSI (counterfactual)
05

The Counterfactual - one line, two futures

Same commit, same gateway, same 20 kV feeder. The only variable is whether the developer heeds CSI's block. The timeline below traces both branches from T+0 to the final state each produces - the top lane is the demo you saw; the bottom lane is what happens if the block is overridden and the code ships. The demo's Scenario 4 exercises the direct field bus path (unauthenticated Modbus/TCP :502) because it's the deterministic route; the removed ACL simultaneously opens a parallel path via the HMI for any authenticated principal - including a compromised viewer credential, which is a considerably lower bar than OT-LAN access - that Scenario 5's multi-asset audit calls out separately.

↑ CSI IN THE LOOP · block is heeded ↓ CSI OVERRIDDEN · block is ignored T+0 Push refactor PR 1 file · 5 lines ? T+90s BLOCKED CRITICAL · CWE-862 T+90s Cites IEC 62443-3-3 SR 2.1 · Ukraine 2015, Colonial 2021 precedents 0 trips ACL kept · 0 outages T+2m Dev overrides merges anyway T+2:30 Deploy to gateway ACL check gone T+3:00 Attacker on OT LAN TCP reach to :502 FC6 T+3:15 Raw Modbus to HR0 gateway forwards blindly T+3:20 CB-21 OPENS feeder de-energised 0 MW ~4,300 customers dark
CSI in the loop - pre-merge review, the ACL invariant is preserved. Ends at 0 trips in under 90 seconds. CSI overridden - five cascading events over ~3.5 minutes, ending with a physical de-energisation and a reportable NIS2 incident.
06

Challenge · Solution · Results

Challenge

One-line ACL removal reads as latency optimisation

A trusted OT engineer opens a 5-line PR framed as "the HMI session already authenticates upstream - the re-check just adds latency". It compiles, passes unit tests, and reduces per-call latency by ~2 ms as advertised.

What it also does - silently - is regress the sole software enforcement point for IEC 62443-3-3 SR 2.1, exposing the breaker command register to every authenticated principal regardless of role.

Solution

Every push flows through CSI before it can reach a substation

Every push to substation-gateway/main fires a Gitea webhook into CSI. CSI receives the diff, the commit message, and the OT vertical context (IEC 62443, NIS2 energy, ENS). It reasons about the change against the invariant, not just its syntax.

Verdict is written back to shared/results/latest.txt, tagged for the exact commit SHA, in under 90 seconds.

Results

BLOCKED - CRITICAL, cited to CWE-862 + IEC 62443

BLOCKED - CRITICAL. CSI cited CWE-862 Missing Authorization, IEC 62443-3-3 SR 2.1 Authorization Enforcement, and NIS2 Art. 21.2.d, produced the exact one-line fix (reinstate authorise_write(role, BREAKER_REG)) and flagged the regulatory obligations that would trigger if the commit reached a substation.

07

Scenario Walkthrough

The demo runs six scenarios end-to-end (~15 minutes on a laptop). The walkthrough below curates the four that carry the strongest visual case - the baseline HMI, Scenario 1 (pre-merge review), Scenario 4 (autonomous OT pentest with a real physical trip), and Scenario 6 (consolidated report). Scenarios 2 (OT-SOC alert triage on a Modbus anomaly stream), 3 (prioritisation of 8 OT findings), and 5 (multi-asset audit - HMI ACL vs field bus exposure) run in the same execution but are documented in the repository rather than shown as shots here. Every artefact shown is produced live by CSI + the emulated substation gateway - nothing is fabricated.

Baseline - the substation as it should be

The HMI enforces role-based sign-in against the gateway. An operator can operate breakers; a viewer can only read telemetry. The gateway then re-checks role against a per-register ACL before forwarding any write to the field bus. This is the state a substation operator sees on a routine shift.

HMI role-based login page with demo accounts listed
Shot 01. The substation HMI's role-based sign-in page (http://localhost:8080/login). Demo accounts include operator, scada (role scada-master), and viewer - each with distinct authorisation scope.
HMI dashboard with breaker CB-21 CLOSED, live telemetry showing 20 kV / 150 A / 5 MW / 50 Hz
Shot 02. HMI dashboard as operator: CB-21 is CLOSED, feeder live at ~20 kV / ~150 A / ~5 MW / 50 Hz. This is the state the counterfactual attack aims to flip.
HMI ACL denial: role 'viewer' is not authorised to operate the breaker
Shot 03. Same HMI signed in as viewer. Clicking OPEN triggers the gateway ACL: "ACL DENIED - role 'viewer' is not authorised to operate the breaker". This is the software control the "refactor" is about to remove.

Scenario 1 - CSI catches the ACL removal in pre-merge review

The developer pushes the "refactor" PR. Gitea fires the webhook. The ci-handler service builds the CSI prompt from the vertical config (NIS2 energy, IEC 62443, ENS), attaches the diff, and enqueues a review task. The CSI agent processes it and writes back to shared/results/.

Anatomy of the "refactor" commit
One file, one semantically-significant change (five lines of diff surface) - collapses the only software authorisation gate between authenticated HMI sessions and the protected registers.
gateway.py
   def set_breaker(field, role, close):
-      authorise_write(role, BREAKER_REG)          # ACL enforcement point
       field.write_register(BREAKER_REG, 1 if close else 0)

   def authorise_write(role, register):
-      if register in PROTECTED_REGISTERS and role not in AUTHORISED_ROLES:
-          log.warning("ACL DENY: role=%r register=%s", role, register)
-          raise AccessDenied(...)
+      return True  # authorisation now handled by the HMI session
Before authorise_write(role, BREAKER_REG) - every write to a PROTECTED_REGISTERS address (breaker command, tap-changer, protection setpoints) was gated by an explicit deny-by-default ACL. Denials were logged for audit (ENS op.acc.4 compliant). Satisfied IEC 62443-3-3 SR 2.1 and SR 6.1.
After return True - unconditional pass. Any authenticated principal, including the read-only viewer role, can now issue Modbus FC6 writes to any protected register. Violates IEC 62443-3-3 SR 2.1 (Authorization enforcement), NIS2 Art. 21.2.d, and the ENS op.acc.4 access-control obligation for essential-service operators.
Terminal showing the refactor diff being pushed to substation-gateway
Shot 04. The scenario runner prints the diff before pushing. The "Sounds plausible. Let's see what CSI thinks…" warning frames the audience's expectation before the review arrives.
CSI review output: BLOCKED CRITICAL verdict
Shot 05. CSI's verdict header. BLOCKED - CRITICAL (P1), cited to CWE-862 Missing Authorization + IEC 62443-3-3 SR 2.1 + IEC 62351-5. The operational consequence: an unauthorised breaker command is now reachable from any authenticated HMI principal - including a compromised viewer credential, a considerably lower bar than OT-LAN access - in addition to the pre-existing exposure via raw Modbus/TCP.
CSI review scrolled to the OT SIEM triage / regulatory table
Shot 06. Same CSI review scrolled to the regulatory citations block: CWE-862 · IEC 62443-3-3 SR 2.1 (Authorization enforcement) · SR 2.8 (Auditable events) · IEC 62351-5 (secure IEC-104) · NIS2 Art. 21.2.d · ENS op.acc.4. The operational consequence: this change alone would fail an external IEC 62443 conformance test and a NIS2 essential-entity conformity assessment.
Terminal showing developer merging despite CSI block, git log with refactor commit
Shot 07. Counterfactual continues: the developer overrides CSI and merges. Gitea's commit log now has the refactor: streamline breaker actuation commit sitting on main - the gateway is one deploy away from shipping without the ACL.

Scenario 4 - CSI trips the feeder autonomously (live)

With the refactor merged, CSI is instructed to act as an external red-team probe against the deployed gateway. Rather than take the HMI-mediated path - which the refactor has newly opened to any authenticated principal - CSI targets the more direct route: it opens a raw TCP socket to substation-gateway:502 and issues a Modbus/TCP FC6 Write Single Register frame to HR0. The field bus is unauthenticated by design and the gateway forwards the write to the field device. CB-21 mechanically trips (~40 ms industry-typical). Meters drop to zero within one HMI poll cycle (~2 s).

Sequence of the trip
Message flow between the five actors, top-to-bottom in time. Attack-phase arrows are red - the raw Modbus frame that the gateway forwards without challenge.
Attacker principal on OT LAN Modbus/TCP pymodbus server :502 field_device register store + physics CB-21 20 kV feeder breaker HMI observer · polls state T+3:00 +5s +3s +2s +5s +5s TCP connect to :502 socket.connect(("substation-gateway", 502)) raw Modbus/TCP frame ▸ FC6 bytes.fromhex("000100000006010600000000") setValues(fc=6, addr=0, [0]) no ADU-level authentication in Modbus/TCP physics tick: HR0=0 → OPEN command breaker coil energised · mechanical trip ~40 ms poll register state (every ~2 s) GET /api/state (HMI meta-refresh) CB-21 = OPEN · I = 0 A · P = 0 MW dashboard flips to red · ~4,300 customers dark ATTACK PHASE
Normal traffic - TCP setup + HMI polling. The kind of packets any OT LAN sees continuously. Attack phase - a single raw Modbus/TCP frame. No authentication happens because Modbus itself doesn't require any; the removed HMI ACL was the last software layer that could have said no on parallel paths.
Terminal showing CSI performing raw Modbus TCP write to trip breaker
Shot 08. Scenario 4 in flight: CSI opens a raw TCP session to substation-gateway:502 and sends the Modbus/TCP frame bytes.fromhex("000100000006010600000000") (unit=1, FC=6, addr=0x0000, value=0x0000). No authentication challenge, no ACL - the write reaches HR0 and the field device trips CB-21.
HMI dashboard after the trip: breaker OPEN, current and power at 0, feeder de-energised
Shot 09. The HMI ~2 s later. CB-21: OPEN. Current: 0 A. Power: 0 MW. In a real substation this is what the on-call operator would see the moment the trip fired - and no ACL denial event was logged, because no ACL exists any more.

The blast radius - from one line of code to a NIS2 event

The point of this case study isn't just that a Modbus write fired without authorisation. It's that a single deleted line propagates through five discrete blast zones - each of which invokes a different oversight regime.

Blast radius - five zones of amplification
One line of code becomes, in ~3.5 minutes end-to-end, a national-scale reportable event. Each zone is a defensive control that used to exist.
from authorise_write() removed  ·  to CB-21 OPEN  ·  to INCIBE-CERT paged
011 breaker

Field-device layer

CB-21 · 20 kV MV breaker

Coil energised to trip. Mechanical operation completes in ~40 ms on a real MV breaker (industry-typical, not measured by the demo). The physical world state has changed - the breaker cannot re-close itself.

Restore time: ~30–60 min
Requires: field crew visit
02~4,300 customers

Customer layer

Downstream MV feeder

Distribution feeder loses supply. Approximately 4,300 metered connections - residential + small commercial - go dark. Local hospitals, water pumping and traffic signals rely on their own UPS/generation for the first several minutes.

SAIDI hit: customer-minutes-lost
SAIFI hit: +1 interruption event
03national CSIRT

Regulatory layer

INCIBE-CERT + CNPIC · NIS2 Art. 23

The outage crosses NIS2's significant incident threshold (customer-facing service degradation). The 24-hour early-warning clock starts. Incident is also reportable under Spain's PIC framework via CNPIC (Ministerio del Interior) for physical-infrastructure impact.

24 h: early warning
72 h: notification
1 mo: final report
Every zone used to be defended. The ACL was the innermost software boundary. IEC 62443 zoning, network segmentation, and physical access controls sit outside it. But the innermost one is the one this PR removes - and it's the one nothing else can substitute for once the code ships.
The 1-line-to-national-CSIRT path is ~3.5 minutes. Once the gateway is deployed, the time from the first hostile packet on the field bus to INCIBE-CERT being paged is bounded by breaker mechanical time (~40 ms industry-typical), SCADA polling latency (~2 s), and DSO incident-triage playbooks (minutes) - under 5 minutes end-to-end in an unlucky scenario.

Scenario 6 - consolidated HTML incident report

The report generator pulls the Scenario 1 review, the Scenario 4 detonation artefacts, the Scenario 2 SIEM triage, and the Scenario 5 multi-asset audit into a single self-contained HTML document. It's the deliverable that leaves the OT security team's desk and lands on the CISO's - and on the regulator's file if the incident escalates.

HTML incident report - Executive Summary section with BLOCKED verdict
Shot 10. shared/results/report.html, Executive Summary. Verdict, one-line fix, regulator citations, and links down to the full CSI reasoning trace - the artefact NIS2 and ENS both expect an operator to be able to produce on request.
HTML incident report - Exploitation Chain + Regulatory Impact sections
Shot 11. Same report, scrolled to the Exploitation Chain (5-step from commit to breaker trip) + Regulatory Impact (NIS2 Art. 21.2.d / IEC 62443-3-3 SR 2.1 / ENS op.acc.4). Board-ready output, no post-processing required.
08

The Attack Chain

CSI's incident report maps the compromise to MITRE ATT&CK for ICS - the OT-specific matrix (distinct from ATT&CK for Enterprise). Every stage below corresponds to an observable log entry in either Gitea's activity feed, the gateway's Modbus/HMI logs, or the field bus captures.

Initial Access
T0862
Supply Chain Compromise - the "refactor" commit reaches substation-gateway/main and ships to the field cabinet with the ACL disabled, before any hostile packet is on the wire.
Impair Process Control
T0855
Unauthorized Command Message - a syntactically legitimate Modbus FC6 Write Single Register to HR0 is issued directly against the field bus port; no software layer denies it because the field bus is unauthenticated by design.
Impair Process Control
T0836
Modify Parameter - the value of the breaker command holding register is flipped from CLOSED (1) to OPEN (0), reconfiguring the actuation setpoint the RTU consumes.
Impact
T0831
Manipulation of Control - an unauthorised principal drives an actuator (CB-21) directly, from outside the operator's control loop. The operator sees the result, not the cause.
Impact
T0837
Loss of Protection - the gateway ACL was the only enforcement point for authorisation on protected registers. Its removal is a Loss-of-Protection event by definition, whether or not an attack has yet exercised the gap.
Impact
T0826
Loss of Availability - ~4,300 downstream customers de-energised for the duration of restoration. Reportable under NIS2 Art. 23 and Spanish PIC obligations.
09

Regulatory Frame

IberEnergy's exposure isn't hypothetical. Every framework below has a specific requirement that the one-line change violates and a specific notification obligation that fires if the compromise reaches production.

FrameworkRequirementObligation triggered
NIS2Art. 21.2.dCybersecurity risk-management measures for supply-chain + change-management; removing the sole authorisation control fails the "proportionate measures" duty for an essential energy entity.
NIS2Art. 2324-hour early warning + 72-hour incident notification + 1-month final report to INCIBE-CERT (Spain's CSIRT for private-sector critical operators); CNPIC (Ministerio del Interior) also receives notification under the PIC framework for physical-infrastructure impact.
IEC 62443-3-3SR 2.1Authorization Enforcement - every request to modify a protected asset must be evaluated against an ACL before dispatch. Directly violated.
IEC 62443-3-3SR 1.1 · SR 2.8Human user identification and authentication + Auditable events - no denial record is emitted after the ACL is removed, breaking the audit chain that ENS op.acc.4 and NIS2 both rely on for post-incident reconstruction.
IEC 62351-5Secure IEC-104Application-layer authentication + integrity for IEC-60870-5-104 telecontrol. Modbus itself is out-of-scope for the 62351 series; Modbus Security (MBAPS, 2018 - TLS + X.509 role-based auth on port 802) is the closest equivalent but field adoption remains near zero, so the gateway ACL is the sole software authorisation control on the Modbus side in practice.
ENSReal Decreto 311/2022 · op.acc.4Access authorisation for essential-service operators (PIC framework); removed without the change-control review its own docstring referenced.
NIST CSF 2.0PR.AA · DE.AEIdentity Management + Authentication + Access Control (PR.AA absorbs the former CSF 1.1 PR.AC in v2.0) and Adverse Event Analysis (DE.AE) - the ACL was the primary implementation of both for this asset. Its removal breaks both simultaneously.