Whisper · Docs
OT · Recipes

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:

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.
the same URI already in the asset's cert SAN, now anchored in the public DNSSEC root Asset key + ApplicationUri OPC UA app-instance cert SAN 802.1AR IDevID · TPM · SE private key stays on-device public SPKI + URI /128 2a04:2a01:7a2::4840 routable · tenant-bound DNSSEC · DANE 3 1 1 · RDAP A name anyone verifies whisper verify --trustless no shared TrustList, no CA op:revoke → gone cross-org at DNS-TTL
The OPC UA ApplicationUri is already a globally-unique name bound into the asset's certificate, a good key-derived identity trapped in a private, per-site TrustList. Whisper binds it to a routable, publicly verifiable /128 and gives it the cross-org off-switch a local CRL never had.
i

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.

i

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']
}})
the manifest, finally enforced: allow what the device declared, deny the rest PLC7 · OPC UA server 2a04:2a01:7a2::4840 default-deny egress historian :4840 OPC UA controller · line 2 :502 Modbus/TCP vendor OTA :443 https MUD allow-list any other host · internet blocked op:budget (cap + kill-switch) op:revoke (off cross-org) last inch: a purely-internal Modbus/DNP3 write from an on-segment foothold still needs identity enforced in the command path
Same manifest, two fates. Pinned to a globally-verifiable identity and enforced at the /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
what your OT SOC sees: a rotating, meaningless “last IP” Suspicious session vendor jump-host · remote-access writes Modbus:502 AWS eu-central 3.68.x.x GCP europe-w 34.90.x.x Azure westeu 20.61.x.x residential-proxy swarm 71.x · Comcast 82.x · KPN 99.x · Orange JA4-identical tooling infra genealogy JA4 fingerprint One operator ASN + hosting genealogy + JA4 / JA3 fingerprint signed evidence → OT SOC / PSIRT what the graph sees: one operator
Attribution survives rotation because it tracks the infrastructure and the tooling, not the ephemeral egress IP. The read-only verbs (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.

i

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.

RecipeComplementsDoes not touch / replace
1 · Bind an asset identitythe OPC UA ApplicationUri cert (Part 6 §6.2.2) · 802.1AR IDevID · BRSKIthe OPC UA local TrustList / CA (still issues the cert); no re-keying
2 · Keyless asset verifythe app-instance cert across the vendor/integrator conduitOPC UA session security at the handshake; issues no OPC UA certs
3 · Make MUD enforceableMUD (RFC 8520) · CISA CPG 2.0 · IEC 62443-3-3 conduitsModbus / DNP3 / PROFINET auth on the wire; the in-path command write
4 · Back-trace on the graphbehavioral OT monitoring · the SOC / PSIRTICS deep-packet inspection; not a claim to evict a below-identity implant
5 · Govern & killplant segmentation · NIST SP 800-82r3 compensating controlsinline ICS-protocol inspection; adds no chokepoint on the OT segment
6 · Lookups tripwireOT asset-visibility feeds · exposure managementNothing 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