OPC UA Security Policies and User Authentication: What to Enable, and What It Costs
One RSA-2048 private-key operation measured 3.363 ms on the laptop this was written on, and one pass of AES-256 over a kilobyte of notification measured about 2.1 microseconds. That ratio, roughly sixteen hundred to one, is the whole of the OPC UA performance argument, and it says something people get backwards: turning on encryption costs almost nothing, and turning on a connection costs a lot.
So the recommendation first. Enable Basic256Sha256 with SignAndEncrypt, or Aes256_Sha256_RsaPss if both ends offer it; turn the None endpoint off; use a named user rather than anonymous wherever anything can be written; and then stop worrying about the per-message cost, because it is not where your CPU is going. What deserves your attention instead is how many clients you have, how often they reconnect, and how short somebody set the security token lifetime.

Two independent choices, not one. Most arguments about OPC UA security are people using the word for one column while meaning the other.
Two settings people think are one
The policy names the algorithms. The mode names how much of the message they are applied to.
A security policy — Basic256Sha256, Aes128_Sha256_RsaOaep, Aes256_Sha256_RsaPss and the two older ones nobody should be enabling — is a bundle of choices made by the OPC Foundation and identified by a URI: the asymmetric signature and encryption algorithms used while the channel is being opened, the symmetric ones used afterwards, the key derivation function, and the minimum and maximum key lengths a certificate is allowed to have. You do not mix and match inside one; you pick a bundle. The message security mode is separate and has three values: None, Sign, SignAndEncrypt. Part 2 is blunt about the combinations: mode None may only be used with policy None, mode None is disabled for every other policy, and policy None “provides no security” and shall be disabled by default. So the grid is smaller than it looks. Either you have no security at all, or you have a real policy and a choice between Sign and SignAndEncrypt.
Nine endpoints out of ten are one of two rows.
That second choice is the one worth thinking about, and the specification itself does not insist on the expensive one.
Part 2, clause 4.8, in its own words: the choice “is dependent on the CSMS, in some applications where data confidentiality is not required, Sign is sufficient.” A standards body that had wanted to mandate encryption had the sentence available and wrote a different one.
What signing buys, and what encryption adds
Signing is the setting that does the work, and this surprises people who assume encryption is the serious one.
With Sign, every message carries a signature computed with keys derived from the nonces exchanged at channel setup, plus a sequence number, a request id, a timestamp, a SecureChannelId and a SessionId. Part 2 walks through the threats one at a time and the same mechanism answers most of them: message alteration is detected because the signature no longer verifies; message spoofing fails because the attacker cannot produce a valid signature or guess the identifiers; replay fails because the sequence numbers and timestamps are inside the signed region; session hijacking requires compromising the channel first, because a session is bound to one. Application authentication — the certificate exchange that decides whether an unknown client may connect at all — happens on any policy that is not None, which means a Sign-only endpoint still refuses strangers. Encryption adds exactly one thing to that list: an observer with a span port or a tap cannot read your values and setpoints. That is worth having and it is not worth pretending to be more than it is.

The last row is there on purpose. A laptop plugged into the panel with an engineering tool defeats every column, and no OPC UA setting is the control for it.
One row of that table catches people out, so it is worth saying separately. A user name and password crossing a Sign-only channel is not in the clear. Part 4 defines the UserNameIdentityToken as encrypted according to the security policy of the UserTokenPolicy, which is a separate field in the endpoint description from the channel’s own policy, and it requires the server to specify one whenever the channel is not SignAndEncrypt. The password gets encrypted with the server’s public key regardless. What Sign leaves readable is the process data, not the credential.
What it costs, measured
Here is the part usually asserted rather than measured, so it was measured.
OpenSSL 3.5.6 on an Intel laptop, two-second runs. An RSA-2048 private-key operation — the signature a client makes when it opens a channel, and the decryption the server does at the other end — ran at 297 per second, which is 3.363 ms each. The matching public-key operation ran at 9967 per second, 0.100 ms. At 4096 bits the private operation collapses to 43.7 per second, 22.9 ms, while the public operation only slows to 0.380 ms. Symmetric work is in a different world: AES-256-CBC managed 478 MB/s on 1024-byte blocks and SHA-256 managed 607 MB/s, so signing and encrypting a one-kilobyte notification costs under four microseconds of CPU between them. Put those together and an OpenSecureChannel exchange, which costs each end roughly two private-key operations and two public-key ones, comes to something like 7 ms of RSA at 2048 bits and about 46 ms at 4096. The same 7 ms would encrypt and sign about 1.6 megabytes of notifications. A subscription delivering a hundred one-kilobyte notifications every second is spending about 0.4 ms of CPU per second on cryptography — four hundredths of one per cent.
These are laptop numbers. A controller’s CPU is slower by a large and vendor-specific factor, and I am not going to invent that factor.
What transfers is the ratio, and the ratio is not subtle: the handshake is three orders of magnitude dearer than the traffic. Doubling the key length multiplies the handshake by nearly seven and leaves the traffic alone.

