MUD egress · verify · attribute
Six copy-paste recipes that give every industrial asset (a PLC, an RTU, an IED, an HMI, an OPC UA server) an identity it can prove, an egress you can govern to one line, and an operator you can name behind a rotating remote-access session. Each runs today against the keyless surface anyone can reach.
They share one primitive: a routable IPv6 /128 that an asset derives from a key it already holds, such as the public half of its OPC UA application-instance certificate, an 802.1AR IDevID, a TPM, or a secure element. The /128 is DNSSEC-signed and DANE-pinned to that same key, so any peer, integrator or regulator can verify it and nobody can forge it, with no commercial CA and no shared TrustList. Everything below is built from shipped parts: the asset /128, keyless verify, the attribution graph, egress governance (policy/firewall/budget), lookups, and one-call revoke. Where a step is a pattern you assemble rather than a single command, or a connector still on the roadmap, it says so in plain words.
Two tiers, by design (Postel's law: liberal in what we accept). The keyless half (verify, reverse-DNS, RDAP, and a read of your own identity's lookups) needs no API key: any counterparty runs it with stock dig/curl. The control half (provision, policy, firewall, budget, revoke) needs your key on POST https://graph.whisper.security/api/query. Both halves appear below, marked.
What's shipped, stated honestly. The asset /128 derivation (public key + device_id = the OPC UA ApplicationUri or an asset serial), the keyless verify / RDAP surface, the attribution graph, egress governance, op:lookups, and one-call revoke are live. Two caveats these recipes are written around: there is no first-class --applicationuri / --mud CLI flag yet, so asset provisioning is shown via the control-plane API (which is live); and the STIX · TAXII / ISAC export connector named at the end is roadmap: the Splunk, Microsoft Sentinel and OpenCTI connectors ship today. Whisper anchors the IP / DNS / transport boundary: it governs who may reach and speak to an asset and never reaches into the Modbus, DNP3 or PROFINET wire. Each recipe names exactly where it stops.
The shared primitive: the address is the asset
An industrial asset already carries a hardware key and a globally-unique name. In OPC UA the ApplicationUri is that name: Part 4 §7 defines it as the "globally unique identifier for the application instance," and Part 6 §6.2.2 requires the application-instance certificate SAN to carry exactly one uniformResourceIdentifier equal to it (a session fails with BadCertificateUriInvalid if the two disagree). So OPC UA already binds a key-derived name to the asset's key. But trust is a local per-site TrustList, and commercial/public CAs are explicitly discouraged, so no peer operator, integrator or regulator can check your asset without joining that TrustList. Whisper takes only the public SubjectPublicKeyInfo (SPKI) of that same key and, with the ApplicationUri as the domain separator, deterministically derives a /128 under 2a04:2a01::/32 (AS219419). The private key never leaves the asset.
The derivation has four properties that make it an identity rather than just an address:
- Deterministic & idempotent. The same key + ApplicationUri always yields the same
/128. Re-flashing a replacement controller for the same asset returns the same address; there is no registry to keep in sync. - Tenant-bound & fleet-unlinkable. The derivation folds in your operator tenant, so the same asset under two operators yields two unrelated
/128s. An outsider cannot link a unit across the vendor/integrator boundary by suffix, and there is no enumeration oracle. - Forge-proof. The
/128is DANE-EE3 1 1-pinned to that asset's own key and has DNSSEC-signed reverse DNS. A cloned device with a different key derives a different address and can't present the pinned key. - Revocable cross-org. One control-plane call tears the
/128, its PTR, and its DANE pin down everywhere at DNS-TTL speed: the shared off-switch a local per-site CRL never had.
Because the domain separator is the ApplicationUri, the ApplicationUri alone yields nothing: it is a structured identifier that flows through every OPC UA deployment and is not a secret, yet you cannot go ApplicationUri → /128 without the key, there is no enumerable directory, and RDAP and reverse-DNS return the registry object, never the asset's live whereabouts. Once an asset holds one, every check in the keyless recipes below runs with no account: just DNS and TLS any counterparty already has. Even a dumb Modbus/TCP:502 PLC behind a protocol gateway gets a verifiable network identity, a PTR and an RDAP object for the first time.
Recipe 1: Bind an asset to the OPC UA ApplicationUri it already carries
Shipped & live. Deriving an asset /128 from the device key it already holds, keyed by its OPC UA ApplicationUri (or asset serial), is in production today. Provision one with the control-plane call below, then drop the address straight into your asset register / OPC UA EndpointDescription: no re-keying, no new CA.
Pass the asset's base64 SPKI as identity_public_key and its ApplicationUri as device_id: that URI is the exact one already bound into the certificate SAN. Re-running with the same key + ApplicationUri returns the same /128. The heredoc keeps the single-quoted Cypher literals intact, so this runs as-is:
# Bind an asset: device_id = its OPC UA ApplicationUri (the urn: already in its cert SAN).
# Idempotent: same key + ApplicationUri returns the SAME /128, no registry to sync.
curl -s https://graph.whisper.security/api/query \
-H "X-API-Key: whisper_live_xxx" \
-H 'content-type: application/json' \
--data @- <<'JSON'
{"query":"CALL whisper.agents({op:'connect', args:{tier:'wireguard', identity_public_key:'<base64 SPKI of the asset key>', device_id:'urn:example-plant:line2:PLC7:server'}}) YIELD op, ok, status, result, error RETURN op, ok, status, result, error"}
JSON
# response
{ "op": "connect", "ok": true, "status": "created",
"result": {
"address": "2a04:2a01:7a2::4840",
"fqdn": "opcua-plc7.asset.<tenant>.agents.whisper.online",
"wireguard": { /* peer, keys, allowed-ips: source-bind the asset's egress to its /128 */ }
} }
Confirm the address and its forward-confirmed name, then place the /128 wherever your inventory expects it: a CMDB row, the OPC UA EndpointDescription, an IEC 62443 asset register:
# The address is the asset: forward-confirmed reverse DNS names it (keyless):
dig +short -x 2a04:2a01:7a2::4840
opcua-plc7.asset.<tenant>.agents.whisper.online.
/128 and gives it the cross-org off-switch a local CRL never had.The call is liberal in what it accepts, strict in what it commits: the same asset key with a different device_id on your tenant is a clear 409, not a silent overwrite; a non-string device_id is a 400 that tells you exactly what was wrong, never an opaque 500. The device_id is generic: pass an OPC UA ApplicationUri, an 802.1AR IDevID serial, or a bare nameplate serial, whatever native identifier the asset carries. A dedicated --applicationuri / --mud CLI flag is on the roadmap; today, provisioning is the control-plane call above (which is live). The shipped CLI verbs are whisper verify --trustless, whisper create --register, whisper kill --revoke, whisper policy, and whisper logs. See CLI & one-command.
Recipe 2: Prove an asset's identity, keyless
OPC UA's own certificate proves the asset: to a party that already holds it in a local TrustList. Across the conduit (to a peer operator, an integrator, a vendor's remote-maintenance service, or a regulator), no one can chain your per-site trust to a public anchor. The /128 closes that: any party re-derives and checks the identity trustlessly against the IANA DNSSEC root, with no Whisper API and no shared TrustList trusted as an authority. No key, no account, no login.
Boundary. This complements the OPC UA application-instance certificate and its local TrustList; it does not replace them and issues no OPC UA certificates. It adds the publicly-verifiable check (the one an integrator, a peer operator, or an auditor can run without joining your TrustList) at exactly the cross-org conduit the per-site PKI can't cross.
The CLI walks the full proof chain in one line; the same check runs either side of a conduit:
# Trustless: re-derived against the IANA DNSSEC root; no Whisper API, no TrustList trusted.
whisper verify --trustless opcua-plc7.asset.<tenant>.agents.whisper.online
dnssec pass DNSSEC-root AAAA, PTR and TLSA(3 1 1) all DNSSEC-validated to the IANA root
dane pass DNSSEC-root served leaf SPKI-SHA256 == TLSA pin (the asset's own OPC UA cert key)
ledger pass DNSSEC-root transparency-log entry present, signature verifies
CRYPTOGRAPHICALLY PROVEN. Trust anchor: DNSSEC root (IANA) + DANE-EE. Whisper API NOT trusted
No CLI required: the same fact answers over one keyless HTTPS call, and again with nothing but dig -x, the tool already in every ops toolbox:
# The public verify endpoint: evidence chain in JSON, no account:
curl -s https://whisper.online/verify-identity/2a04:2a01:7a2::4840 | jq
{"is_whisper_agent":true,"dane_ok":true,"jws_ok":true,
"evidence":{"address":"2a04:2a01:7a2::4840","ptr":"…","forward_aaaa":"2a04:2a01:7a2::4840"}}
# Because the reverse is forward-confirmed (PTR ⇄ AAAA), dig -x alone proves the name:
dig +short -x 2a04:2a01:7a2::4840
opcua-plc7.asset.<tenant>.agents.whisper.online.
# And a device with no registered identity says so plainly (no key required):
curl -s https://whisper.online/verify-identity/2a04:2a01::1
{"is_whisper_agent":false,"detail":"no Whisper agent identity anchors this address"}
That is mutual, forge-proof peer verification at the layer OPC UA's own model leaves open across organisations: neither side depends on a token that could be replayed from another host, and neither side has to trust a CA, or a TrustList, the other one chose.
Recipe 3: Make the device's MUD declaration enforceable at the /128
This is the OT differentiator. Under MUD (RFC 8520), an industrial device already declares exactly what it should communicate with: a manufacturer-signed manifest of from-device-policy and to-device-policy ACLs (RFC 8519), emitted over DHCP (options 161 / 112), an LLDP vendor TLV (type 127), or the id-pe-mud-url X.509 extension (OID 1.3.6.1.5.5.7.1.25) co-located with an 802.1AR IDevID. MUD's fatal weakness: the manifest is fetched from the manufacturer's domain and enforced by a local MUD manager at the nearest switch, so the rules are only a suggestion, and a compromised device still reaches anything the local admin didn't think to block. Bind that declaration to the asset's verifiable /128 and enforce it as default-deny egress governance where traffic actually leaves, cross-org and checkable by anyone.
Keyless to read, keyed to enforce. The device's MUD URL can be DANE-pinned and checked with no key (below). Setting the policy is the control half: it takes your API key.
Start from the device's own manifest: the manufacturer already ships it. Here it is, abbreviated to the two ACEs that matter:
# The device's own MUD manifest (RFC 8520), CMS-signed by the manufacturer, abbreviated.
# Its MUD URL can be DANE-pinned and checked keyless, exactly like Recipe 2:
dig +short TLSA _443._tcp.mud.vendor.example
3 1 1 b653a4ef…fcb82d1d
{ "ietf-mud:mud": { "mud-url": "https://mud.vendor.example/plc-x/v2",
"to-device-policy": { "access-lists": {"access-list":[{"name":"cl-from-historian"}]} },
"from-device-policy": { "access-lists": {"access-list":[{"name":"cl-to-controller"}]} } },
"ietf-access-control-list:acls": { "acl": [ { "name":"cl-to-controller",
"aces":{"ace":[{ "matches":{"ipv6":{"dst-dnsname":"controller.line2.local"}},
"actions":{"forwarding":"accept"} }]} } ] } }
Now translate the manifest's declared destinations into default-deny egress governance bound to the asset's /128. Egress is source-bound to that address, so policy is enforced by name and by address: a conduit at asset granularity, not a VLAN:
# The MUD declaration, enforced: this PLC may reach ONLY the destinations it declared:
# its historian, its controller, and its vendor OTA endpoint. Everything else is denied.
whisper policy set --default deny \
--allow historian.example-plant.com,controller.line2.local,ota.vendor.example
# The same as a per-asset firewall over the control plane (allow/deny by host, cidr or port).
# Ports are the real ones: 4840 = OPC UA, 502 = Modbus/TCP. Send this to
# POST https://graph.whisper.security/api/query with your X-API-Key.
CALL whisper.agents({op:'firewall', args:{
agent:'2a04:2a01:7a2::4840', default:'deny',
allow:['historian.example-plant.com:4840', 'controller.line2.local:502', 'ota.vendor.example:443']
}})
/128 where traffic actually egresses, the manufacturer's declaration of intent finally becomes a cryptographic, cross-org control, not a suggestion at the nearest hop. The dashed box marks the honest edge, spelled out below.This directly implements the CISA CPG 2.0 pattern (an IPv6-addressable asset register plus logical segmentation permitting only required communications) and the IEC 62443-3-3 conduit, but keyed to a cryptographically verifiable identity rather than a spoofable MUD URL or a VLAN tag. See Egress governance for the full policy surface.
The honest edge. Egress governance changes who may reach and speak to an asset, and constrains what a governed asset may reach. It contains C2, exfil and lateral movement across the convergence bridge and the remote-access surface. It does not add authentication to Modbus, DNP3 or PROFINET on the wire, and it does not stop a purely-internal manipulation by an attacker who already holds an OT-segment foothold: closing that last inch needs identity enforced in the command path, at the PLC, a protocol-aware broker-gateway, or the EWS. We say so before your assessor does.
Recipe 4: Back-trace the operator behind a suspicious remote-access session
Identity stops the next forgery; the graph names the operator behind a session already in your logs: attribution that survives IP rotation because it fingerprints the operator and the tooling, not the ephemeral egress IP. OT environments run remote-access sprawl (Claroty Team82: 55% run 4+ tools, 33% run 6+), so a destructive write can arrive over a vendor's jump-host or a rotating proxy and all your OT SOC ever logs is a meaningless last IP. Behavioral OT monitoring is excellent inside the plant. But it stops at the Purdue boundary and the firewall. The graph collapses the rotation.
Boundary, stated plainly. This is an additive feed into the OT monitoring, the SOC, and the PSIRT you already run; Whisper does not do deep-packet ICS-protocol inspection. And it is honest about reach: attribution names the operator behind a rotating egress. It does not, by itself, evict a below-identity-layer implant, a stolen-but-enrolled identity, or an insider at the console. Use it as the who/where accelerator for incident handling, not a claim to have stopped a nation-state-class campaign.
Pass the suspicious address to whisper.identify over the public graph API with your key. There is no whisper identify CLI verb: the read-only Cypher call below is the interface:
curl -s https://graph.whisper.security/api/query \
-H "X-API-Key: whisper_live_xxx" \
-H 'content-type: application/json' \
-d '{"query":"CALL whisper.identify(\"45.83.x.x\")"}' | jq
operator: <fingerprinted> · seen across AWS, GCP and Azure + a remote-access pool
proxy swarm collapsed by JA4: same tooling, 41 exit IPs → 1 operator
identify, origins, walk, variants, history) each return a reproducible, replayable JSON evidence chain your OT SOC, PSIRT, and a regulator can replay.Two levers, kept honestly separate: infrastructure genealogy clusters cloud rotation by shared ASN, hosting and certificate lineage; a JA4/JA3 client fingerprint collapses a rotating remote-access or residential-proxy pool to one operator, invisible to the proxy because it lives in the TLS handshake. More in Graph & cognition.
Recipe 5: Govern asset egress, cap it and kill it worldwide
Recipe 3 set the allow-list; this is the rest of the control plane: the cap and the off-switch. Because the asset's /128 resolves through Whisper's own graph-first resolver and its egress is source-bound to that address, you can put a hard ceiling on what a live asset moves and cut it off in one call. This is the containment that a flat network structurally lacks: compromise one asset and you've compromised that asset, not the plant.
Boundary. This rides existing DNS / IPv6 and adds no inline packet chokepoint on the OT segment. It is not an ICS-protocol firewall, and it does not sit in the PLC's command path. The verify plane is built to fail open: a Whisper outage never bricks an asset; checks degrade to your existing anchors. It complements the plant's own segmentation and monitoring; it never replaces them.
# Cap the asset's egress and arm the kill-switch. Send to
# POST https://graph.whisper.security/api/query with your X-API-Key.
CALL whisper.agents({op:'budget', args:{
agent:'2a04:2a01:7a2::4840', max_mb_per_day:50, on_exceed:'kill'
}})
# A compromised PLC, a module swap, a change of integrator, a decommission:
# one call tears down the /128, its PTR and its DANE pin everywhere at DNS-TTL speed.
CALL whisper.agents({op:'revoke', args:{agent:'2a04:2a01:7a2::4840'}})
# The CLI does the same teardown in one verb:
whisper kill --revoke 2a04:2a01:7a2::4840
After the TTL, every keyless check in Recipe 2 flips to is_whisper_agent:false for that address everywhere on the planet, and dig -x returns nothing: no CRL to distribute, no per-vendor blocklist to push, no fleet-wide firmware flash. Revocation is the cross-org off-switch OPC UA's local TrustList never had.
Nothing issued in the dark. Every provision and every revoke lands in a public, append-only RFC 6962 Merkle transparency log, Ed25519-signed and anchored to Bitcoin via OpenTimestamps: an auditable, non-repudiable issuance/revocation trail for an EU CRA filing, an IEC 62443 assessment or a TSA review. Honest status: tamper-evident and Bitcoin-anchored today, but not yet independently witnessed (it already speaks the C2SP tlog-witness protocol, so an external witness can co-sign). And you can bind each historian feed or telemetry stream to the asset's forge-proof /128, and sign it under that same identity, so an integrator, an auditor or downstream settlement can trust the numbers came from the real asset, not a spoofed source on the flat network.
Recipe 6: See who probed or enumerated your asset
Discovery is Stage 1 of the OT kill chain: before anyone speaks to your PLC, they scan and resolve it: Censys catalogued 145,000+ internet-exposed ICS services across 175 countries, and Shodan indexes the rest. Because every asset identity resolves through Whisper's own authoritative DNS and RDAP, the owner sees exactly who looked: a reconnaissance tripwire the OPC UA local, out-of-band TrustList never gave you. This is the reverse of op:logs (the asset's own outbound): op:lookups answers who checked this asset. It reads keyless for your own address:
# The recon tripwire: who has been resolving / RDAP-querying this asset's identity?
# A spike in PTR/AAAA/TLSA/RDAP lookups from an unexpected origin = someone enumerating
# before the write lands. Early warning, not a post-mortem. No key required:
curl -s https://whisper.online/ip/2a04:2a01:7a2::4840/lookups | jq
{ "address":"2a04:2a01:7a2::4840",
"lookups":[ {"kind":"PTR","count":128,"first":"…","last":"…"},
{"kind":"TLSA","count":74,"…":"…"},
{"kind":"rdap","count":19,"…":"…"} ] }
# With your key, the same over the control plane, per asset, alongside op:logs:
CALL whisper.agents({op:'lookups', args:{agent:'2a04:2a01:7a2::4840', window:'24h'}})
Paired with op:logs (the asset's own outbound activity) and /ip/<addr>/transparency (its ordered lifecycle), you have both halves of the picture: what an asset reaches out to, and who is reaching in to look at it. And pair the tripwire with Recipe 4: an unexpected surge of lookups against a line's PLCs, then whisper.identify on the querying source, turns "someone is casing the plant" from a hunch you find in the post-mortem into a named operator you can act on before the write lands.
Where each recipe stops, and what's roadmap
Honest scoping, one row per recipe. Whisper anchors the IP / DNS / transport boundary; the ICS protocols on the wire, the OPC UA local TrustList, and the plant's own segmentation stay exactly where they are.
| Recipe | Complements | Does not touch / replace |
|---|---|---|
| 1 · Bind an asset identity | the OPC UA ApplicationUri cert (Part 6 §6.2.2) · 802.1AR IDevID · BRSKI | the OPC UA local TrustList / CA (still issues the cert); no re-keying |
| 2 · Keyless asset verify | the app-instance cert across the vendor/integrator conduit | OPC UA session security at the handshake; issues no OPC UA certs |
| 3 · Make MUD enforceable | MUD (RFC 8520) · CISA CPG 2.0 · IEC 62443-3-3 conduits | Modbus / DNP3 / PROFINET auth on the wire; the in-path command write |
| 4 · Back-trace on the graph | behavioral OT monitoring · the SOC / PSIRT | ICS deep-packet inspection; not a claim to evict a below-identity implant |
| 5 · Govern & kill | plant segmentation · NIST SP 800-82r3 compensating controls | inline ICS-protocol inspection; adds no chokepoint on the OT segment |
| 6 · Lookups tripwire | OT asset-visibility feeds · exposure management | Nothing else (a keyless read of your own identity's query record) |
Roadmap, clearly labelled. Streaming this evidence into a SIEM ships today for Splunk, Microsoft Sentinel and OpenCTI (signed, replayable JSON → CEF / ECS fields). A STIX 2.1 over TAXII feed and a sector-ISAC (E-ISAC / WaterISAC) machine-readable export for sharing revocation and attribution evidence are proposed, not yet available. And the first-class typed --applicationuri / --mud CLI flags are roadmap: provision and govern via the control-plane API shown above, which is live. Nothing on this list is required for the six recipes; they run on shipped primitives alone. Every proposed integration sits at the asset / IP boundary, complements the stack you already run, and is never named against a specific operator as a breach victim.
Next
- Asset & PLC identity: the full derivation the
/128above comes from, key + ApplicationUri to address - DANE & TLSA: the
3 1 1pin every check here rests on, byte for byte - Verify an agent: the full keyless proof chain
whisper verify --trustlesswalks - IEC 62443 · EU CRA · TSA: where these recipes map into assessment and incident-reporting evidence