| status | draft | ||
|---|---|---|---|
| type | spec | ||
| depends-on |
|
The most important architectural property in IRON. Every design decision is evaluated against it.
READ: Sensors → Edge → NATS (data subjects) → Storage → UI
(one direction, always, no exceptions)
WRITE: Operator → Auth → RBAC → Command Service → Audit → NATS (command subjects)
→ Edge → Machine
(explicit, authorized, fully logged, confirmed)
A visualization bug must be incapable of sending a command to a machine. In classical SCADA monoliths (WinCC, Wonderware, MasterSCADA) these paths are entangled in a single process. That is the architectural mistake IRON refuses to make.
- READ and WRITE paths MUST NOT share code, processes, or configuration files.
- Tags (READ) live in
config/tags/. Commands (WRITE) live inconfig/commands/. A tag definition MUST NOT contain a writable target. A command definition MUST NOT be referenced by display widgets except through the Command Service API. - The alarm engine is a READ-path consumer. An alarm MUST NOT trigger a command. See alarm-engine.md for why, and for what to do instead.
# config/commands/reactor_01.yaml — physically separate from tags/
reactor_01:
pump_start:
target: modbus://plc-01/coil/0x0020
type: bool
requires_confirm: true
requires_role: operatorLogical separation is not enough: both paths traverse the same NATS broker, so the separation MUST also be enforced at the transport authorization layer.
- Data and commands MUST use disjoint subject namespaces:
data.>andcmd.>(no subject belongs to both). - Each edge agent MUST hold credentials that allow it to publish only to its
own data subjects (
data.<site>.<area>.>) and subscribe only to its own command subjects (cmd.<site>.<area>.>). An agent's credentials MUST NOT permit publishing tocmd.>— a compromised edge device cannot inject commands to other zones. - Only the Command Service identity may publish to
cmd.>. No other component — not the dashboard, not the alarm engine, not the historian — holds publish rights on command subjects. - All NATS connections MUST use TLS; agent credentials SHOULD use mTLS or NATS NKeys with per-device keys, revocable individually.
This makes the separation a property of broker authorization, not a convention in application code. A compromise of any READ-path component yields zero capability to write. Full threat model: security.md.
- iron-server's Command Service is the only WRITE entry point. It is a distinct crate with its own credentials and no dependency on the tag engine or UI modules; the UI reaches it through one narrow, auditable API.
- The UI-facing role MAY be deployed on a separate host with no command
credentials at all (
iron server --role ui/--role core— deployment.md, contours in security.md). The separation then holds even if the UI host is fully compromised: the keys are not there to steal. - The edge agent's command executor is a distinct module from the polling
engine. It executes only commands received on its authorized command
subjects, validates them against
config/commands/, and refuses anything not declared there.
- A compromised dashboard sending spurious commands to machines
- A developer accidentally wiring a read widget to a write endpoint
- Privilege escalation from the UI layer to the command layer
- A compromised edge device in one area writing to another area
A malicious insider with valid operator credentials, a compromised iron-server host, or a vulnerability in the Command Service itself can still send authorized-looking commands. Defense in depth — audit logging, confirmation, RBAC scoping, command rate limits (command-path.md) — exists for exactly this reason. And safety-critical protection never depends on IRON at all: see "What IRON does not claim" in security.md.