A log axis, because a linear one would draw the bottom two bars as nothing. That is the finding: on the data path they are nothing.
Where the cost actually lands
Not on the values. On the connections, and on one setting nobody looks at.
Asymmetric cryptography happens at exactly three moments in a session’s life: OpenSecureChannel, CreateSession and ActivateSession. After that the channel runs on symmetric keys derived from the nonces, and stays there — until the security token expires and the client re-runs OpenSecureChannel to get a new one. The client asks for a lifetime and the server revises it, and Part 4 explains why the mechanism exists at all: “several cryptanalytic attacks become easier as more material encrypted with a specific key is available”, so a channel carrying a lot of data wants a shorter one. What that means arithmetically is direct. A one-hour token is one handshake per client per hour. A five-minute token is twelve. With twelve clients on a gateway that is 144 handshakes an hour instead of 12, and on a small CPU with a lot of clients that is the number that becomes measurable rather than anything to do with encryption.
Nobody sets that number. It arrives from a stack default and stays there for a decade.
Then there is the reconnect, which is the same cost arriving all at once.
A switch reboots, a link flaps, a gateway restarts: every client that was connected re-opens its channel, re-creates its session and re-activates it, in the same second. Twelve clients is thirty-six asymmetric operations arriving together on a CPU that also has a program to scan. That is why a controller with an embedded server sometimes looks fine for weeks and then hiccups after a network event, and why spreading reconnect attempts — most clients have a retry backoff setting — is worth more than any policy choice. If you want the number for your own hardware, there is only one honest way to get it: note the maximum scan time, clear it, run for a day with the server enabled and the real client count, and look again. Do it once with a network interruption in the middle.

Four boxes cost microseconds and one costs milliseconds, and the last box is the first box happening again on a timer you chose.
When anonymous is a defensible answer
It sometimes is, and saying so is more useful than a recommendation nobody follows.
Anonymous access on a cell network that has no route to anywhere else is a reasonable engineering decision, and the conditions that make it reasonable are worth stating precisely rather than waved at. The network segment carries the controller, its HMI and the one gateway, and nothing on it routes to the business network except through a firewall that permits one direction and one port. The physical access to that segment is the same physical access that would let somebody put a laptop on the controller’s own port with the vendor’s engineering tool, at which point every software control in this article is beside the point. The anonymous role is read-only, which on a modern server is enforced per node rather than assumed — an S7-1500 from V3.1 gives the Anonymous role no namespace permissions at all by default, which is the same idea arriving from the other direction. And nobody is on that segment with a machine that is also on another network.
That last condition is the one that fails, quietly, about eighteen months later.
An engineering laptop with a corporate wireless connection and a cable into the cell is a bridge, whatever the intent. A contractor’s machine is another. A wireless access point somebody added for a tablet is a third. The honest version of the anonymous decision is not “it is a closed network” but “it is a closed network and here is the list of machines that touch it, and we will look at that list again when it changes” — and if you cannot produce that list, use a named user, because you have just discovered that you do not know what is on the segment. The switching and VLAN side of keeping a cell separate is where that conversation actually happens.
Writing is a different matter from reading, and the line is clean: anonymous reading on a segregated cell is defensible, anonymous writing is not. A setpoint change with no name attached to it is an audit trail with a hole in it, and the moment somebody asks why the line ran at the wrong recipe for a shift, that hole is the whole investigation.
The four identities, and the roles they attach to
Part 4 defines four kinds of user identity token and they differ mostly in what they cost you to operate rather than in how strong they are.
Anonymous carries no user information. UserName carries a name and a password, the server checks it against its own list, and the operating cost is that somebody has to own those passwords — Part 2 says plainly that passwords shall not be hardcoded into an application and shall be manageable by administrative users, which is the sentence every integrator’s commissioning script violates. X509 carries a user certificate and the proof is a signature rather than a secret, which is stronger and costs you a second PKI with its own expiry dates and its own trust store, separate from the application one. Issued tokens come from an authorization service, OAuth2 or a JWT, and cost you an identity server that has to stay up for a line to run. Whichever you pick, permissions attach through roles rather than through the token itself, and the specification names a standard set — SecurityAdmin, ConfigureAdmin, Supervisor, Engineer, Operator, Observer and AuthenticatedUser — with the least-privilege guidance that a client should be granted the minimum it needs, and that the admin roles should not be granted except while somebody is actively doing that job. A client can change identity mid-session with ActivateSession, which is the mechanism for exactly that, and the server re-evaluates the permissions on every monitored item when it happens.

Column three is the one to read. Every row above the first buys accountability and charges an annual maintenance fee for it.
Roles are where the permissions live. The token only decides which role you get.
One behaviour to know before it confuses you at three in the morning. Part 4 tells servers to defend against repeated invalid identity tokens, either by locking out the client for a period or by delaying the response and increasing that delay with each failure. A reconnect loop with a stale password does not fail fast — it fails slower and slower, which reads exactly like a network problem and is not one.
Where the line is
Careful is: no None endpoint, Basic256Sha256 or better, SignAndEncrypt because it is nearly free, a certificate per application rather than one copied around, a named user on anything that writes, and a token lifetime you chose rather than inherited.
Paranoid is: 4096-bit keys on a controller with a hundred clients, a five-minute token lifetime on a link carrying four tags, a full CA hierarchy with revocation lists for six devices in one cell, and X.509 user certificates for an operator who logs into one HMI. Each of those is defensible somewhere and none of them is defensible everywhere, and all four are bought with time that would be better spent on the segregation the MES and business-network conversation will need anyway.
Next step
Read the endpoint list your own server is publishing, before you change anything, because that list is what a client sees and it is frequently not what somebody thinks they configured. Any client will show it — point it at the discovery endpoint without connecting and it will enumerate every combination of policy, mode and user token the server offers. If there is a None row in there, that is the first thing to remove, and it costs a restart rather than a project. Then set the token lifetime deliberately and write the number in the handover.
When the client refuses to connect afterwards, which it will, the certificate side of it is the four refusals a Python OPC UA client meets against an S7-1500, and the full walkthrough of both ends of a working link is OPC UA PLC integration from a PLC tag to a SCADA client.