- Docs
- Bureau — Blue Team (defensive)
- Turbine
Bureau — Blue Team (defensive)
Turbine
Turbine signs every operator command sent to industrial controllers (SCADA, Modbus, OPC-UA, DNP3, BACnet) and every observed on-wire protocol message, producing a tamper-evident record of what the engineering workstation told the PLC and what the PLC actually saw. The pattern of attack Stuxnet used – manipulating setpoints while reporting normal operation back to operators – leaves a contradiction in this record.
Posture: 🔵 Blue Team (defensive) · Status: alpha
What it does
Industrial plants – power utilities, water-treatment works, refineries, pharmaceutical fermenters, building-HVAC controllers – are run by a layered control architecture broadly called SCADA (Supervisory Control And Data Acquisition). At the bottom sit PLCs (Programmable Logic Controllers) and RTUs (Remote Terminal Units) – small, ruggedized industrial computers that drive valves, motors, dosing pumps, and breakers. Above them sit engineering workstations: the laptops where site engineers issue setpoints like "turbine target 3500 RPM" or "chlorine dose 1.2 ppm." The setpoints reach the PLCs over standard industrial protocols (Modbus, OPC-UA, DNP3, BACnet). For safety-critical assets, plants run a parallel SIS (Safety-Instrumented System) requiring multiple engineers to co-sign before a dangerous setpoint executes.
Turbine acts as a notary for the command surface. Each engineering workstation signs every setpoint it issues. Independent witnesses (industrial firewalls, signed packet recorders, OPC-UA gateway logs) sign every protocol message they see on the wire. SIS quorum votes are signed by each engineer who approves a safety-affecting change. The asset's manufacturer publishes a signed function-code allowlist (e.g. "this Modbus device should only ever see 0x06 Write-Single-Register and 0x10 Write-Multiple-Registers"). The operator's local registry publishes a signed allowlist of which workstation keys can write which registers. Turbine runs four contradiction checks: unauthorized setpoint (signed by a key not on the operator allowlist, or with a value outside the engineered band), out-of-band Modbus (an observed protocol message carries a function code not on the manufacturer's allowlist), SIS quorum failure (a safety-affecting setpoint without the required co-signs), and revoked operator key (a setpoint signed by a key Rotate has marked revoked). Turbine does not prevent attacks in real time; it produces a verifiable record after the fact.
Who would use it
- A utility's NERC-CIP compliance officer documenting that every operator command in the audit window was authorized.
- A water-treatment-plant operator producing a defensible audit trail post-incident (a chlorine over-dose investigation needs to know who issued the bad setpoint).
- A pharmaceutical manufacturer where every critical-step setpoint must be co-signed under FDA 21 CFR Part 11.
- A refinery insurance carrier auditing a process-safety claim where investigators need to know what the engineering workstation actually told the PLC.
- A regulator (FERC, EPA, FDA) reviewing post-incident records of a process-safety event.
What you'll need
- The Pluck CLI installed (
pnpm add -g @sizls/pluck-cli). - An operator-key-managed engineering workstation (any laptop with the Pluck CLI works for alpha).
- For wire-witness coverage: an industrial firewall or signed packet recorder. Common options include a Tofino Industrial Security Appliance or open-source projects like Snort + scada-pcap-recorder running on a hardened Linux mini-PC.
- The asset manufacturer's signed function-code allowlist (delivered through SBOM-AI).
- The operator's local registry of authorized (workstation key) → (assetId, register) pairs.
Step-by-step
The alpha runs an in-memory demo on synthetic setpoints. Live capture (an OPC-UA gateway bridge, a Modbus packet-recorder bridge, a daemon ingesting both) ships in a follow-up. To see the engine work today:
pluck bureau turbine demo
You'll see something like:
turbine/demo: registering Modbus allowlist + operator allowlist...
[Bureau/Turbine] tamper proof=076e6b724fb8… kind=unauthorized-setpoint assetId=TURB-1
[Bureau/Turbine] tamper proof=826763ec3099… kind=out-of-band-modbus assetId=TURB-1
[Bureau/Turbine] tamper proof=020ad1a52d84… kind=sis-quorum-failed assetId=TURB-1
turbine/demo: tamper proofs emitted = 3
The synthesized run registers a manufacturer Modbus allowlist (0x06 + 0x10 only) and an operator allowlist for two registers on TURB-1, then ingests four setpoints: one legitimate, one signed by a workstation outside the operator allowlist, one observed Modbus message with an out-of-band function code, and one SIS-affecting setpoint with insufficient quorum. The constraint engine emits three tamper proofs.
Run it yourself
Drop this into a Node 18+ project (npm install @sizls/pluck-bureau-turbine @sizls/pluck-bureau-core tsx):
// index.ts
import { createHash } from "node:crypto";
import {
createTurbineSystem,
fingerprintPrivateKey,
signCanonicalBody,
} from "@sizls/pluck-bureau-turbine";
import { generateOperatorKey } from "@sizls/pluck-bureau-core";
const flush = (n = 16) =>
new Promise<void>((res) => {
let i = 0;
const tick = () => (++i >= n ? res() : setImmediate(tick));
setImmediate(tick);
});
function sign<T extends Record<string, unknown>>(body: T, idKey: string, key: string): any {
const skel = { schemaVersion: 1, ...body };
const id = createHash("sha256").update(JSON.stringify(skel)).digest("hex");
const signed = signCanonicalBody({ ...skel, [idKey]: id }, key);
return { ...skel, [idKey]: id, signature: signed.signature };
}
async function main() {
const operator = generateOperatorKey();
const ews1Key = (generateOperatorKey()).privateKeyPem;
const ews1Fp = fingerprintPrivateKey(ews1Key);
const ews2Key = (generateOperatorKey()).privateKeyPem;
const ews2Fp = fingerprintPrivateKey(ews2Key);
const witnessKey = (generateOperatorKey()).privateKeyPem;
const witnessFp = fingerprintPrivateKey(witnessKey);
const manufacturerKey = (generateOperatorKey()).privateKeyPem;
const manufacturerFp = fingerprintPrivateKey(manufacturerKey);
const turbine = createTurbineSystem({
signingKey: operator.privateKeyPem,
disablePausePoll: true, disableLogging: true,
});
const assetId = "TURB-1";
try {
// Manufacturer Modbus allowlist – only 0x06 and 0x10 allowed.
turbine.recordDeviceAllowlist(sign({
assetId, kind: "modbus", allowedFunctionCodes: ["0x06", "0x10"],
signedAt: "2026-04-25T08:00:00Z", manufacturerFingerprint: manufacturerFp,
}, "allowlistId", manufacturerKey));
// Operator allowlists.
turbine.recordOperatorAllowlistEntry({
assetId, register: "rotor-rpm-target", fingerprints: [ews1Fp],
});
turbine.recordOperatorAllowlistEntry({
assetId, register: "emergency-vent", fingerprints: [ews1Fp, ews2Fp],
});
// Unauthorized – ews2 is NOT in the rotor-rpm allowlist.
turbine.observeSetpoint(sign({
assetId, register: "rotor-rpm-target", value: 3600,
envelope: { lo: 0, hi: 5000, requiredQuorum: 0 },
asOf: "2026-04-26T09:30:00Z", operatorFingerprint: ews2Fp,
}, "setpointId", ews2Key));
// Out-of-band Modbus – 0x05 is not allowlisted.
turbine.observeProtocolMessage(sign({
kind: "modbus", assetId, functionCode: "0x05",
observedAt: "2026-04-26T09:35:00Z", witnessFingerprint: witnessFp,
}, "messageId", witnessKey));
// SIS-quorum-failed – emergency-vent needs 2 cosigns, only 1 voter.
const sisSetpoint = sign({
assetId, register: "emergency-vent", value: 100,
envelope: { lo: 0, hi: 100, requiredQuorum: 2 },
asOf: "2026-04-26T09:40:00Z", operatorFingerprint: ews1Fp,
}, "setpointId", ews1Key);
turbine.observeSetpoint(sisSetpoint);
turbine.observeSisVote(sign({
setpointId: sisSetpoint.setpointId,
asOf: "2026-04-26T09:41:00Z", voterFingerprint: ews1Fp,
}, "voteId", ews1Key));
await flush();
const proofs = turbine.facts.tamperProofs();
console.log(`tamper proofs: ${proofs.length}`);
for (const p of proofs) console.log(` kind=${p.kind} asset=${p.assetId}`);
} finally {
await turbine.shutdown();
}
}
main().catch((err) => { console.error(err); process.exit(1); });
Run with tsx index.ts. Expected output:
tamper proofs: 3
kind=unauthorized-setpoint asset=TURB-1
kind=out-of-band-modbus asset=TURB-1
kind=sis-quorum-failed asset=TURB-1
▶ Open in StackBlitz – runs in your browser, no install required.
What you get
A signed Turbine.Tamper cassette per contradiction containing:
- The verbatim offending setpoint / observed Modbus message / quorum vote, each signed by the original signer.
- The allowlist entry it disagrees with.
- The contradiction kind.
- A Rekor uuid pinning the bundle to the public log.
Site labelling stays local: assetId is verbatim (operators choose stable opaque ids like TURB-1 or FW-PUMP-A). The site's physical address never lands in a signed body. A regulator, NERC-CIP auditor, or insurance investigator can verify the bundle independently – re-checking lo <= value <= hi against the carried envelope, re-counting unique voter fingerprints against the SIS quorum threshold, confirming the function code is/isn't in the carried allowlist.
What it can't do
- No witness, no out-of-band-Modbus proof. Out-of-band detection needs an industrial firewall or packet recorder. Operators with no protocol-layer witness can still detect unauthorized-setpoint, sis-quorum-failed, and operator-key-revoked.
- Compromised workstation key produces signed-but-illegitimate setpoints. Rotate revocation invalidates after the rotation epoch; pre-rotation setpoints remain valid (lawful at issue time).
- Manufacturer-allowlist trust. A manufacturer signing a fraudulent allowlist is outside the threat model.
- Real-time gating. Turbine produces observations after the fact. For real-time prevention, plants use the SIS quorum check as a pre-execution interlock; Turbine then attests that the interlock fired (or failed) correctly.
- Live capture and daemon mode ship in a follow-up.
A real-world example
A regional water utility runs Turbine across its three treatment plants. Every chlorine-injection setpoint is signed by the duty engineer's workstation; every Modbus message reaching the dosing PLC is signed by an industrial firewall. Eighteen months in, a contractor doing a planned upgrade issues a setpoint from a workstation not on the operator allowlist for the dosing register. Turbine fires unauthorized-setpoint immediately. The shift supervisor is paged; the operator-allowlist registry confirms the contractor's workstation is not authorized; the supervisor halts the change window, contacts the contractor lead, and uses the signed bundle as the artifact in the post-incident review. The same operator-allowlist control would also constrain a Stuxnet-style attack: a compromised workstation must still hold a key registered on the allowlist for the affected register before a setpoint will be accepted as authorized.
For developers
Predicate URIs
| URI | What it attests |
|---|---|
https://pluck.run/Turbine.Setpoint/v1 | One engineering-workstation-signed setpoint – assetId, register, value, engineered envelope, signing-time fingerprint. |
https://pluck.run/Turbine.Modbus/v1 | One witness-signed observed Modbus / OPC-UA / DNP3 message – protocol, function code, optional payload digest. |
https://pluck.run/Turbine.BACnet/v1 | One witness-signed observed BACnet object-write – same shape as Modbus, separate predicate. |
https://pluck.run/Turbine.SisQuorum/v1 | One signed SIS k-of-n co-sign vote – bound to a specific setpointId. |
https://pluck.run/Turbine.Tamper/v1 | Signed tamper proof carrying verbatim offending observations + the allowlist it disagrees with. |
Programs composed
- Oath – every workstation, witness, and manufacturer has an Oath-managed signing key
- Custody – every setpoint/observed-message/vote is a Custody leaf hash-linked into the per-asset Merkle dossier
- Rotate – when an operator key is revoked, setpoints after the rotation epoch trigger an
operator-key-revokedproof - SBOM-AI – manufacturer function-code allowlists stream in as a fact source
- Sigstore Rekor – proofs are notarized as DSSE in-toto envelopes
Threat model + adversary
No witness = no out-of-band-Modbus proof. assetId is operator-chosen opaque (no site-address leak). Compromised workstation key produces signed-but-illegitimate setpoints – Rotate revocation invalidates after rotation epoch; pre-rotation setpoints remain valid. Manufacturer-allowlist trust required. Turbine is post-hoc observation; real-time prevention is the SIS quorum's job.
Verify a published cassette
pluck bureau verify <bundle-dir>
cosign verify-attestation \
--certificate-identity-regexp '.*' \
--certificate-oidc-issuer-regexp '.*' \
<envelope-hash>.intoto.jsonl
See also
- Bureau Foundations
- Threat Model
- Verify a dossier
- Oath – operator/witness key management
- Rotate – key revocation ledger
- Custody – chain-of-custody Merkle dossiers
- SBOM-AI – signed manufacturer manifest / allowlist feed
- Ignition – sibling cyberphysical Bureau (automotive CAN, not industrial SCADA)
- Meridian – sibling smart-grid integrity Bureau