OPC-UA
The modern secure-by-design industrial protocol (IEC 62541). The stack is strong; the field deployments turn the awkward parts off, so a None endpoint and a trust-any cert list undo all of it.
OPC-UA (OPC Unified Architecture, IEC 62541) is the modern, platform-neutral industrial protocol meant to replace the old, unauthenticated field protocols like Modbus. It carries a full security stack: signed and encrypted channels, X.509 mutual authentication, user tokens, and a per-node permission model. The catch is that every one of those layers is optional, and field integrators routinely turn the awkward ones off to make an HMI connect.
What it is
OPC-UA is a service-oriented client/server protocol (it also has a pub/sub mode over UDP or MQTT, less common). Over TCP it registers on port 4840 and speaks a binary encoding wrapped in a Hello/Acknowledge handshake. Above that, a connection is built in stages:
| Stage | Decision it carries |
|---|---|
| OpenSecureChannel | SecurityMode: None / Sign / SignAndEncrypt, plus a SecurityPolicy (crypto suite) |
| CreateSession + ActivateSession | user token: Anonymous / UserName / Certificate |
| Service calls | Browse, Read, Write, Call, CreateSubscription, and the rest |
The server exposes an address space: a graph of nodes, each with a NodeId written ns=<namespace>;<id>, where the id is numeric (i=85, the Objects folder), a string (s=Tank.Setpoint), a GUID, or a bytestring. ns=0 is the standard, identical-everywhere OPC-UA base; a plant's own tags usually live in a higher namespace such as ns=2.
Why it matters
OPC-UA sits at Level 2/3 of an OT stack, brokering between HMIs, historians, and the controllers below. Because the security is layered and optional, the same four field misconfigurations recur:
- A
SecurityMode=Noneendpoint left up next to the secure one, so the whole SecureChannel can be skipped. - A trust list set to accept any self-signed client certificate ("trust-any" for onboarding, never reverted).
- Anonymous access enabled "for diagnostics" and never turned off.
- Flat permissions where every session can Write every Variable and Call every Method, because no role model was ever configured.
None of these is a crypto break. Every one is a deployment mistake, and together they turn a "secure" server into a Modbus-grade open door.
How to work it
Discovery is free: GetEndpoints is unauthenticated by design, so a server tells anyone who asks how to connect.
- Find servers by port, then confirm with a real handshake (they also bind 48010, 49320, and high ports):
nmap -p 4840,4843,48010,49320 --open <subnet>
- Pull the endpoint inventory and read SecurityMode / SecurityPolicy / accepted user tokens per endpoint:
uadiscover opc.tcp://<ip>:4840
- Browse and read the address space over the weakest endpoint (asyncua ships the
ua*CLI:uabrowse,uals,uaread):
uabrowse -u opc.tcp://<ip>:4840/plant/none -n "ns=0;i=85"
- Where permissions are flat, a
uawriteto a Variable moves a setpoint and auacallinvokes a Method that should have required authorization. Everything the CLI does you can script with the pythonasyncuaClient.
Pitfalls
- SecurityMode and the user token are orthogonal. A
Nonechannel can still ask for a username (sent in cleartext); an encrypted channel can still allow anonymous. Read both fields, not just one. - NodeId types are strict:
ns=2;s=Tank.Setpoint(string) is notns=2;i=...(numeric). Copying the wrong form gets you a BadNodeIdUnknown, not the value. Signis notSignAndEncrypt. A signed channel is tamper-evident but still cleartext on the wire, so it is readable to a sniffer.- These are real processes. A Write or a Method Call moves pumps and valves on a live plant; stay on the lab or a system you own.